1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package main
28
29 import (
30 "bytes"
31 "cmp"
32 "flag"
33 "fmt"
34 "log"
35 "os"
36 "path/filepath"
37 "regexp"
38 "slices"
39 "strings"
40
41 "simd/archsimd/_gen/gentools"
42 "simd/archsimd/_gen/specgen"
43 )
44
45
46 func paths(goroot string) (md, dot string) {
47 specDir := specgen.MustFindSpecDir(goroot)
48 genDir := filepath.Join(filepath.Clean(filepath.Join(specDir, "..", "..")), "archsimd", "_gen")
49 return filepath.Join(genDir, "SPEC-TRANSITION.md"), filepath.Join(genDir, "SPEC-TASKS.dot")
50 }
51
52
53 func load(md string) *doc {
54 data, err := os.ReadFile(md)
55 if err != nil {
56 log.Fatal(err)
57 }
58 return parse(string(data))
59 }
60
61 func main() {
62 log.SetFlags(0)
63 log.SetPrefix("spectasks: ")
64 genOpts := gentools.RegisterFlags(nil)
65 flag.Parse()
66
67 md, _ := paths(genOpts.GOROOT)
68 d := load(md)
69
70
71
72
73 if probs := d.problems(); len(probs) > 0 {
74 for _, s := range probs {
75 fmt.Fprintf(os.Stderr, "spectasks: %s\n", s)
76 }
77 log.Fatalf("%s disagrees with itself; go test ./cmd/spectasks reports this properly", filepath.Base(md))
78 }
79
80 var files gentools.Files
81 defer files.FlushOrExit()
82
83 buf := files.NewRawFile("simd/archsimd/_gen/SPEC-TASKS.dot")
84 d.dot(buf)
85 }
86
87
88 type dep struct {
89 id string
90 }
91
92 type task struct {
93 id string
94 done bool
95 track string
96 title string
97 needs, prefers []dep
98
99 critical bool
100 }
101
102 type track struct {
103 letter, name string
104 tasks []string
105 }
106
107 type milestone struct {
108 name string
109 needs []dep
110 }
111
112 type doc struct {
113 tasks map[string]*task
114 order []string
115 tracks []track
116 milestones []milestone
117
118
119 part4 map[string]struct{ needs, prefers, blocks []dep }
120 }
121
122
123
124
125 type mdTable struct {
126 head []string
127 rows [][]string
128 }
129
130
131
132 func cells(line string) []string {
133 s := strings.TrimSpace(line)
134 if !strings.HasPrefix(s, "|") || !strings.HasSuffix(s, "|") {
135 return nil
136 }
137 parts := strings.Split(s[1:len(s)-1], "|")
138 for i, p := range parts {
139 parts[i] = strings.TrimSpace(p)
140 }
141 return parts
142 }
143
144
145
146
147 func findTable(lines []string, head ...string) *mdTable {
148 for i, line := range lines {
149 got := cells(line)
150 if !slices.Equal(got, head) {
151 continue
152 }
153 if i+1 >= len(lines) || !strings.HasPrefix(strings.TrimSpace(lines[i+1]), "|-") {
154 log.Fatalf("table %q at line %d has no separator row", head, i+1)
155 }
156 t := &mdTable{head: got}
157 for _, row := range lines[i+2:] {
158 c := cells(row)
159 if c == nil {
160 break
161 }
162 if len(c) != len(head) {
163 log.Fatalf("line %d: got %d cells, want %d", i+1, len(c), len(head))
164 }
165 t.rows = append(t.rows, c)
166 }
167 return t
168 }
169 log.Fatalf("no table with header %q", head)
170 return nil
171 }
172
173 var (
174
175 depRE = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
176
177 headingRE = regexp.MustCompile("^### `([a-z0-9]+(?:-[a-z0-9]+)*)` — ")
178
179 fieldRE = regexp.MustCompile(`\*\*(Needs|Prefers|Blocks):\*\* ([^·]*)`)
180
181 markupRE = regexp.MustCompile("[`*]")
182 )
183
184
185
186 func parseDeps(s string) []dep {
187 s = strings.TrimSpace(markupRE.ReplaceAllString(s, ""))
188 if s == "" || s == "—" || s == "-" {
189 return nil
190 }
191 var out []dep
192 for _, f := range strings.Split(s, ",") {
193 f = strings.TrimSpace(f)
194 if !depRE.MatchString(f) {
195 log.Fatalf("cannot parse dependency %q in %q; want a task id", f, s)
196 }
197 out = append(out, dep{f})
198 }
199
200
201 slices.SortFunc(out, func(a, b dep) int { return cmp.Compare(a.id, b.id) })
202 return out
203 }
204
205 func parse(text string) *doc {
206 lines := strings.Split(text, "\n")
207 d := &doc{
208 tasks: make(map[string]*task),
209 part4: make(map[string]struct{ needs, prefers, blocks []dep }),
210 }
211
212 for _, r := range findTable(lines, "", "Track", "What it is", "Tasks").rows {
213 var tasks []string
214 for _, dep := range parseDeps(r[3]) {
215 tasks = append(tasks, dep.id)
216 }
217 d.tracks = append(d.tracks, track{
218 letter: markupRE.ReplaceAllString(r[0], ""),
219 name: r[1],
220 tasks: tasks,
221 })
222 }
223
224 for _, r := range findTable(lines, "✓", "T", "Task", "What it is", "Needs", "Prefers").rows {
225 id := markupRE.ReplaceAllString(r[2], "")
226 if !depRE.MatchString(id) {
227 log.Fatalf("bad task id %q; want lowercase words joined by hyphens", r[2])
228 }
229 if d.tasks[id] != nil {
230 log.Fatalf("task %q listed twice", id)
231 }
232 d.tasks[id] = &task{
233 id: id,
234 done: strings.Contains(r[0], "x"),
235 track: r[1],
236 title: markupRE.ReplaceAllString(r[3], ""),
237 needs: parseDeps(r[4]),
238 prefers: parseDeps(r[5]),
239 }
240 d.order = append(d.order, id)
241 }
242
243 for _, r := range findTable(lines, "Milestone", "Reached after", "What it means").rows {
244 d.milestones = append(d.milestones, milestone{
245 name: markupRE.ReplaceAllString(r[0], ""),
246 needs: parseDeps(r[1]),
247 })
248 }
249
250
251 cur := ""
252 for _, line := range lines {
253 if m := headingRE.FindStringSubmatch(line); m != nil {
254 cur = m[1]
255 continue
256 }
257 if cur == "" || !strings.HasPrefix(line, "**Done:** [") {
258 continue
259 }
260 var e struct{ needs, prefers, blocks []dep }
261 for _, m := range fieldRE.FindAllStringSubmatch(line, -1) {
262 switch m[1] {
263 case "Needs":
264 e.needs = parseDeps(m[2])
265 case "Prefers":
266 e.prefers = parseDeps(m[2])
267 case "Blocks":
268 e.blocks = parseDeps(m[2])
269 }
270 }
271 d.part4[cur] = e
272 cur = ""
273 }
274 return d
275 }
276
277
278
279 func ids(ds []dep) []string {
280 out := make([]string, len(ds))
281 for i, d := range ds {
282 out[i] = d.id
283 }
284 return out
285 }
286
287 func fmtIDs(xs []string) string {
288 if len(xs) == 0 {
289 return "—"
290 }
291 return strings.Join(xs, ", ")
292 }
293
294
295
296
297 func (d *doc) problems() []string {
298 var probs []string
299 fail := func(format string, args ...any) {
300 probs = append(probs, fmt.Sprintf(format, args...))
301 }
302
303
304 seen := make(map[string]string)
305 for _, tr := range d.tracks {
306 for _, id := range tr.tasks {
307 if prev, ok := seen[id]; ok {
308 fail("task %s is in tracks %s and %s", id, prev, tr.letter)
309 }
310 seen[id] = tr.letter
311 if t := d.tasks[id]; t == nil {
312 fail("track %s lists task %s, which the dependency table does not have", tr.letter, id)
313 } else if t.track != tr.letter {
314 fail("task %s is in track %s per the track table, %s per the dependency table", id, tr.letter, t.track)
315 }
316 }
317 }
318
319
320
321
322 pos := make(map[string]int, len(d.order))
323 for i, id := range d.order {
324 pos[id] = i
325 }
326
327
328
329 blocks := make(map[string][]string)
330 for _, id := range d.order {
331 t := d.tasks[id]
332 if seen[id] == "" {
333 fail("task %s is in no track", id)
334 }
335 for _, e := range slices.Concat(t.needs, t.prefers) {
336 if d.tasks[e.id] == nil {
337 fail("task %s depends on task %s, which does not exist", id, e.id)
338 }
339 }
340 for _, e := range t.needs {
341 if d.tasks[e.id] != nil && pos[e.id] > pos[id] {
342 fail("task %s needs %s but comes before it; the table must be in topological order", id, e.id)
343 }
344 blocks[e.id] = append(blocks[e.id], id)
345 }
346 for _, e := range t.prefers {
347 if d.tasks[e.id] != nil && pos[e.id] > pos[id] {
348 fail("task %s prefers %s but comes before it; the table must be in topological order", id, e.id)
349 }
350 }
351 p4, ok := d.part4[id]
352 if !ok {
353 fail("task %s has no Part 4 entry", id)
354 continue
355 }
356 if !slices.Equal(ids(t.needs), ids(p4.needs)) {
357 fail("task %s Needs: table says %s, Part 4 says %s", id, fmtIDs(ids(t.needs)), fmtIDs(ids(p4.needs)))
358 }
359 if !slices.Equal(ids(t.prefers), ids(p4.prefers)) {
360 fail("task %s Prefers: table says %s, Part 4 says %s", id, fmtIDs(ids(t.prefers)), fmtIDs(ids(p4.prefers)))
361 }
362 }
363 for _, id := range d.order {
364 want := blocks[id]
365 slices.Sort(want)
366 if got := ids(d.part4[id].blocks); !slices.Equal(got, want) {
367 fail("task %s Blocks: header says %s, reversing Needs gives %s", id, fmtIDs(got), fmtIDs(want))
368 }
369 }
370
371
372
373
374 tdeps := make(map[string]map[string]bool, len(d.order))
375 var computeDeps func(string) map[string]bool
376 computeDeps = func(id string) map[string]bool {
377 if r, ok := tdeps[id]; ok {
378 return r
379 }
380 r := make(map[string]bool)
381 tdeps[id] = r
382 for _, e := range slices.Concat(d.tasks[id].needs, d.tasks[id].prefers) {
383 if d.tasks[e.id] != nil {
384 r[e.id] = true
385 for dep := range computeDeps(e.id) {
386 r[dep] = true
387 }
388 }
389 }
390 return r
391 }
392 for _, id := range d.order {
393 computeDeps(id)
394 }
395 for i, id := range d.order {
396 for _, id2 := range d.order[i+1:] {
397 if d.tasks[id].track > d.tasks[id2].track && !tdeps[id2][id] {
398 fail("task %s (track %s) precedes %s (track %s) but %s does not depend on %s; unrelated tasks sort by track",
399 id, d.tasks[id].track, id2, d.tasks[id2].track, id2, id)
400 }
401 }
402 }
403
404 for _, m := range d.milestones {
405 for _, e := range m.needs {
406 if d.tasks[e.id] == nil {
407 fail("milestone %q depends on task %s, which does not exist", m.name, e.id)
408 }
409 }
410 }
411
412 return probs
413 }
414
415
416
417
418
419 func (d *doc) mark() {
420 var into, outOf func(string) int
421 memo := func(f func(string) int) func(string) int {
422 m := make(map[string]int)
423 return func(id string) int {
424 if v, ok := m[id]; ok {
425 return v
426 }
427 m[id] = 1
428 v := f(id)
429 m[id] = v
430 return v
431 }
432 }
433
434 into = memo(func(id string) int {
435 best := 0
436 for _, e := range d.tasks[id].needs {
437 if d.tasks[e.id] != nil {
438 best = max(best, into(e.id))
439 }
440 }
441 return best + 1
442 })
443
444 outOf = memo(func(id string) int {
445 best := 0
446 for _, other := range d.order {
447 for _, e := range d.tasks[other].needs {
448 if e.id == id {
449 best = max(best, outOf(other))
450 }
451 }
452 }
453 return best + 1
454 })
455
456 span := 0
457 for _, id := range d.order {
458 span = max(span, into(id))
459 }
460 for _, id := range d.order {
461 d.tasks[id].critical = into(id)+outOf(id)-1 == span
462 }
463 }
464
465
466 func (d *doc) reached(m milestone) bool {
467 for _, e := range m.needs {
468 if !d.tasks[e.id].done {
469 return false
470 }
471 }
472 return len(m.needs) > 0
473 }
474
475
476
477
478 var trackFill = []string{
479 "#dce9f7", "#d9f2e2", "#fbe6cd", "#ebdcf7", "#f7dce3", "#d8f0f0", "#e8e8e8",
480 }
481
482
483
484 func wrap(s string, width int) string {
485 var lines []string
486 line := ""
487 for _, w := range strings.Fields(s) {
488 switch {
489 case line == "":
490 line = w
491 case len([]rune(line))+1+len([]rune(w)) <= width:
492 line += " " + w
493 default:
494 lines = append(lines, line)
495 line = w
496 }
497 }
498 return strings.Join(append(lines, line), `\n`)
499 }
500
501
502
503 func node(id string) string { return quote(id) }
504
505 func quote(s string) string {
506 return `"` + strings.NewReplacer(`"`, `\"`, "\n", `\n`).Replace(s) + `"`
507 }
508
509 func (d *doc) dot(w *bytes.Buffer) {
510 d.mark()
511 p := func(format string, args ...any) { fmt.Fprintf(w, format+"\n", args...) }
512
513 p(`// Task graph for SPEC-TRANSITION.md. Do not edit.`)
514 p(`// Generated by archsimd/_gen/cmd/spectasks; regenerate with:`)
515 p(`// go run ./cmd/spectasks -w`)
516 p(`// Render with, e.g.:`)
517 p(`// dot -Tsvg SPEC-TASKS.dot -o SPEC-TASKS.svg`)
518 p(`//`)
519 p(`// The key in the rendered graph explains the notation.`)
520 p(``)
521 p(`digraph spectransition {`)
522 p(` rankdir=LR;`)
523 p(` newrank=true;`)
524 p(` ranksep=0.55; nodesep=0.22;`)
525 p(` fontname="Helvetica"; fontsize=11;`)
526 p(` node [shape=box, style="rounded,filled", fillcolor=white,`)
527 p(` fontname="Helvetica", fontsize=10, margin="0.10,0.06"];`)
528 p(` edge [color="#666666", arrowsize=0.7];`)
529 p(``)
530
531
532
533
534
535 emit := func(t *task, fill string) {
536 attrs := []string{
537 "label=" + quote(t.id+`\n`+wrap(t.title, 22)),
538 "fillcolor=" + quote(fill),
539 `color="#999999"`,
540 }
541 if t.done {
542 attrs = append(attrs, `fillcolor="#d0d0d0"`, `fontcolor="#777777"`)
543 }
544 p(` %s [%s];`, node(t.id), strings.Join(attrs, ", "))
545 }
546
547
548
549
550 seen := make(map[string]bool, len(d.order))
551 for i, tr := range d.tracks {
552 for _, id := range tr.tasks {
553 if t := d.tasks[id]; t != nil && !seen[id] {
554 seen[id] = true
555 emit(t, trackFill[i%len(trackFill)])
556 }
557 }
558 }
559 for _, id := range d.order {
560 if !seen[id] {
561 emit(d.tasks[id], "white")
562 }
563 }
564
565 p(``)
566
567 for _, id := range d.order {
568 t := d.tasks[id]
569 for _, e := range t.needs {
570 var attrs []string
571 if t.critical && d.tasks[e.id].critical {
572 attrs = append(attrs, `color="#c0392b"`, `penwidth=1.8`)
573 }
574 p(` %s -> %s%s;`, node(e.id), node(id), attrList(attrs))
575 }
576 for _, e := range t.prefers {
577 p(` %s -> %s [style=dotted, color="#aaaaaa", arrowsize=0.5];`, node(e.id), node(id))
578 }
579 }
580 p(``)
581
582 for i, m := range d.milestones {
583 fill := "#fff8dc"
584 if d.reached(m) {
585 fill = "#d7d7d7"
586 }
587 p(` m%d [label=%s, shape=box, style="filled,bold", fillcolor=%s, color="#8a6d3b"];`,
588 i, quote(wrap(m.name, 22)), quote(fill))
589 for _, e := range m.needs {
590 p(` %s -> m%d [color="#8a6d3b", style=bold, arrowsize=0.7];`, node(e.id), i)
591 }
592 }
593
594
595
596
597
598 p(``)
599 p(` key [shape=plaintext, style="", label=<`)
600 p(` <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="1" CELLPADDING="3" BGCOLOR="#fafafa">`)
601 p(` <TR><TD ALIGN="LEFT" COLSPAN="2"><B>Tracks</B></TD></TR>`)
602 for i, tr := range d.tracks {
603 p(` <TR><TD BGCOLOR="%s" WIDTH="18"> </TD><TD ALIGN="LEFT">%s</TD></TR>`,
604 trackFill[i%len(trackFill)], html(tr.letter+" — "+tr.name))
605 }
606 p(` <TR><TD COLSPAN="2"><FONT POINT-SIZE="5"> </FONT></TD></TR>`)
607 p(` <TR><TD ALIGN="LEFT" COLSPAN="2"><B>Reading the graph</B></TD></TR>`)
608
609
610 arrow := func(color, glyph string) string {
611 return fmt.Sprintf(`<FONT COLOR="%s" POINT-SIZE="14">%s</FONT>`, color, glyph)
612 }
613 swatch := func(fill, border string) string {
614 return fmt.Sprintf(`<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0" `+
615 `COLOR="%s" BGCOLOR="%s"><TR><TD WIDTH="16" HEIGHT="9"> </TD></TR></TABLE>`, border, fill)
616 }
617 for _, row := range [][2]string{
618 {arrow("#666666", "→"), "must be done first"},
619 {arrow("#aaaaaa", "⇢"), "better done first, but not required"},
620 {arrow("#c0392b", "→"), "on a longest chain: slipping it slips the project"},
621 {swatch("#fff8dc", "#8a6d3b"), "milestone: reached, not worked on"},
622 {swatch("#d0d0d0", "#999999"), "done"},
623 } {
624 p(` <TR><TD ALIGN="RIGHT">%s</TD><TD ALIGN="LEFT">%s</TD></TR>`, row[0], html(row[1]))
625 }
626 p(` </TABLE>`)
627 p(` >];`)
628 p(`}`)
629 }
630
631
632 func html(s string) string {
633 return strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace(s)
634 }
635
636 func attrList(attrs []string) string {
637 if len(attrs) == 0 {
638 return ""
639 }
640 return " [" + strings.Join(attrs, ", ") + "]"
641 }
642
View as plain text