this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

internal/core/adt: new data structure for reqSets

The typo checking algorithm associates a defID
with conjuncts of nodes that keep track from which
definitions or embeddings conjuncts originate.

Previously, each node kept track of each relevant
set of definitions and embeddings by keeping
track of all allowed ids within each set. This
results in polynomial space usage.

The new algorithm, instead, keep track of the
global parent relationship. To check if a
conjuncts is part of the set is than a matter
of repeatedly following the parent of the
conjunct until the ID matches the that of the
group or until the top is reached (not a member).

In addition, the algorithm needs to handle
"equivalences", tracked by replaceIDs. If the
same Vertex is added more than once, it is only
added once. It may originate, however, from
different closedness contexts. The redirects
keep track of this equivalence.

Note that the parent relationsip is now tracked
with a single allocation per defID. So there
is only a constant overhead per defID to track
the sets (apart from redirects), to track set
inclusion, instead of polynomial space overhead.
The time complexity for a lookup of one conjunct
is O(H), where H is the height of the closedness
inclusion tree.
The set of redirects per node is typically
small and should not significantly contribute
to memory usage. It is tracked by MaxRedirect.

This change can result in significant speedup.
For the Issue that prompted this change total
running time saw a reduction of 50%.

This change uncovered some bugs of the previous
algorithm: if conjuncts were added from Vertices
that were created from other OpContexts, the
defIDs were included as is. Of course they were
meaningless and determining whether they were
part of the group would result in more or less
random results.

In the new algorithm, the defID is used as an
index. This could result in a panic if the
defID was larger than the largest defID in the
current context.

Generally speaking, defIDs are overwritten by
the defID of the referrer upon first reference
of a conjunct. But if a Vertex is included as
data, this was not the case. This also uncovered
some bugs where a new OpContext was created
during evalution.

To make the code more robust, we now track the
opID of a Conjunct and ignore the defID if it
does not originate from the current conjunct.
This is strictly better than having random
behavior and generally should not matter as
a Verex for which conjunct info is copied
should be a data vertex, in which case the
defIDs are irrelevant.

That said, we should investigate the
Misaligned* counters.

Issue #3981

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: Ic10490f2f793213a1b8c266911a6b214ffe3db9a
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1218697
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>

+190 -684
+4
cmd/cue/cmd/testdata/script/stats.txtar
··· 54 54 "ConjunctInfos": 6, 55 55 "MaxConjunctInfos": 2, 56 56 "MaxReqSets": 0, 57 + "MaxRedirect": 0, 57 58 "GenerationMismatch": 0, 58 59 "MisalignedConjunct": 0, 59 60 "MisalignedConstraint": 0, ··· 78 79 ConjunctInfos: 6 79 80 MaxConjunctInfos: 2 80 81 MaxReqSets: 0 82 + MaxRedirect: 0 81 83 GenerationMismatch: 0 82 84 MisalignedConjunct: 0 83 85 MisalignedConstraint: 0 ··· 101 103 ConjunctInfos: 6 102 104 MaxConjunctInfos: 2 103 105 MaxReqSets: 0 106 + MaxRedirect: 0 104 107 GenerationMismatch: 0 105 108 MisalignedConjunct: 0 106 109 MisalignedConstraint: 0 ··· 123 126 "ConjunctInfos": 6, 124 127 "MaxConjunctInfos": 2, 125 128 "MaxReqSets": 0, 129 + "MaxRedirect": 0, 126 130 "GenerationMismatch": 0, 127 131 "MisalignedConjunct": 0, 128 132 "MisalignedConstraint": 0,
+11 -4
cue/stats/stats.go
··· 73 73 NumCloseIDs int64 // Number of close IDs used 74 74 ConjunctInfos int64 // Number of conjunct infos created 75 75 MaxConjunctInfos int64 // Maximum number of conjunct infos in a node 76 - MaxReqSets int64 // Number of replace IDs processed 76 + MaxReqSets int64 // Maximum number of requirement sets 77 + MaxRedirect int64 // Maximum number of redirects in containsDefID 77 78 78 79 // Exception counters 79 80 // ··· 145 146 if other.MaxReqSets > c.MaxReqSets { 146 147 c.MaxReqSets = other.MaxReqSets 147 148 } 149 + if other.MaxRedirect > c.MaxRedirect { 150 + c.MaxRedirect = other.MaxRedirect 151 + } 148 152 149 153 c.Freed += other.Freed 150 154 c.Retained += other.Retained ··· 161 165 c.MisalignedConjunct -= start.MisalignedConjunct 162 166 c.MisalignedConstraint -= start.MisalignedConstraint 163 167 c.NumCloseIDs -= start.NumCloseIDs 164 - 165 168 c.ConjunctInfos -= start.ConjunctInfos 169 + 170 + // For max values, we don't subtract since they represent peaks 171 + // c.MaxConjunctInfos and c.MaxReqSets and c.MaxRedirect remain as-is 166 172 167 173 c.Freed -= start.Freed 168 174 c.Retained -= start.Retained ··· 199 205 MisalignedConjunct: {{.MisalignedConjunct}}{{end}}{{if .MisalignedConstraint}} 200 206 MisalignedConstraint: {{.MisalignedConstraint}}{{end}}{{end}}{{if .NumCloseIDs}} 201 207 202 - NumCloseIDs: {{.NumCloseIDs}}{{end}}{{if or (ge .MaxReqSets 150) (ge .MaxConjunctInfos 8)}} 208 + NumCloseIDs: {{.NumCloseIDs}}{{end}}{{if or (ge .MaxReqSets 150) (ge .MaxConjunctInfos 8) (ge .MaxRedirect 2)}} 203 209 204 210 ConjunctInfos: {{.ConjunctInfos}} 205 211 MaxConjunctInfos: {{.MaxConjunctInfos}}{{if .MaxReqSets}} 206 - MaxReqSets: {{.MaxReqSets}}{{end}}{{end}}`)) 212 + MaxReqSets: {{.MaxReqSets}}{{end}}{{if .MaxRedirect}} 213 + MaxRedirect: {{.MaxRedirect}}{{end}}{{end}}`)) 207 214 }) 208 215 209 216 func (s Counts) String() string {
+2 -2
cue/testdata/benchmarks/inlinedisjunction.txtar
··· 28 28 29 29 ConjunctInfos: 99 30 30 MaxConjunctInfos: 12 31 - MaxReqSets: 33 31 + MaxReqSets: 12 32 32 -- out/eval -- 33 33 (struct){ 34 34 #def: (#struct){ |((#struct){ ··· 72 72 + 73 73 +ConjunctInfos: 99 74 74 +MaxConjunctInfos: 12 75 - +MaxReqSets: 33 75 + +MaxReqSets: 12 76 76 -- out/eval/stats -- 77 77 Leaks: 0 78 78 Freed: 4674
+11 -1
cue/testdata/benchmarks/issue1684.txtar
··· 73 73 Disjuncts: 6534 74 74 75 75 NumCloseIDs: 4115 76 + 77 + ConjunctInfos: 7501 78 + MaxConjunctInfos: 4 79 + MaxReqSets: 12 80 + MaxRedirect: 5 76 81 -- out/evalalpha -- 77 82 (struct){ 78 83 #Secret: (#struct){ ··· 140 145 diff old new 141 146 --- old 142 147 +++ new 143 - @@ -1,9 +1,11 @@ 148 + @@ -1,9 +1,16 @@ 144 149 -Leaks: 0 145 150 -Freed: 1064333 146 151 -Reused: 1064282 ··· 159 164 +Disjuncts: 6534 160 165 + 161 166 +NumCloseIDs: 4115 167 + + 168 + +ConjunctInfos: 7501 169 + +MaxConjunctInfos: 4 170 + +MaxReqSets: 12 171 + +MaxRedirect: 5 162 172 -- diff/-out/evalalpha<==>+out/eval -- 163 173 diff old new 164 174 --- old
+2 -2
cue/testdata/benchmarks/issue2176.txtar
··· 72 72 73 73 ConjunctInfos: 856 74 74 MaxConjunctInfos: 28 75 - MaxReqSets: 91 75 + MaxReqSets: 26 76 76 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 77 77 diff old new 78 78 --- old ··· 103 103 + 104 104 +ConjunctInfos: 856 105 105 +MaxConjunctInfos: 28 106 - +MaxReqSets: 91 106 + +MaxReqSets: 26 107 107 -- out/eval/stats -- 108 108 Leaks: 90 109 109 Freed: 684
+11 -1
cue/testdata/benchmarks/listdedup.txtar
··· 44 44 Disjuncts: 24 45 45 46 46 NumCloseIDs: 22 47 + 48 + ConjunctInfos: 70 49 + MaxConjunctInfos: 6 50 + MaxReqSets: 9 51 + MaxRedirect: 2 47 52 -- out/evalalpha -- 48 53 (struct){ 49 54 A: (#struct){ |((#struct){ ··· 465 470 diff old new 466 471 --- old 467 472 +++ new 468 - @@ -1,9 +1,11 @@ 473 + @@ -1,9 +1,16 @@ 469 474 Leaks: 0 470 475 -Freed: 24096 471 476 -Reused: 24051 ··· 485 490 +Disjuncts: 24 486 491 + 487 492 +NumCloseIDs: 22 493 + + 494 + +ConjunctInfos: 70 495 + +MaxConjunctInfos: 6 496 + +MaxReqSets: 9 497 + +MaxRedirect: 2 488 498 -- out/eval/stats -- 489 499 Leaks: 0 490 500 Freed: 24096
+2 -2
cue/testdata/benchmarks/typocheck.txtar
··· 35 35 36 36 ConjunctInfos: 905 37 37 MaxConjunctInfos: 103 38 - MaxReqSets: 409 38 + MaxReqSets: 104 39 39 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 40 40 diff old new 41 41 --- old ··· 61 61 + 62 62 +ConjunctInfos: 905 63 63 +MaxConjunctInfos: 103 64 - +MaxReqSets: 409 64 + +MaxReqSets: 104 65 65 -- out/eval/stats -- 66 66 Leaks: 0 67 67 Freed: 700
+11 -1
cue/testdata/builtins/closed.txtar
··· 103 103 Disjuncts: 6 104 104 105 105 NumCloseIDs: 88 106 + 107 + ConjunctInfos: 259 108 + MaxConjunctInfos: 5 109 + MaxReqSets: 6 110 + MaxRedirect: 2 106 111 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 107 112 diff old new 108 113 --- old 109 114 +++ new 110 - @@ -1,9 +1,11 @@ 115 + @@ -1,9 +1,16 @@ 111 116 -Leaks: 61 112 117 -Freed: 214 113 118 -Reused: 208 ··· 128 133 +Disjuncts: 6 129 134 + 130 135 +NumCloseIDs: 88 136 + + 137 + +ConjunctInfos: 259 138 + +MaxConjunctInfos: 5 139 + +MaxReqSets: 6 140 + +MaxRedirect: 2 131 141 -- out/eval/stats -- 132 142 Leaks: 61 133 143 Freed: 214
+11 -1
cue/testdata/comprehensions/issue3929.txtar
··· 98 98 Disjuncts: 53 99 99 100 100 NumCloseIDs: 101 101 + 102 + ConjunctInfos: 157 103 + MaxConjunctInfos: 6 104 + MaxReqSets: 13 105 + MaxRedirect: 3 101 106 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 102 107 diff old new 103 108 --- old 104 109 +++ new 105 - @@ -1,11 +1,11 @@ 110 + @@ -1,11 +1,16 @@ 106 111 -Leaks: 2 107 112 -Freed: 653 108 113 -Reused: 632 ··· 125 130 +Disjuncts: 53 126 131 + 127 132 +NumCloseIDs: 101 133 + + 134 + +ConjunctInfos: 157 135 + +MaxConjunctInfos: 6 136 + +MaxReqSets: 13 137 + +MaxRedirect: 3 128 138 -- out/eval/stats -- 129 139 Leaks: 2 130 140 Freed: 653
+1 -9
cue/testdata/cycle/chain.txtar
··· 1675 1675 Notifications: 15 1676 1676 1677 1677 NumCloseIDs: 1515 1678 - 1679 - ConjunctInfos: 2033 1680 - MaxConjunctInfos: 6 1681 - MaxReqSets: 234 1682 1678 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 1683 1679 diff old new 1684 1680 --- old 1685 1681 +++ new 1686 - @@ -1,11 +1,16 @@ 1682 + @@ -1,11 +1,12 @@ 1687 1683 -Leaks: 56 1688 1684 -Freed: 1788 1689 1685 -Reused: 1774 ··· 1707 1703 +Notifications: 15 1708 1704 + 1709 1705 +NumCloseIDs: 1515 1710 - + 1711 - +ConjunctInfos: 2033 1712 - +MaxConjunctInfos: 6 1713 - +MaxReqSets: 234 1714 1706 -- out/eval/stats -- 1715 1707 Leaks: 56 1716 1708 Freed: 1788
+5 -3
cue/testdata/cycle/comprehension.txtar
··· 420 420 421 421 ConjunctInfos: 1114 422 422 MaxConjunctInfos: 10 423 - MaxReqSets: 157 423 + MaxReqSets: 12 424 + MaxRedirect: 1 424 425 -- out/evalalpha -- 425 426 Errors: 426 427 selfReferential.insertionError.A: adding field foo3 not allowed as field set was already referenced: ··· 1456 1457 diff old new 1457 1458 --- old 1458 1459 +++ new 1459 - @@ -1,11 +1,16 @@ 1460 + @@ -1,11 +1,17 @@ 1460 1461 -Leaks: 74 1461 1462 -Freed: 1384 1462 1463 -Reused: 1358 ··· 1483 1484 + 1484 1485 +ConjunctInfos: 1114 1485 1486 +MaxConjunctInfos: 10 1486 - +MaxReqSets: 157 1487 + +MaxReqSets: 12 1488 + +MaxRedirect: 1 1487 1489 -- out/eval/stats -- 1488 1490 Leaks: 74 1489 1491 Freed: 1384
+5 -3
cue/testdata/cycle/issue990.txtar
··· 94 94 95 95 ConjunctInfos: 996 96 96 MaxConjunctInfos: 5 97 - MaxReqSets: 158 97 + MaxReqSets: 19 98 + MaxRedirect: 4 98 99 -- out/evalalpha -- 99 100 (struct){ 100 101 #AC: (#struct){ ··· 328 329 diff old new 329 330 --- old 330 331 +++ new 331 - @@ -1,11 +1,15 @@ 332 + @@ -1,11 +1,16 @@ 332 333 -Leaks: 6 333 334 -Freed: 3232 334 335 -Reused: 3213 ··· 354 355 + 355 356 +ConjunctInfos: 996 356 357 +MaxConjunctInfos: 5 357 - +MaxReqSets: 158 358 + +MaxReqSets: 19 359 + +MaxRedirect: 4 358 360 -- diff/-out/evalalpha<==>+out/eval -- 359 361 diff old new 360 362 --- old
+11 -1
cue/testdata/eval/disjunctions.txtar
··· 331 331 Notifications: 2 332 332 333 333 NumCloseIDs: 132 334 + 335 + ConjunctInfos: 415 336 + MaxConjunctInfos: 6 337 + MaxReqSets: 7 338 + MaxRedirect: 2 334 339 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 335 340 diff old new 336 341 --- old 337 342 +++ new 338 - @@ -1,11 +1,12 @@ 343 + @@ -1,11 +1,17 @@ 339 344 -Leaks: 40 340 345 -Freed: 837 341 346 -Reused: 825 ··· 359 364 +Notifications: 2 360 365 + 361 366 +NumCloseIDs: 132 367 + + 368 + +ConjunctInfos: 415 369 + +MaxConjunctInfos: 6 370 + +MaxReqSets: 7 371 + +MaxRedirect: 2 362 372 -- out/eval/stats -- 363 373 Leaks: 40 364 374 Freed: 837
+5 -3
cue/testdata/eval/issue3672.txtar
··· 314 314 315 315 ConjunctInfos: 676 316 316 MaxConjunctInfos: 7 317 - MaxReqSets: 211 317 + MaxReqSets: 15 318 + MaxRedirect: 6 318 319 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 319 320 diff old new 320 321 --- old 321 322 +++ new 322 - @@ -1,9 +1,15 @@ 323 + @@ -1,9 +1,16 @@ 323 324 -Leaks: 237 324 325 -Freed: 327955 325 326 -Reused: 327930 ··· 343 344 + 344 345 +ConjunctInfos: 676 345 346 +MaxConjunctInfos: 7 346 - +MaxReqSets: 211 347 + +MaxReqSets: 15 348 + +MaxRedirect: 6 347 349 -- out/eval/stats -- 348 350 Leaks: 237 349 351 Freed: 327955
+11 -1
cue/testdata/eval/issue3805.txtar
··· 180 180 Disjuncts: 16 181 181 182 182 NumCloseIDs: 57 183 + 184 + ConjunctInfos: 243 185 + MaxConjunctInfos: 5 186 + MaxReqSets: 14 187 + MaxRedirect: 3 183 188 -- diff/-out/evalalpha/stats<==>+out/eval/stats -- 184 189 diff old new 185 190 --- old 186 191 +++ new 187 - @@ -1,9 +1,11 @@ 192 + @@ -1,9 +1,16 @@ 188 193 -Leaks: 9 189 194 -Freed: 1508 190 195 -Reused: 1482 ··· 205 210 +Disjuncts: 16 206 211 + 207 212 +NumCloseIDs: 57 213 + + 214 + +ConjunctInfos: 243 215 + +MaxConjunctInfos: 5 216 + +MaxReqSets: 14 217 + +MaxRedirect: 3 208 218 -- out/eval/stats -- 209 219 Leaks: 9 210 220 Freed: 1508
+4 -8
internal/core/adt/context.go
··· 29 29 "cuelang.org/go/cue/token" 30 30 "cuelang.org/go/internal" 31 31 "cuelang.org/go/internal/cuedebug" 32 - "cuelang.org/go/internal/intset" 33 32 ) 34 33 35 34 // Runtime defines an interface for low-level representation conversion and ··· 110 109 111 110 nest int 112 111 113 - nextDefID defID // used in typocheck.go 112 + // used in typocheck.go 113 + nextDefID defID // next available defID 114 + containments []defID // parent relations 115 + redirectsBuf []replaceID // reusable buffer used in containsDefID 114 116 115 117 stats stats.Counts 116 118 freeListNode *nodeContext ··· 207 209 currentDisjunctionID int // sequence number for call to processDisjunctions 208 210 209 211 disjunctStack []disjunctInfo // stack of disjunct IDs 210 - 211 - // These fields are reused by [reqSets.replaceIDs] to reduce allocations. 212 - replaceIDsIndex map[defID]replaceInfo 213 - replaceIDsOrig reqSets 214 - replaceIDsQueue reqSets 215 - replaceIDsVisited *intset.Set[defID] 216 212 217 213 // altPath, if non-empty, provides an alternative path for errors. This is 218 214 // necessary to get the right path for incomplete errors in the presence of
+1
internal/core/adt/eval_test.go
··· 139 139 orig.Conjuncts > counts.Conjuncts*2, 140 140 counts.Notifications > 10, 141 141 counts.NumCloseIDs > 100, 142 + counts.MaxReqSets > 15, 142 143 counts.Leaks()-orig.Leaks() > 17, 143 144 counts.Allocs-orig.Allocs > 50: 144 145 // For now, we only care about disjuncts.
+82 -171
internal/core/adt/typocheck.go
··· 170 170 "slices" 171 171 172 172 "cuelang.org/go/cue/ast" 173 - "cuelang.org/go/internal/intset" 174 173 ) 175 174 176 175 type defID uint32 ··· 204 203 func (c *OpContext) getNextDefID() defID { 205 204 c.stats.NumCloseIDs++ 206 205 c.nextDefID++ 206 + 207 + if len(c.containments) == 0 { 208 + // Our ID starts at 1. Create an extra element for the zero value. 209 + c.containments = make([]defID, 1, 16) 210 + } 211 + c.containments = append(c.containments, 0) 212 + 207 213 return c.nextDefID 208 214 } 209 215 ··· 268 274 if x.from == x.to { 269 275 return 270 276 } 277 + 278 + if x.from < x.to && n.ctx.containments[x.to] == 0 { 279 + n.ctx.containments[x.to] = x.from 280 + return 281 + } 282 + 271 283 // TODO: we currently may compute n.reqSets too early in some rare 272 284 // circumstances. We clear the set if it needs to be recomputed. 273 285 n.computedCloseInfo = false ··· 301 313 if len(n.conjunctInfo) > int(n.ctx.stats.MaxConjunctInfos) { 302 314 n.ctx.stats.MaxConjunctInfos = int64(len(n.conjunctInfo)) 303 315 } 304 - 305 316 } 306 317 307 318 // addResolver adds a resolver to typo checking. Both definitions and ··· 567 578 // This requires a copy to not pollute the base for the next iteration. 568 579 if len(na.replaceIDs) > 0 { 569 580 required = slices.Clone(required) 570 - required.replaceIDs(ctx, na.replaceIDs...) 571 581 } 572 582 573 583 n.filterSets(&required, func(n *nodeContext, a requirement) bool { 574 584 if hasParentEllipsis(n, a, n.conjunctInfo) { 575 - a[0].removed = true 585 + a.removed = true 576 586 } 577 587 return true 578 588 }) ··· 589 599 continue 590 600 } 591 601 592 - if n.hasEvidenceForAll(required, na.conjunctInfo) { 602 + if na.hasEvidenceForAll(required, na.conjunctInfo) { 593 603 continue 594 604 } 595 605 ··· 611 621 // conjuncts for each of the typo-checked structs represented by 612 622 // the reqSets. 613 623 func (n *nodeContext) hasEvidenceForAll(a reqSets, conjuncts []conjunctInfo) bool { 614 - for i := uint32(0); int(i) < len(a); i += a[i].size { 615 - if a[i].size == 0 { 616 - panic("unexpected set length") 617 - } 618 - if a[i].ignored { 624 + for i, rs := range a { 625 + if rs.ignored { 619 626 continue 620 627 } 621 - if a[i].removed { 628 + if rs.removed { 622 629 continue 623 630 } 624 631 625 - if !n.hasEvidenceForOne(a, i, conjuncts) { 632 + if !n.hasEvidenceForOne(a, uint32(i), conjuncts) { 626 633 if n.ctx.LogEval > 0 { 627 634 n.Logf("DENIED BY %d", a[i].id) 628 635 } ··· 635 642 // hasEvidenceForOne reports whether a single typo-checked set has evidence for 636 643 // any of its defIDs. 637 644 func (n *nodeContext) hasEvidenceForOne(all reqSets, i uint32, conjuncts []conjunctInfo) bool { 638 - a := all[i : i+all[i].size] 639 - 645 + a := all[i] 640 646 for _, x := range conjuncts { 641 - if n.containsDefID(requirement(a), x.id) { 647 + if n.containsDefID(&a, x.id) { 642 648 return true 643 649 } 644 650 } 645 651 646 - embedScope := all.lookupSet(a[0].embed) 652 + embedScope, ok := all.lookupSet(a.embed) 647 653 648 - if len(embedScope) == 0 { 654 + if !ok { 649 655 return false 650 656 } 651 657 652 - outerScope := all.lookupSet(a[0].parent) 658 + outerScope, ok := all.lookupSet(a.parent) 653 659 654 - if len(outerScope) > 0 && outerScope[0].removed { 660 + if ok && outerScope.removed { 655 661 return true 656 662 } 657 663 658 664 outer: 659 665 for _, c := range conjuncts { 660 - if n.containsDefID(requirement(embedScope), c.embed) { 666 + if n.containsDefID(&embedScope, c.embed) { 661 667 // Within the scope of the embedding. 662 668 continue outer 663 669 } 664 670 665 - if len(outerScope) == 0 || a[0].parent == 0 { 671 + if !ok || a.parent == 0 { 666 672 return true 667 673 } 668 674 669 675 // If this conjunct is within the outer struct, but outside the 670 676 // embedding scope, this means it was "added" and we do not have 671 677 // to verify it within the embedding scope. 672 - if n.containsDefID(requirement(outerScope), c.id) { 678 + if n.containsDefID(&outerScope, c.id) { 673 679 return true 674 680 } 675 681 } ··· 677 683 } 678 684 679 685 func (n *nodeContext) containsDefID(node requirement, child defID) bool { 680 - for _, ri := range node { 681 - if child == ri.id { 686 + // TODO(perf): cache result 687 + // TODO(perf): we could keep track of the minimum defID that could map so 688 + // that we can use this to bail out early. 689 + c := n.ctx 690 + c.redirectsBuf = c.redirectsBuf[:0] 691 + for n := n; n != nil; n = n.node.Parent.state { 692 + c.redirectsBuf = append(c.redirectsBuf, n.replaceIDs...) 693 + if n.node.Parent == nil { 694 + break 695 + } 696 + } 697 + 698 + if int64(len(c.redirectsBuf)) > c.stats.MaxRedirect { 699 + c.stats.MaxRedirect = int64(len(c.redirectsBuf)) 700 + } 701 + 702 + return n.containsDefIDRec(node.id, child) 703 + } 704 + 705 + func (n *nodeContext) containsDefIDRec(node, child defID) bool { 706 + c := n.ctx 707 + 708 + // NOTE: this loop is O(H) 709 + for p := child; p != 0; p = c.containments[p] { 710 + if p == node { 682 711 return true 683 712 } 713 + 714 + // TODO(perf): can be binary search if we keep redirects sorted. Also, p 715 + // should be monotonically decreasing, so we could use this to direct 716 + // the binary search or-- at the very least--to only have to pass the 717 + // array once. 718 + for _, r := range c.redirectsBuf { 719 + if r.to == p && r.from != child { 720 + if n.containsDefIDRec(node, r.from) { 721 + return true 722 + } 723 + } 724 + } 684 725 } 685 - return false 726 + 727 + return child == node 686 728 } 687 729 688 730 // reqSets defines a set of sets of defIDs that can each satisfy a required id. ··· 693 735 // head. For non-head elements, size is 0. 694 736 type reqSets []reqSet 695 737 696 - type requirement []reqSet 738 + type requirement *reqSet 697 739 698 740 // A single reqID might be satisfied by multiple defIDs, if the definition 699 741 // associated with the reqID embeds other definitions, for instance. In this ··· 706 748 type reqSet struct { 707 749 id defID 708 750 parent defID 709 - // size is the number of elements in the set. This is only set for the head. 710 - // Entries with equivalence IDs have size set to 0. 711 - size uint32 712 - embed defID // TODO(flatclose): can be removed later. 751 + embed defID // TODO(flatclose): can be removed later. 713 752 // once indicates that a reqSet closes only one level, i.e. closedness 714 753 // is the result of a close() 715 754 once bool ··· 727 766 removed bool 728 767 } 729 768 730 - // assert checks the invariants of a reqSets. It can be used for debugging. 731 - func (a reqSets) assert() { 732 - for i := 0; i < len(a); { 733 - e := a[i] 734 - if e.size == 0 { 735 - panic("head element with 0 size") 736 - } 737 - if i+int(e.size) > len(a) { 738 - panic("set extends beyond end of slice") 739 - } 740 - for j := 1; j < int(e.size); j++ { 741 - if a[i+j].size != 0 { 742 - panic("non-head element with non-zero size") 743 - } 744 - } 745 - 746 - i += int(e.size) 747 - } 748 - } 749 - 750 - type replaceInfo struct { 751 - skip, delete bool 752 - replacements []defID 753 - } 754 - 755 - // replaceIDs transitively applies replacement rules to the requirement sets, 756 - // modifying the receiver in place. 757 - // 758 - // The algorithm uses an efficient, non-recursive Breadth-First Search (BFS) 759 - // to expand equivalences for each requirement group. It first pre-processes 760 - // the list of rules into structures for quick lookups: 761 - // 762 - // 1. Head Deletion: A rule with `add: false` marks a group for deletion if 763 - // its `from` ID matches the group's head. 764 - // 2. ID Removal: A rule with `to: deleteID` marks the `from` ID to be 765 - // excluded from any group. 766 - // 3. Graph Traversal: All other rules build a replacement graph. The BFS 767 - // traverses this graph, starting from a group's initial members, to find 768 - // all reachable IDs. Traversal is pruned for branches leading to a removed 769 - // ID or into an embedding's scope. 770 - // 771 - // Finally, a new group is reconstructed from all the valid IDs found. 772 - func (a *reqSets) replaceIDs(ctx *OpContext, b ...replaceID) { 773 - if len(b) == 0 { 774 - return 775 - } 776 - 777 - // TODO(mvdan): can we build this map directly instead of a slice? 778 - index := ctx.replaceIDsIndex 779 - if index == nil { 780 - index = make(map[defID]replaceInfo, len(b)) 781 - } 782 - for _, rule := range b { 783 - info := index[rule.from] 784 - if rule.to == deleteID { 785 - info.delete = true 786 - } else { 787 - // All non-delete rules, regardless of the `add` flag, contribute 788 - // to the replacement graph. 789 - info.replacements = append(info.replacements, rule.to) 790 - } 791 - index[rule.from] = info 792 - } 793 - 794 - origSets := append(ctx.replaceIDsOrig[:0], *a...) 795 - newSets := (*a)[:0] 796 - queue := ctx.replaceIDsQueue 797 - visited := ctx.replaceIDsVisited 798 - if visited == nil { 799 - visited = intset.New[defID](32) 800 - ctx.replaceIDsVisited = visited 801 - } 802 - 803 - for i := 0; i < len(origSets); { 804 - head := origSets[i] 805 - currentGroup := origSets[i : i+int(head.size)] 806 - i += int(head.size) 807 - 808 - if index[head.id].skip { 809 - continue 810 - } 811 - 812 - queue = queue[:0] 813 - visited.Clear() 814 - 815 - for _, set := range currentGroup { 816 - if !index[set.id].delete && !visited.Has(set.id) { 817 - visited.Add(set.id) 818 - queue = append(queue, set) 819 - } 820 - } 821 - 822 - for qIdx := 0; qIdx < len(queue); qIdx++ { 823 - currentSet := queue[qIdx] 824 - 825 - for _, nextID := range index[currentSet.id].replacements { 826 - if !index[nextID].delete && !visited.Has(nextID) { 827 - // Trim subtree for embedded conjunctions. 828 - if head.embed == nextID { 829 - continue 830 - } 831 - visited.Add(nextID) 832 - queue = append(queue, reqSet{id: nextID, once: currentSet.once}) 833 - } 834 - } 835 - } 836 - 837 - if len(queue) > 0 { 838 - queue[0].size = uint32(len(queue)) 839 - newSets = append(newSets, queue...) 840 - } 841 - } 842 - 843 - // to be reused later on 844 - clear(index) 845 - ctx.replaceIDsIndex = index 846 - ctx.replaceIDsOrig = origSets[:0] 847 - ctx.replaceIDsQueue = queue[:0] 848 - visited.Clear() 849 - 850 - *a = newSets 851 - } 852 - 853 769 // mergeCloseInfo merges the conjunctInfo of nw that is missing from nv into nv. 854 770 // 855 771 // This is used to merge conjunctions from a disjunct to the Vertex that ··· 947 863 once: once, 948 864 ignored: y.ignore, 949 865 embed: y.embed, 950 - size: 1, 951 866 }) 952 867 953 868 if y.parent != 0 && !y.ignore { ··· 967 882 } 968 883 } 969 884 } 970 - 971 - a.replaceIDs(n.ctx, n.replaceIDs...) 972 885 973 886 // If 'v' is a hidden field, then all reqSets in 'a' for which there is no 974 887 // corresponding entry in conjunctInfo should be removed from 'a'. ··· 1004 917 n.filterSets(a, func(n *nodeContext, a requirement) bool { 1005 918 var f conjunctFlags 1006 919 hasAny := false 920 + 1007 921 for _, c := range conjuncts { 1008 922 if n.containsDefID(requirement(a), c.id) { 1009 923 hasAny = true 1010 924 flags := c.flags 1011 - if c.id < a[0].id { 925 + if c.id < a.id { 1012 926 flags &^= cHasStruct 1013 927 } 1014 928 f |= flags ··· 1018 932 return false 1019 933 } 1020 934 if !hasAny && hasParentEllipsis(n, a, parentConjuncts) { 1021 - a[0].removed = true 935 + a.removed = true 1022 936 } 1023 937 return true 1024 938 }) ··· 1045 959 1046 960 func (n *nodeContext) filterNonRecursive(a *reqSets) { 1047 961 n.filterSets(a, func(n *nodeContext, e requirement) bool { 1048 - x := e[0] 962 + x := e 1049 963 if x.once { // || x.id == 0 1050 - e[0].ignored = true 964 + e.ignored = true 1051 965 } 1052 966 return true // keep the entry 1053 967 }) ··· 1056 970 // filter keeps all reqSets e in a for which f(e) and removes the rest. 1057 971 func (n *nodeContext) filterSets(a *reqSets, f func(n *nodeContext, e requirement) bool) { 1058 972 temp := (*a)[:0] 1059 - for i := 0; i < len(*a); { 1060 - e := (*a)[i] 1061 - set := (*a)[i : i+int(e.size)] 973 + for i := range *a { 974 + set := (*a)[i] 1062 975 1063 - if f(n, requirement(set)) { 1064 - temp = append(temp, set...) 976 + if f(n, requirement(&set)) { 977 + temp = append(temp, set) 1065 978 } 1066 - 1067 - i += int(e.size) 1068 979 } 1069 980 *a = temp 1070 981 } 1071 982 1072 983 // lookupSet returns the set in a with the given id or nil if no such set. 1073 - func (a reqSets) lookupSet(id defID) reqSets { 984 + func (a reqSets) lookupSet(id defID) (reqSet, bool) { 1074 985 if id != 0 { 1075 - for i := uint32(0); int(i) < len(a); i += a[i].size { 986 + for i := range a { 1076 987 if a[i].id == id { 1077 - return a[i : i+a[i].size] 988 + return a[i], true 1078 989 } 1079 990 } 1080 991 } 1081 - return nil 992 + return reqSet{}, false 1082 993 }
-471
internal/core/adt/typocheck_test.go
··· 19 19 "testing" 20 20 ) 21 21 22 - func TestReplaceIDs(t *testing.T) { 23 - tests := []struct { 24 - name string 25 - reqSets reqSets 26 - replace []replaceID 27 - expected reqSets 28 - }{{ 29 - name: "empty result", 30 - reqSets: reqSets{ 31 - {id: 1, size: 1}, 32 - }, 33 - replace: []replaceID{ 34 - {from: 1, to: deleteID}, 35 - }, 36 - }, { 37 - name: "replace with zero id", 38 - reqSets: reqSets{ 39 - {id: 1, size: 1}, 40 - }, 41 - replace: []replaceID{ 42 - {from: 1, to: deleteID}, 43 - }, 44 - expected: reqSets{}, 45 - }, { 46 - name: "replace equivalent", 47 - reqSets: reqSets{ 48 - {id: 1, size: 2}, 49 - {id: 2}, // e.g. from embedding 50 - }, 51 - replace: []replaceID{ 52 - {from: 2, to: 3}, // replacing an embedding is additive. 53 - }, 54 - expected: reqSets{ 55 - {id: 1, size: 3}, 56 - {id: 2}, 57 - {id: 3}, 58 - }, 59 - }, { 60 - name: "no replacement", 61 - reqSets: reqSets{ 62 - {id: 1, size: 1}, 63 - }, 64 - replace: []replaceID{}, 65 - expected: reqSets{ 66 - {id: 1, size: 1}, 67 - }, 68 - }, { 69 - name: "remove multiple from equivalence set", 70 - reqSets: reqSets{ 71 - {id: 1, size: 4}, 72 - {id: 2}, 73 - {id: 3}, 74 - {id: 4}, 75 - {id: 5, size: 1}, 76 - }, 77 - replace: []replaceID{ 78 - {from: 4, to: deleteID}, 79 - {from: 2, to: deleteID}, 80 - }, 81 - expected: reqSets{ 82 - {id: 1, size: 2}, 83 - {id: 3}, 84 - {id: 5, size: 1}, 85 - }, 86 - }, { 87 - name: "add new id to existing set", 88 - reqSets: reqSets{ 89 - {id: 1, size: 1}, 90 - }, 91 - replace: []replaceID{ 92 - {from: 1, to: 2}, 93 - }, 94 - expected: reqSets{ 95 - {id: 1, size: 2}, 96 - {id: 2}, 97 - }, 98 - }, { 99 - name: "add new id to multiple sets", 100 - reqSets: reqSets{ 101 - {id: 1, size: 1}, 102 - {id: 3, size: 1}, 103 - }, 104 - replace: []replaceID{ 105 - {from: 3, to: 4}, 106 - {from: 1, to: 2}, 107 - }, 108 - expected: reqSets{ 109 - {id: 1, size: 2}, 110 - {id: 2}, 111 - {id: 3, size: 2}, 112 - {id: 4}, 113 - }, 114 - }, { 115 - name: "add new id to empty set", 116 - reqSets: reqSets{}, 117 - replace: []replaceID{ 118 - {from: 0, to: 1}, 119 - }, 120 - expected: reqSets{}, 121 - }, { 122 - name: "add new id to non-existent set", 123 - reqSets: reqSets{ 124 - {id: 1, size: 1}, 125 - }, 126 - replace: []replaceID{ 127 - {from: 2, to: 3}, 128 - }, 129 - expected: reqSets{ 130 - {id: 1, size: 1}, 131 - }, 132 - }, { 133 - name: "add then delete", 134 - reqSets: reqSets{ 135 - {id: 1, size: 1}, // delete this 136 - {id: 3, size: 1}, 137 - }, 138 - replace: []replaceID{ 139 - {from: 1, to: 2}, 140 - {from: 1, to: deleteID}, 141 - }, 142 - expected: reqSets{ 143 - {id: 3, size: 1}, 144 - }, 145 - }, { 146 - name: "delete then add", 147 - reqSets: reqSets{ 148 - {id: 1, size: 1}, // delete this 149 - {id: 3, size: 1}, 150 - }, 151 - replace: []replaceID{ 152 - {from: 1, to: deleteID}, 153 - {from: 1, to: 2}, 154 - }, 155 - expected: reqSets{ 156 - {id: 3, size: 1}, 157 - }, 158 - }, { 159 - name: "fixed point", 160 - reqSets: reqSets{ 161 - {id: 1, size: 1}, 162 - {id: 4, size: 2}, 163 - {id: 1}, 164 - }, 165 - replace: []replaceID{ 166 - {from: 1, to: 2}, 167 - {from: 2, to: 3}, 168 - {from: 3, to: 4}, 169 - }, 170 - expected: reqSets{ 171 - {id: 1, size: 4}, 172 - {id: 2}, 173 - {id: 3}, 174 - {id: 4}, 175 - {id: 4, size: 4}, 176 - {id: 1}, 177 - {id: 2}, 178 - {id: 3}, 179 - }, 180 - }, { 181 - name: "fixed point with jumps", 182 - reqSets: reqSets{ 183 - {id: 4, size: 1}, 184 - {id: 1, size: 1}, 185 - }, 186 - replace: []replaceID{ 187 - {from: 1, to: 3}, 188 - {from: 2, to: 1}, 189 - {from: 3, to: 2}, 190 - }, 191 - expected: reqSets{ 192 - {id: 4, size: 1}, 193 - {id: 1, size: 3}, 194 - {id: 3}, // TODO: maybe order? 195 - {id: 2}, 196 - }, 197 - }, { 198 - name: "fixed idempotent", 199 - reqSets: reqSets{ 200 - {id: 1, size: 3}, 201 - {id: 3}, 202 - {id: 2}, 203 - {id: 4, size: 2}, 204 - {id: 1}, 205 - }, 206 - replace: []replaceID{ 207 - {from: 1, to: 3}, 208 - {from: 2, to: 1}, 209 - {from: 3, to: 2}, 210 - }, 211 - expected: reqSets{ 212 - {id: 1, size: 3}, 213 - {id: 3}, 214 - {id: 2}, 215 - {id: 4, size: 4}, 216 - {id: 1}, 217 - {id: 3}, 218 - {id: 2}, 219 - }, 220 - }, { 221 - name: "cycle", 222 - reqSets: []reqSet{ 223 - {id: 1, size: 1}, 224 - {id: 2, size: 1}, 225 - {id: 3, size: 1, embed: 2}, 226 - {id: 4, size: 1, embed: 2}, 227 - }, 228 - replace: []replaceID{ 229 - {from: 1, to: 2}, // , headOnly: true}, 230 - {from: 2, to: 3}, 231 - {from: 2, to: 4}, 232 - {from: 3, to: 1}, 233 - {from: 4, to: 1}, 234 - }, 235 - expected: reqSets{ 236 - {id: 1, size: 4}, 237 - {id: 2}, 238 - {id: 3}, 239 - {id: 4}, 240 - {id: 2, size: 4}, 241 - {id: 3}, 242 - {id: 4}, 243 - {id: 1}, 244 - {id: 3, size: 2, embed: 2}, 245 - {id: 1}, 246 - {id: 4, size: 2, embed: 2}, 247 - {id: 1}, 248 - }, 249 - }, { 250 - name: "exclude 1", 251 - reqSets: []reqSet{ 252 - {id: 3, size: 1, embed: 2}, 253 - }, 254 - replace: []replaceID{ 255 - {from: 1, to: 2}, 256 - {from: 2, to: 3}, 257 - {from: 3, to: 1}, 258 - }, 259 - expected: reqSets{ 260 - {id: 3, size: 2, embed: 2}, 261 - {id: 1}, 262 - }, 263 - }, { 264 - name: "exclude 2", 265 - reqSets: []reqSet{ 266 - {id: 5, size: 1}, 267 - {id: 6, size: 1}, 268 - {id: 7, size: 1, embed: 6}, 269 - {id: 8, size: 1, embed: 6}, 270 - }, 271 - replace: []replaceID{ 272 - {from: 5, to: 6}, 273 - {from: 6, to: 7}, 274 - {from: 6, to: 8}, 275 - {from: 7, to: 0}, 276 - {from: 8, to: 0}, 277 - }, 278 - expected: reqSets{ 279 - {id: 5, size: 4}, 280 - {id: 6}, 281 - {id: 7}, 282 - {id: 8}, 283 - {id: 6, size: 3}, 284 - {id: 7}, 285 - {id: 8}, 286 - {id: 7, size: 2, embed: 6}, 287 - {id: 0}, 288 - {id: 8, size: 2, embed: 6}, 289 - {id: 0}, 290 - }, 291 - }, { 292 - name: "exclude 3", 293 - reqSets: []reqSet{ 294 - {id: 5, size: 1}, 295 - {id: 8, size: 1, embed: 7}, 296 - {id: 9, size: 1, embed: 7}, 297 - }, 298 - replace: []replaceID{ 299 - {from: 5, to: 6}, 300 - {from: 6, to: 7}, 301 - {from: 7, to: 8}, 302 - {from: 7, to: 9}, 303 - {from: 8, to: 6}, 304 - {from: 9, to: 6}, 305 - }, 306 - expected: reqSets{ 307 - {id: 5, size: 5}, 308 - {id: 6}, 309 - {id: 7}, 310 - {id: 8}, 311 - {id: 9}, 312 - {id: 8, size: 2, embed: 7}, 313 - {id: 6}, 314 - {id: 9, size: 2, embed: 7}, 315 - {id: 6}, 316 - }, 317 - }, { 318 - name: "exclude 4", 319 - // represents 320 - // #a: [>="k"]: int // 11 321 - // #b: [<="m"]: int // 12 322 - // #c: [>="w"]: int // 13 323 - // #d: [<="y"]: int // 14 324 - // X: { // 8 325 - // #a & #b // 9 326 - // #c & #d // 10 327 - // } 328 - // ignored groups (9, 10), are omitted. 329 - reqSets: []reqSet{ 330 - {id: 8, size: 1}, 331 - {id: 11, size: 1, embed: 9}, 332 - {id: 12, size: 1, embed: 9}, 333 - {id: 13, size: 1, embed: 10}, 334 - {id: 14, size: 1, embed: 10}, 335 - }, 336 - replace: []replaceID{ 337 - {from: 8, to: 9}, 338 - {from: 8, to: 10}, 339 - {from: 9, to: 11}, 340 - {from: 11, to: 8}, 341 - {from: 9, to: 12}, 342 - {from: 12, to: 8}, 343 - {from: 10, to: 13}, 344 - {from: 13, to: 8}, 345 - {from: 10, to: 14}, 346 - {from: 14, to: 8}, 347 - }, 348 - expected: reqSets{ 349 - {id: 8, size: 7}, 350 - {id: 9}, 351 - {id: 10}, 352 - {id: 11}, 353 - {id: 12}, 354 - {id: 13}, 355 - {id: 14}, 356 - {id: 11, size: 5, embed: 9}, 357 - {id: 8}, 358 - {id: 10}, 359 - {id: 13}, 360 - {id: 14}, 361 - {id: 12, size: 5, embed: 9}, 362 - {id: 8}, 363 - {id: 10}, 364 - {id: 13}, 365 - {id: 14}, 366 - {id: 13, size: 5, embed: 10}, 367 - {id: 8}, 368 - {id: 9}, 369 - {id: 11}, 370 - {id: 12}, 371 - {id: 14, size: 5, embed: 10}, 372 - {id: 8}, 373 - {id: 9}, 374 - {id: 11}, 375 - {id: 12}, 376 - }, 377 - }} 378 - 379 - for _, tt := range tests { 380 - t.Run(tt.name, func(t *testing.T) { 381 - if tt.name != "exclude1" { 382 - // return 383 - } 384 - tt.reqSets.assert() 385 - tt.expected.assert() 386 - 387 - tt.reqSets.replaceIDs(&OpContext{}, tt.replace...) 388 - if !slices.Equal(tt.reqSets, tt.expected) { 389 - t.Errorf("got: \n%v, want:\n%v", tt.reqSets, tt.expected) 390 - } 391 - }) 392 - } 393 - } 394 - 395 - func TestHasEvidence(t *testing.T) { 396 - tests := []struct { 397 - name string 398 - reqSets reqSets 399 - conjuncts []conjunctInfo 400 - want bool 401 - }{{ 402 - name: "single match", 403 - reqSets: reqSets{ 404 - {id: 1, size: 1}, 405 - }, 406 - conjuncts: []conjunctInfo{ 407 - {id: 1}, 408 - }, 409 - want: true, 410 - }, { 411 - name: "no match", 412 - reqSets: reqSets{ 413 - {id: 1, size: 1}, 414 - }, 415 - conjuncts: []conjunctInfo{ 416 - {id: 2}, 417 - }, 418 - want: false, 419 - }, { 420 - name: "no conjuncts", 421 - reqSets: reqSets{ 422 - {id: 1, size: 1}, 423 - }, 424 - conjuncts: []conjunctInfo{}, 425 - want: false, 426 - }, { 427 - name: "no requirements", 428 - reqSets: reqSets{}, 429 - conjuncts: []conjunctInfo{ 430 - {id: 2}, 431 - }, 432 - want: true, 433 - }, { 434 - name: "no requirements, no conjuncts", 435 - reqSets: reqSets{}, 436 - conjuncts: []conjunctInfo{}, 437 - want: true, 438 - }, { 439 - name: "multiple, all match", 440 - reqSets: reqSets{ 441 - {id: 1, size: 1}, 442 - {id: 2, size: 1}, 443 - }, 444 - conjuncts: []conjunctInfo{ 445 - {id: 1}, 446 - {id: 2}, 447 - }, 448 - want: true, 449 - }, { 450 - name: "multiple, one does not match", 451 - reqSets: reqSets{ 452 - {id: 1, size: 1}, 453 - {id: 2, size: 1}, 454 - }, 455 - conjuncts: []conjunctInfo{ 456 - {id: 2}, 457 - }, 458 - want: false, 459 - }, { 460 - name: "multiset match", 461 - reqSets: reqSets{ 462 - {id: 1, size: 2}, 463 - {id: 2}, 464 - }, 465 - conjuncts: []conjunctInfo{ 466 - {id: 2}, 467 - }, 468 - want: true, 469 - }, { 470 - name: "multiset no match", 471 - reqSets: reqSets{ 472 - {id: 1, size: 2}, 473 - {id: 2}, 474 - }, 475 - conjuncts: []conjunctInfo{ 476 - {id: 3}, 477 - }, 478 - want: false, 479 - }} 480 - 481 - n := &nodeContext{} 482 - n.ctx = &OpContext{} 483 - for _, tt := range tests { 484 - t.Run(tt.name, func(t *testing.T) { 485 - tt.reqSets.assert() 486 - 487 - if got := n.hasEvidenceForAll(tt.reqSets, tt.conjuncts); got != tt.want { 488 - t.Errorf("got %v, want %v", got, tt.want) 489 - } 490 - }) 491 - } 492 - } 493 22 func TestMergeCloseInfo(t *testing.T) { 494 23 tests := []struct { 495 24 name string