1
2
3
4
5 package main
6
7 import (
8 "cmp"
9 "fmt"
10 "path/filepath"
11 "regexp"
12 "simd/archsimd/_gen/specgen"
13 "slices"
14 "strings"
15
16 "gopkg.in/yaml.v3"
17 )
18
19
20
21
22
23
24 type specOp struct {
25 name string
26 covPairs int
27 }
28
29
30
31
32
33
34 type relation int
35
36 const (
37 relEqual relation = iota
38 relSubset
39 relSuperset
40 relOverlap
41 relDisjoint
42 relSpecOnly
43 relAPIOnly
44 numRelations
45 )
46
47
48
49 func (r relation) shared() bool { return r != relSpecOnly && r != relAPIOnly }
50
51 var relationName = [numRelations]string{
52 relEqual: "api = spec",
53 relSubset: "api ⊂ spec",
54 relSuperset: "api ⊃ spec",
55 relOverlap: "overlapping",
56 relDisjoint: "disjoint",
57 relSpecOnly: "api = ∅",
58 relAPIOnly: "spec = ∅",
59 }
60
61
62 type nameRel struct {
63 name string
64 rel relation
65 api, spec int
66 apiOnly []string
67 specOnly int
68 }
69
70
71
72 type relTotal struct{ names, apiOnly, specOnly int }
73
74
75 type fileCount struct {
76 file string
77 n int
78 }
79
80
81
82
83 type metrics struct {
84 facts []fact
85 decls []*decl
86 spec map[methodKey]*specgen.Func
87
88
89 covTriple *tally[archMethodKey]
90 covPair *tally[methodKey]
91 covName *tally[string]
92 covTarget *pivot[archMethodKey]
93 covGen *pivot[methodKey]
94 covMulti *tally[string]
95 covCommon *tally[string]
96
97 specPairs, specNames int
98
99
100 ops []specOp
101 specOnly []string
102 names []nameRel
103 rel [numRelations]relTotal
104 apiOnly, apiOnlyDecls int
105
106
107
108
109 agree, typeOK *tally[declKey]
110 paramNameOK, resultNameOK *tally[declKey]
111 paramsFilled, resultsFilled *tally[declKey]
112
113
114 consistent, typeSame, paramNameSame, resultNameSame *tally[methodKey]
115
116
117 fromSpec *tally[declKey]
118 bounded *tally[declKey]
119 handConflict int
120 handConflictDecls []string
121 handConflictByFile []fileCount
122 crossPkg *tally[methodKey]
123 mirror *tally[methodKey]
124
125
126 categories map[string]int
127 catEntries, catUniq, catExported int
128 catCompilerOnly int
129 catExportedFields map[string]int
130 comments int
131 commentsSpecCovered int
132 commentsSpecCoveredNames []string
133 commutDrift int
134 }
135
136 func measure(api []*decl, specFuncs []*specgen.Func, genDir string) *metrics {
137 m := &metrics{spec: map[methodKey]*specgen.Func{}}
138
139 specNames := map[string]bool{}
140 for _, f := range specFuncs {
141 m.spec[specPair(f)] = f
142 specNames[f.Name] = true
143 }
144 m.specPairs, m.specNames = len(m.spec), len(specNames)
145
146
147 for _, d := range api {
148 if nonSpecDecl(d) {
149 continue
150 }
151 m.decls = append(m.decls, d)
152 for _, t := range d.targets {
153 m.facts = append(m.facts, fact{d, t})
154 }
155 }
156
157 covered := func(f fact) bool { return m.spec[f.methodKey()] != nil }
158
159
160 m.covTriple = tallyBy(m.facts, fact.archMethodKey, covered)
161 m.covPair = tallyBy(m.facts, func(f fact) methodKey { return f.methodKey() }, covered)
162 m.covName = tallyBy(m.facts, func(f fact) string { return f.name }, func(f fact) bool { return specNames[f.name] })
163 m.covTarget = pivotBy(m.facts, func(f fact) string { return f.target }, fact.archMethodKey, covered)
164 m.covGen = pivotBy(m.facts, func(f fact) string { return f.gen }, func(f fact) methodKey { return f.methodKey() }, covered)
165 for _, g := range m.covGen.dims() {
166 if t, _, _ := strings.Cut(g, "/"); t != g {
167 m.covGen.fold(g, t)
168 }
169 }
170
171
172
173
174 tools := groupBy(m.facts, func(f fact) string { return f.name })
175 m.covMulti = new(tally[string])
176 for name, fs := range tools {
177 seen := map[string]bool{}
178 for _, f := range fs {
179 if f.gen != "hand-written" {
180 seen[f.tool()] = true
181 }
182 }
183 if len(seen) > 1 {
184 m.covMulti.add(name, specNames[name])
185 }
186 }
187 m.covCommon = new(tally[string])
188 for _, n := range commonMethods(api) {
189 m.covCommon.add(n, m.covName.done(n))
190 }
191
192
193 byOp := map[string]*specOp{}
194 for _, f := range specFuncs {
195 src, _, _ := f.SpecFunc()
196 op := byOp[src]
197 if op == nil {
198 op = &specOp{name: src}
199 byOp[src] = op
200 }
201 if m.covPair.has(specPair(f)) {
202 op.covPairs++
203 }
204 }
205 for _, op := range byOp {
206 if op.covPairs == 0 {
207
208
209
210
211
212 m.specOnly = append(m.specOnly, op.name)
213 }
214 m.ops = append(m.ops, *op)
215 }
216 slices.Sort(m.specOnly)
217 slices.SortFunc(m.ops, func(a, b specOp) int { return cmp.Compare(a.name, b.name) })
218
219
220
221
222 apiRecv, specRecv := map[string]map[string]bool{}, map[string]map[string]bool{}
223 add := func(m map[string]map[string]bool, name, recv string) {
224 if m[name] == nil {
225 m[name] = map[string]bool{}
226 }
227 m[name][recv] = true
228 }
229 for _, d := range m.decls {
230 add(apiRecv, d.name, d.recv)
231 }
232 for p := range m.spec {
233 if !nonSpecOp(p.name) {
234 add(specRecv, p.name, p.recv)
235 }
236 }
237 for name := range apiRecv {
238 m.names = append(m.names, nameRelation(name, apiRecv[name], specRecv[name]))
239 }
240 for name := range specRecv {
241 if apiRecv[name] == nil {
242 m.names = append(m.names, nameRelation(name, nil, specRecv[name]))
243 }
244 }
245 slices.SortFunc(m.names, func(a, b nameRel) int { return cmp.Compare(a.name, b.name) })
246 for _, r := range m.names {
247 t := &m.rel[r.rel]
248 t.names++
249 t.apiOnly += len(r.apiOnly)
250 t.specOnly += r.specOnly
251 if r.rel.shared() {
252 m.apiOnly += len(r.apiOnly)
253 }
254 }
255
256
257 m.apiOnlyDecls = countBy(filter(m.facts, func(f fact) bool {
258 return !covered(f) && specNames[f.name]
259 }), fact.declKey)
260
261
262 overlap := filter(m.facts, covered)
263 specIn := func(f fact) []param { return specParams(m.spec[f.methodKey()].In) }
264 specOut := func(f fact) []param { return specParams(m.spec[f.methodKey()].Out) }
265 typeOK := func(f fact) bool {
266 return equalBy(f.in, specIn(f), paramType) && equalBy(f.out, specOut(f), paramType)
267 }
268 resultNameOK := func(f fact) bool { return !namesClash(f.out, specOut(f)) }
269 paramNameOK := func(f fact) bool { return !namesClash(f.in, specIn(f)) }
270 resultsFilled := func(f fact) bool { return !namesMissing(f.out, specOut(f)) }
271 paramsFilled := func(f fact) bool { return !namesMissing(f.in, specIn(f)) }
272
273 m.typeOK = tallyBy(overlap, fact.declKey, typeOK)
274 m.resultNameOK = tallyBy(overlap, fact.declKey, resultNameOK)
275 m.paramNameOK = tallyBy(overlap, fact.declKey, paramNameOK)
276 m.resultsFilled = tallyBy(overlap, fact.declKey, resultsFilled)
277 m.paramsFilled = tallyBy(overlap, fact.declKey, paramsFilled)
278 m.agree = tallyBy(overlap, fact.declKey, func(f fact) bool {
279 return typeOK(f) && resultNameOK(f) && paramNameOK(f)
280 })
281
282
283
284
285 m.consistent = new(tally[methodKey])
286 m.typeSame = new(tally[methodKey])
287 m.paramNameSame = new(tally[methodKey])
288 m.resultNameSame = new(tally[methodKey])
289 for p, ds := range groupBy(m.decls, (*decl).methodKey) {
290 if len(ds) < 2 {
291 continue
292 }
293 params := func(d *decl) []param { return d.in }
294 results := func(d *decl) []param { return d.out }
295 typesAgree := func(get func(*decl) []param) bool {
296 return !slices.ContainsFunc(ds[1:], func(d *decl) bool {
297 return !equalBy(get(ds[0]), get(d), paramType)
298 })
299 }
300 namesAgree := func(get func(*decl) []param) bool {
301 lists := make([][]param, len(ds))
302 for i, d := range ds {
303 lists[i] = get(d)
304 }
305 return namesConsistent(lists)
306 }
307 t := typesAgree(params) && typesAgree(results)
308 pn, rn := namesAgree(params), namesAgree(results)
309 m.typeSame.add(p, t)
310 m.paramNameSame.add(p, pn)
311 m.resultNameSame.add(p, rn)
312 m.consistent.add(p, t && pn && rn)
313 }
314
315
316
317
318
319
320 documented := filter(overlap, func(f fact) bool { return len(m.spec[f.methodKey()].Doc) > 0 })
321 saysSpec := func(f fact) bool {
322 specDoc, err := specgen.FormatComment(m.spec[f.methodKey()].Doc)
323 if err != nil {
324 return false
325 }
326 return strings.HasPrefix(f.doc, strings.TrimRight(specDoc, "\n"))
327 }
328 m.fromSpec = tallyBy(documented, fact.declKey, saysSpec)
329 handConflicts := filter(documented, func(f fact) bool {
330 return f.gen == "hand-written" && f.doc != "" && !saysSpec(f)
331 })
332 m.handConflict = countBy(handConflicts, fact.declKey)
333 byFile := map[string]int{}
334 seenByFile := map[declKey]bool{}
335 for _, f := range handConflicts {
336 dk := f.declKey()
337 if !seenByFile[dk] {
338 seenByFile[dk] = true
339 byFile[f.file]++
340 m.handConflictDecls = append(m.handConflictDecls, fmt.Sprint(dk))
341 }
342 }
343 slices.Sort(m.handConflictDecls)
344 for _, k := range sortedKeys(byFile) {
345 m.handConflictByFile = append(m.handConflictByFile, fileCount{k, byFile[k]})
346 }
347
348 m.bounded = tallyBy(filter(m.facts, func(f fact) bool { return f.doc != "" }),
349 fact.declKey, func(f fact) bool { return f.hasSep })
350
351
352
353
354 m.crossPkg = new(tally[methodKey])
355 docs := map[string]map[methodKey]string{"simd": {}, "archsimd": {}}
356 for _, d := range m.decls {
357 if d.doc != "" {
358 docs[d.pkg][d.methodKey()] = d.doc
359 }
360 }
361 for p, sd := range docs["simd"] {
362 if ad, ok := docs["archsimd"][p]; ok {
363 ssd, sad := stripDocNotes(sd), stripDocNotes(ad)
364 m.crossPkg.add(p, ssd != "" && ssd == sad)
365 }
366 }
367
368
369
370 m.mirror = new(tally[methodKey])
371 stubs, emul := map[methodKey]string{}, map[methodKey]string{}
372 for _, d := range api {
373 switch d.file {
374 case "simd_stubs.go":
375 stubs[d.methodKey()] = d.doc
376 case "simd_emulated.go":
377 emul[d.methodKey()] = d.doc
378 }
379 }
380 for p, s := range stubs {
381 if e, ok := emul[p]; ok {
382 m.mirror.add(p, s == e)
383 }
384 }
385
386
387 cat := makeCategoriesInventory(genDir)
388 m.categories = cat.allFields
389 m.catEntries = cat.entries
390 m.catUniq = cat.uniq
391 m.catExported = cat.exported
392 m.catCompilerOnly = cat.compilerOnly
393 m.catExportedFields = cat.exportedFields
394 m.comments = countMidwayComments(genDir)
395
396
397 root := readYAML(commentsPath(genDir))
398 eachEntry(root, func(k, v *yaml.Node) {
399 if isString(v) && specNames[k.Value] {
400 m.commentsSpecCovered++
401 m.commentsSpecCoveredNames = append(m.commentsSpecCoveredNames, k.Value)
402 }
403 })
404
405
406 m.commutDrift = commutativityDrift(genDir, m.spec)
407 return m
408 }
409
410
411 func nameRelation(name string, api, spec map[string]bool) nameRel {
412 r := nameRel{name: name, api: len(api), spec: len(spec)}
413 shared := 0
414 for recv := range api {
415 if spec[recv] {
416 shared++
417 } else {
418 r.apiOnly = append(r.apiOnly, recv)
419 }
420 }
421 slices.Sort(r.apiOnly)
422 r.specOnly = len(spec) - shared
423 switch {
424 case len(spec) == 0:
425 r.rel = relAPIOnly
426 case len(api) == 0:
427 r.rel = relSpecOnly
428 case len(r.apiOnly) == 0 && r.specOnly == 0:
429 r.rel = relEqual
430 case len(r.apiOnly) == 0:
431 r.rel = relSubset
432 case r.specOnly == 0:
433 r.rel = relSuperset
434 case shared == 0:
435 r.rel = relDisjoint
436 default:
437 r.rel = relOverlap
438 }
439 return r
440 }
441
442
443
444 func commutativityDrift(genDir string, spec map[methodKey]*specgen.Func) int {
445 var n int
446 paths, _ := filepath.Glob(filepath.Join(genDir, "simdgen", "ops", "*", "categories.yaml"))
447 slices.Sort(paths)
448 for _, p := range paths {
449 root := readYAML(p)
450 if root == nil || root.Kind != yaml.SequenceNode {
451 continue
452 }
453 for _, entry := range root.Content {
454 if entry.Kind != yaml.MappingNode {
455 continue
456 }
457 goName := mapValue(entry, "go")
458 if goName == nil {
459 continue
460 }
461 isExported := goName.Value != "" && !(goName.Value[0] >= 'a' && goName.Value[0] <= 'z')
462 if !isExported || mapValue(entry, "noTypes") != nil {
463 continue
464 }
465 commNode := mapValue(entry, "commutative")
466 if commNode == nil {
467 continue
468 }
469 catCommut := commNode.Value == "true"
470 re, err := regexp.Compile("^" + goName.Value + "$")
471 if err != nil {
472 continue
473 }
474 for p, f := range spec {
475 if re.MatchString(p.name) && f.Commutative != catCommut {
476 n++
477 break
478 }
479 }
480 }
481 }
482 return n
483 }
484
485 func filter[T any](s []T, keep func(T) bool) []T {
486 var out []T
487 for _, v := range s {
488 if keep(v) {
489 out = append(out, v)
490 }
491 }
492 return out
493 }
494
495
496 func specPair(f *specgen.Func) methodKey {
497 var recv string
498 if f.Recv.Type != nil {
499 recv = f.Recv.Type.String()
500 }
501 return methodKey{recv, f.Name}
502 }
503
504
505
506 func stripDocNotes(doc string) string {
507 var keptParas []string
508 var curPara []string
509 flush := func() bool {
510 if len(curPara) == 0 {
511 return true
512 }
513 first := strings.TrimSpace(curPara[0])
514 if strings.HasPrefix(first, "//go:") || strings.HasPrefix(first, "//line ") {
515 return false
516 }
517 text := strings.TrimPrefix(first, "// ")
518 text = strings.TrimPrefix(text, "//")
519 if specgen.HasNotePrefix(text) {
520 return false
521 }
522 keptParas = append(keptParas, strings.Join(curPara, "\n"))
523 curPara = nil
524 return true
525 }
526
527 for _, line := range strings.Split(doc, "\n") {
528 trimmed := strings.TrimSpace(line)
529 if strings.HasPrefix(trimmed, "//go:") || strings.HasPrefix(trimmed, "//line ") {
530 if !flush() {
531 return strings.Join(keptParas, "\n//\n")
532 }
533 break
534 }
535 if trimmed == "//" {
536 if !flush() {
537 return strings.Join(keptParas, "\n//\n")
538 }
539 continue
540 }
541 curPara = append(curPara, line)
542 }
543 flush()
544 return strings.Join(keptParas, "\n//\n")
545 }
546
View as plain text