aontu 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/aontu.js +7 -0
  2. package/dist/aontu.js.map +1 -1
  3. package/dist/ctx.d.ts +14 -2
  4. package/dist/ctx.js +56 -9
  5. package/dist/ctx.js.map +1 -1
  6. package/dist/err.js +36 -15
  7. package/dist/err.js.map +1 -1
  8. package/dist/lang.d.ts +5 -1
  9. package/dist/lang.js +100 -24
  10. package/dist/lang.js.map +1 -1
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/dist/type.d.ts +5 -0
  13. package/dist/type.js.map +1 -1
  14. package/dist/unify.js +372 -55
  15. package/dist/unify.js.map +1 -1
  16. package/dist/utility.js +6 -2
  17. package/dist/utility.js.map +1 -1
  18. package/dist/val/BagVal.d.ts +0 -3
  19. package/dist/val/BagVal.js +6 -6
  20. package/dist/val/BagVal.js.map +1 -1
  21. package/dist/val/ConjunctVal.d.ts +1 -1
  22. package/dist/val/ConjunctVal.js +138 -15
  23. package/dist/val/ConjunctVal.js.map +1 -1
  24. package/dist/val/CopyFuncVal.js +3 -2
  25. package/dist/val/CopyFuncVal.js.map +1 -1
  26. package/dist/val/DisjunctVal.js +40 -10
  27. package/dist/val/DisjunctVal.js.map +1 -1
  28. package/dist/val/ExpectVal.js +17 -4
  29. package/dist/val/ExpectVal.js.map +1 -1
  30. package/dist/val/FuncBaseVal.js +2 -2
  31. package/dist/val/FuncBaseVal.js.map +1 -1
  32. package/dist/val/IntegerVal.js +1 -1
  33. package/dist/val/IntegerVal.js.map +1 -1
  34. package/dist/val/JunctionVal.d.ts +1 -0
  35. package/dist/val/JunctionVal.js +6 -1
  36. package/dist/val/JunctionVal.js.map +1 -1
  37. package/dist/val/KeyFuncVal.js +0 -2
  38. package/dist/val/KeyFuncVal.js.map +1 -1
  39. package/dist/val/ListVal.d.ts +1 -0
  40. package/dist/val/ListVal.js +36 -65
  41. package/dist/val/ListVal.js.map +1 -1
  42. package/dist/val/MapVal.d.ts +3 -2
  43. package/dist/val/MapVal.js +71 -91
  44. package/dist/val/MapVal.js.map +1 -1
  45. package/dist/val/MoveFuncVal.d.ts +2 -1
  46. package/dist/val/MoveFuncVal.js +78 -13
  47. package/dist/val/MoveFuncVal.js.map +1 -1
  48. package/dist/val/NilVal.d.ts +2 -1
  49. package/dist/val/NilVal.js +15 -1
  50. package/dist/val/NilVal.js.map +1 -1
  51. package/dist/val/OpBaseVal.js +1 -1
  52. package/dist/val/OpBaseVal.js.map +1 -1
  53. package/dist/val/PathFuncVal.js +25 -4
  54. package/dist/val/PathFuncVal.js.map +1 -1
  55. package/dist/val/{RefVal.d.ts → PathVal.d.ts} +4 -3
  56. package/dist/val/{RefVal.js → PathVal.js} +75 -77
  57. package/dist/val/PathVal.js.map +1 -0
  58. package/dist/val/PlusOpVal.d.ts +1 -1
  59. package/dist/val/PrefVal.js +20 -7
  60. package/dist/val/PrefVal.js.map +1 -1
  61. package/dist/val/SpreadVal.d.ts +20 -0
  62. package/dist/val/SpreadVal.js +194 -0
  63. package/dist/val/SpreadVal.js.map +1 -0
  64. package/dist/val/Val.d.ts +4 -1
  65. package/dist/val/Val.js +88 -44
  66. package/dist/val/Val.js.map +1 -1
  67. package/dist/val/VarVal.js +3 -3
  68. package/dist/val/VarVal.js.map +1 -1
  69. package/package.json +6 -7
  70. package/src/aontu.ts +9 -1
  71. package/src/ctx.ts +95 -11
  72. package/src/err.ts +37 -17
  73. package/src/lang.ts +113 -23
  74. package/src/tsconfig.json +2 -1
  75. package/src/type.ts +5 -0
  76. package/src/unify.ts +390 -64
  77. package/src/utility.ts +5 -2
  78. package/src/val/BagVal.ts +6 -7
  79. package/src/val/ConjunctVal.ts +133 -15
  80. package/src/val/CopyFuncVal.ts +3 -2
  81. package/src/val/DisjunctVal.ts +43 -11
  82. package/src/val/ExpectVal.ts +19 -5
  83. package/src/val/FuncBaseVal.ts +2 -2
  84. package/src/val/IntegerVal.ts +1 -1
  85. package/src/val/JunctionVal.ts +5 -1
  86. package/src/val/KeyFuncVal.ts +0 -3
  87. package/src/val/ListVal.ts +40 -86
  88. package/src/val/MapVal.ts +78 -119
  89. package/src/val/MoveFuncVal.ts +79 -14
  90. package/src/val/NilVal.ts +17 -1
  91. package/src/val/OpBaseVal.ts +1 -1
  92. package/src/val/PathFuncVal.ts +29 -4
  93. package/src/val/PathVal.ts +435 -0
  94. package/src/val/PrefVal.ts +21 -8
  95. package/src/val/{RefVal.ts → RefVal.ts.old} +31 -20
  96. package/src/val/SpreadVal.ts +275 -0
  97. package/src/val/Val.ts +141 -50
  98. package/src/val/VarVal.ts +3 -3
  99. package/dist/val/RefVal.js.map +0 -1
@@ -36,6 +36,57 @@ import {
36
36
  top
37
37
  } from './top'
38
38
 
39
+ import { MapVal } from './MapVal'
40
+
41
+
42
+ // Shallow merge two MapVals: combine keys without deep unification.
43
+ // For shared keys, merge child terms from both maps into one ConjunctVal
44
+ // so spreads at that level see all children from both terms.
45
+ function shallowMerge(a: MapVal, b: MapVal, ctx: AontuContext): MapVal {
46
+ const out = new MapVal({ peg: {} }, ctx)
47
+ out.site = a.site
48
+ out.closed = a.closed || b.closed
49
+ out.optionalKeys = [...a.optionalKeys]
50
+ for (const k of b.optionalKeys) {
51
+ if (!out.optionalKeys.includes(k)) out.optionalKeys.push(k)
52
+ }
53
+
54
+ let done = true
55
+
56
+ // Copy all keys from a
57
+ for (const k in a.peg) {
58
+ out.peg[k] = a.peg[k]
59
+ }
60
+
61
+ // Merge keys from b
62
+ for (const k in b.peg) {
63
+ if (k in out.peg) {
64
+ const av = out.peg[k]
65
+ const bv = b.peg[k]
66
+
67
+ // Flatten both sides' terms into a single ConjunctVal
68
+ const terms: Val[] = []
69
+ if (av.isConjunct) { terms.push(...av.peg) } else { terms.push(av) }
70
+ if (bv.isConjunct) { terms.push(...bv.peg) } else { terms.push(bv) }
71
+
72
+ out.peg[k] = new ConjunctVal({ peg: terms }, ctx)
73
+ done = false
74
+ }
75
+ else {
76
+ out.peg[k] = b.peg[k]
77
+ }
78
+ }
79
+
80
+ for (const k in out.peg) {
81
+ done = done && (DONE === out.peg[k]?.dc)
82
+ }
83
+
84
+ out.dc = done ? DONE : 1
85
+ propagateMarks(a, out)
86
+ propagateMarks(b, out)
87
+ return out
88
+ }
89
+
39
90
 
40
91
 
41
92
  // TODO: move main logic to op/conjunct
@@ -71,6 +122,11 @@ class ConjunctVal extends JunctionVal {
71
122
  unify(peer: Val, ctx: AontuContext): Val {
72
123
  peer = peer ?? top()
73
124
 
125
+ // Fast path: done conjunct self-unifying with TOP.
126
+ if (this.done && peer.isTop) {
127
+ return this
128
+ }
129
+
74
130
  const te = ctx.explain && explainOpen(ctx, ctx.explain, 'Conjunct', this, peer)
75
131
 
76
132
  let done = true
@@ -96,8 +152,13 @@ class ConjunctVal extends JunctionVal {
96
152
  this.peg[vI].mark.hide = newhide
97
153
  // console.log('CONJUNCT-TERM', this.id, vI, this.peg[vI].canon)
98
154
 
155
+ // Defer unify-with-TOP for map terms with spreads when other
156
+ // map terms exist — the shallow merge fold handles them instead.
157
+ const hasMapPeers = this.peg.length > 1 && this.peg[vI].isMap &&
158
+ this.peg.some((p: Val, i: number) => i !== vI && p.isMap)
99
159
  upeer[vI] = (this.peg[vI].done && peer.isTop) ? this.peg[vI] :
100
- unite(ctx.clone({ explain: ec(te, 'OWN') }), this.peg[vI], peer, 'cj-own')
160
+ (hasMapPeers && hasSpreads(this.peg[vI])) ? this.peg[vI] :
161
+ unite(te ? ctx.clone({ explain: ec(te, 'OWN') }) : ctx, this.peg[vI], peer, 'cj-own')
101
162
 
102
163
  upeer[vI].mark.type = newtype = newtype || upeer[vI].mark.type
103
164
  upeer[vI].mark.hide = newhide = newhide || upeer[vI].mark.hide
@@ -117,7 +178,22 @@ class ConjunctVal extends JunctionVal {
117
178
  }
118
179
 
119
180
  upeer = norm(upeer)
120
- // console.log('CONJUNCT-UPEER', this.id, upeer.map((v: Val) => v.canon))
181
+
182
+ // Stable partition: fold pure terms first, spread-bearing terms
183
+ // last. Pure terms (no spreads, no path-dependent functions)
184
+ // unify cheaply. Dynamic terms are then folded in, applying
185
+ // spread constraints once to the merged result rather than
186
+ // incrementally at every intermediate fold step.
187
+ const pure: Val[] = []
188
+ const dyn: Val[] = []
189
+ for (const term of upeer) {
190
+ if (term.isPathDependent || hasSpreads(term)) {
191
+ dyn.push(term)
192
+ } else {
193
+ pure.push(term)
194
+ }
195
+ }
196
+ upeer = pure.concat(dyn)
121
197
 
122
198
  // Unify terms against each other
123
199
 
@@ -141,25 +217,36 @@ class ConjunctVal extends JunctionVal {
141
217
 
142
218
  // Can't unite with a RefVal, unless also a RefVal with same path.
143
219
  // else if (t0 instanceof RefVal && !(t1 instanceof RefVal)) {
144
- else if (t0.isRef && !(t1.isRef)) {
220
+ else if (t0.isPath && !(t1.isPath)) {
145
221
  outvals.push(t0)
146
222
  t0 = t1
147
223
  }
148
224
 
149
- else if (t1.isRef && !(t0.isRef)) {
225
+ else if (t1.isPath && !(t0.isPath)) {
150
226
  outvals.push(t0)
151
227
  t0 = t1
152
228
  }
153
229
 
154
230
 
231
+ // Shallow merge: when both are maps AND the conjunct contains spreads,
232
+ // combine keys into inner ConjunctVals instead of deep unification.
233
+ // This ensures spreads see ALL children across terms before being applied.
234
+ else if (t0.isMap && t1.isMap && (hasSpreads(t0) || hasSpreads(t1))) {
235
+ const merged = shallowMerge(t0 as any, t1 as any, ctx)
236
+ done = done && DONE === merged.dc
237
+ newtype = this.mark.type || merged.mark.type
238
+ newhide = this.mark.hide || merged.mark.hide
239
+ t0 = merged
240
+ }
241
+
155
242
  else {
156
- val = unite(ctx.clone({ explain: ec(te, 'DEF') }), t0, t1, 'cj-peer-t0t1')
243
+ val = unite(te ? ctx.clone({ explain: ec(te, 'DEF') }) : ctx, t0, t1, 'cj-peer-t0t1')
157
244
  // console.log('CONJUNCT-T', t0.canon, t1?.canon, '->', val.canon)
158
245
  done = done && DONE === val.dc
159
246
  newtype = this.mark.type || val.mark.type
160
247
  newhide = this.mark.hide || val.mark.hide
161
248
 
162
- // Unite was just a conjunt anyway, so discard.
249
+ // Unite was just a conjunct anyway, so discard.
163
250
  if (val.isConjunct) {
164
251
  outvals.push(t0)
165
252
  t0 = t1
@@ -215,6 +302,15 @@ class ConjunctVal extends JunctionVal {
215
302
 
216
303
 
217
304
  gen(ctx?: AontuContext) {
305
+ // If this conjunct contains a SpreadVal + a generable Val,
306
+ // gen the generable Val (spread is an unresolved constraint
307
+ // that's a no-op at gen time — e.g., empty map with spread).
308
+ if (this.peg.length === 2) {
309
+ const [a, b] = this.peg
310
+ if (a.isSpread && b.isGenable) return b.gen(ctx as any)
311
+ if (b.isSpread && a.isGenable) return a.gen(ctx as any)
312
+ }
313
+
218
314
  // Unresolved conjunct cannot be generated, so always an error.
219
315
  let nil = makeNilErr(
220
316
  ctx,
@@ -247,18 +343,17 @@ class ConjunctVal extends JunctionVal {
247
343
  function norm(terms: Val[]): Val[] {
248
344
 
249
345
  let expand: Val[] = []
250
- for (let tI = 0, pI = 0; tI < terms.length; tI++, pI++) {
251
- if (terms[tI].isConjunct) {
252
- const cterms = terms[tI].peg
253
- for (let cI = 0; cI < cterms.length; cI++) {
254
- expand[pI + cI] = cterms[cI]
346
+ function flattenTerms(terms: Val[]) {
347
+ for (let tI = 0; tI < terms.length; tI++) {
348
+ if (terms[tI].isConjunct) {
349
+ flattenTerms(terms[tI].peg)
350
+ }
351
+ else {
352
+ expand.push(terms[tI])
255
353
  }
256
- pI += cterms.length - 1
257
- }
258
- else {
259
- expand[pI] = terms[tI]
260
354
  }
261
355
  }
356
+ flattenTerms(terms)
262
357
 
263
358
 
264
359
  // Consistent ordering ensures order independent unification.
@@ -282,6 +377,29 @@ function norm(terms: Val[]): Val[] {
282
377
  }
283
378
 
284
379
 
380
+ // Check if a Val tree contains any SpreadVal constraints.
381
+ // Cached on _hasSpreads for performance.
382
+ function hasSpreads(v: any): boolean {
383
+ if (v._hasSpreads !== undefined) return v._hasSpreads
384
+ let result = false
385
+ if (v.isSpread) {
386
+ result = true
387
+ }
388
+ else if (v.isMap || v.isList) {
389
+ for (const k in v.peg) {
390
+ if (v.peg[k]?.isVal && hasSpreads(v.peg[k])) { result = true; break }
391
+ }
392
+ }
393
+ else if (v.isJunction) {
394
+ for (const p of v.peg) {
395
+ if (p?.isVal && hasSpreads(p)) { result = true; break }
396
+ }
397
+ }
398
+ v._hasSpreads = result
399
+ return result
400
+ }
401
+
402
+
285
403
  export {
286
404
  norm,
287
405
  ConjunctVal,
@@ -61,9 +61,10 @@ class CopyFuncVal extends FuncBaseVal {
61
61
 
62
62
  // console.log('CR', out)
63
63
 
64
- if (!out.isRef) {
64
+ if (!out.isPath) {
65
+ out.mark.type = false
66
+ out.mark.hide = false
65
67
  walk(out, (_key: string | number | undefined, val: Val) => {
66
- // console.log('WALK', val)
67
68
  val.mark.type = false
68
69
  val.mark.hide = false
69
70
  return val
@@ -30,7 +30,8 @@ import {
30
30
  top
31
31
  } from './top'
32
32
 
33
- import { NilVal } from '../val/NilVal'
33
+
34
+ import { NilVal, TRIAL_NIL } from '../val/NilVal'
34
35
  import { PrefVal } from '../val/PrefVal'
35
36
  import { JunctionVal } from '../val/JunctionVal'
36
37
 
@@ -67,6 +68,11 @@ class DisjunctVal extends JunctionVal {
67
68
  unify(peer: Val, ctx: AontuContext): Val {
68
69
  peer = peer ?? top()
69
70
 
71
+ // Fast path: already-done disjunct unifying with TOP.
72
+ if (this.done && peer.isTop) {
73
+ return this
74
+ }
75
+
70
76
  const te = ctx.explain && explainOpen(ctx, ctx.explain, 'Disjunct', this, peer)
71
77
 
72
78
  if (!this.prefsRanked) {
@@ -79,21 +85,46 @@ class DisjunctVal extends JunctionVal {
79
85
 
80
86
  let oval: Val[] = []
81
87
 
82
- // Conjunction (&) distributes over disjunction (|)
88
+ // Conjunction (&) distributes over disjunction (|).
89
+ //
90
+ // Each member is tried against peer in isolation: if that trial
91
+ // produces any errors, the member fails and is marked with a NilVal.
92
+ // Previously this used `ctx?.clone({err: []})` per member - a
93
+ // per-iteration context clone (two Object.creates) just to hold a
94
+ // throwaway error array. For schemas with many disjunctions
95
+ // (e.g. `*true | boolean`, `method: GET | PUT | ...`) this was the
96
+ // single largest source of clones in the unify hot path.
97
+ //
98
+ // Swap-and-restore avoids the clone: the existing ctx's err array
99
+ // is saved, replaced with a fresh array for each trial, then
100
+ // restored. ctx mutation is scoped to this loop and fully undone
101
+ // before return.
102
+ const savedErr = ctx.err
103
+ const savedTrialMode = ctx._trialMode
104
+ // C1-inner: tell `makeNilErr` to return TRIAL_NIL in this scope
105
+ // instead of allocating per-failure NilVals. Save/restore so
106
+ // nested DisjunctVal trials (and the outer non-trial code) are
107
+ // not affected.
108
+ ctx._trialMode = true
83
109
  for (let vI = 0; vI < this.peg.length; vI++) {
84
110
  const v = this.peg[vI]
85
- const cloneCtx = ctx?.clone({ err: [] })
111
+ const trialErr: any[] = []
112
+ ctx.err = trialErr
86
113
 
87
- // // // console.log('DJ-DIST-A', this.peg[vI].canon, peer.canon)
88
- oval[vI] = unite(cloneCtx.clone({ explain: ec(te, 'DIST:' + vI) }), v, peer, 'dj-peer')
89
- // // // console.log('DJ-DIST-B', oval[vI].canon, cloneCtx?.err)
114
+ oval[vI] = unite(te ? ctx.clone({ explain: ec(te, 'DIST:' + vI) }) : ctx, v, peer, 'dj-peer')
90
115
 
91
- if (0 < cloneCtx?.err.length) {
92
- oval[vI] = makeNilErr(cloneCtx, '|:empty-dist', this)
116
+ if (0 < trialErr.length) {
117
+ // C1: failed-trial marker is never user-visible — it just
118
+ // signals "this disjunct member doesn't match" and is
119
+ // filtered out before the result is built. Use the shared
120
+ // sentinel instead of allocating a fresh NilVal per trial.
121
+ oval[vI] = TRIAL_NIL
93
122
  }
94
123
 
95
124
  done = done && DONE === oval[vI].dc
96
125
  }
126
+ ctx._trialMode = savedTrialMode
127
+ ctx.err = savedErr
97
128
 
98
129
  // // // console.log('DISJUNCT-unify-B', this.id, oval.map(v => v.canon))
99
130
 
@@ -107,12 +138,13 @@ class DisjunctVal extends JunctionVal {
107
138
 
108
139
  // // // console.log('DISJUNCT-unify-C', this.id, oval.map(v => v.id + '=' + v.canon))
109
140
 
110
- // TODO: not an error Nil!
111
- let remove = new NilVal()
141
+ // Dedup: duplicate Vals in the disjunct are replaced with the
142
+ // trial sentinel, which is filtered out a few lines below.
143
+ // (No need for a fresh NilVal — any isNil value gets filtered.)
112
144
  for (let vI = 0; vI < oval.length; vI++) {
113
145
  for (let kI = vI + 1; kI < oval.length; kI++) {
114
146
  if (oval[kI].same(oval[vI])) {
115
- oval[kI] = remove
147
+ oval[kI] = TRIAL_NIL
116
148
  }
117
149
  }
118
150
  }
@@ -49,13 +49,27 @@ class ExpectVal extends FeatureVal {
49
49
 
50
50
  if (!peer.isTop) {
51
51
  this.peer = undefined === this.peer ? peer :
52
- unite(ctx.clone({ explain: ec(te, 'PEER') }), this.peer, peer, 'expect-peer')
52
+ unite(te ? ctx.clone({ explain: ec(te, 'PEER') }) : ctx, this.peer, peer, 'expect-peer')
53
53
 
54
- const peeru =
55
- unite(ctx.clone({ explain: ec(te, 'EXPECT') }), this.peer, this.peg, 'expect-self')
54
+ // Unwrap nested ExpectVals to prevent infinite recursion
55
+ let peg = this.peg
56
+ while (peg?.isExpect) {
57
+ peg = peg.peg
58
+ }
56
59
 
57
- if (peeru.isGenable) {
58
- out = peeru
60
+ // Guard against re-entrant recursion
61
+ if ((this as any)._expectDepth > 0) {
62
+ out.dc = this.dc + 1
63
+ }
64
+ else {
65
+ (this as any)._expectDepth = ((this as any)._expectDepth || 0) + 1
66
+ const peeru =
67
+ unite(te ? ctx.clone({ explain: ec(te, 'EXPECT') }) : ctx, this.peer, peg, 'expect-self')
68
+ ;(this as any)._expectDepth--
69
+
70
+ if (peeru.isGenable) {
71
+ out = peeru
72
+ }
59
73
  }
60
74
  }
61
75
 
@@ -100,7 +100,7 @@ class FuncBaseVal extends FeatureVal {
100
100
 
101
101
  let newarg = arg
102
102
  if (!arg.done) {
103
- newarg = arg.unify(TOP, ctx.clone({ explain: ec(te, 'ARG') }))
103
+ newarg = arg.unify(TOP, te ? ctx.clone({ explain: ec(te, 'ARG') }) : ctx)
104
104
  newtype = newtype || newarg.mark.type
105
105
  newhide = newhide || newarg.mark.hide
106
106
  // console.log('FUNCBASE-UNIFY-PEG-B', arg.canon, arg.done, '->', newarg.canon, newarg.done)
@@ -118,7 +118,7 @@ class FuncBaseVal extends FeatureVal {
118
118
  // console.log('FUNC-RESOLVED', ctx.cc, resolved?.canon)
119
119
 
120
120
  out = resolved.done && peer.isTop ? resolved :
121
- unite(ctx.clone({ explain: ec(te, 'PEG') }),
121
+ unite(te ? ctx.clone({ explain: ec(te, 'PEG') }) : ctx,
122
122
  resolved, peer, 'func-' + this.funcname() + '/' + this.id)
123
123
  propagateMarks(this, out)
124
124
 
@@ -43,7 +43,7 @@ class IntegerVal extends ScalarVal {
43
43
 
44
44
  if (null != peer) {
45
45
  if (peer.isScalarKind) {
46
- out = peer.unify(this, ctx.clone({ explain: ec(te, 'KND') }))
46
+ out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'KND') }) : ctx)
47
47
  }
48
48
  else if (
49
49
  peer.isScalar &&
@@ -17,6 +17,7 @@ import { FeatureVal } from './FeatureVal'
17
17
  // (ConjunctVal and DisjunctVal)
18
18
  abstract class JunctionVal extends FeatureVal {
19
19
  isJunction = true
20
+ _canonCache?: string
20
21
 
21
22
  constructor(
22
23
  spec: ValSpec,
@@ -38,10 +39,13 @@ abstract class JunctionVal extends FeatureVal {
38
39
  }
39
40
 
40
41
  get canon() {
41
- return this.peg.map((v: Val) => {
42
+ if (this._canonCache !== undefined) return this._canonCache
43
+ const c = this.peg.map((v: Val) => {
42
44
  return (v as any).isJunction && Array.isArray(v.peg) && 1 < v.peg.length ?
43
45
  '(' + v.canon + ')' : v.canon // v.id + '=' + v.canon
44
46
  }).join(this.getJunctionSymbol()) // + '<' + (this.mark.hide ? 'H' : '') + '>'
47
+ if (this.done) this._canonCache = c
48
+ return c
45
49
  }
46
50
 
47
51
  // Abstract method to be implemented by subclasses to define their junction symbol
@@ -18,7 +18,6 @@ import { ConjunctVal } from '../val/ConjunctVal'
18
18
 
19
19
  import { FuncBaseVal } from './FuncBaseVal'
20
20
 
21
-
22
21
  class KeyFuncVal extends FuncBaseVal {
23
22
  isKeyFunc = true
24
23
 
@@ -41,14 +40,12 @@ class KeyFuncVal extends FuncBaseVal {
41
40
 
42
41
 
43
42
  unify(peer: Val, ctx: AontuContext): Val {
44
- // TODO: this delay makes keys in spreads and refs work, but it is a hack - find a better way.
45
43
  let out: Val = this
46
44
 
47
45
  if (ctx.cc < 3) {
48
46
  this.notdone()
49
47
 
50
48
  if (peer.isTop || (peer.id === this.id)) {
51
- // TODO: clone needed to avoid triggering unify_cycle - find a better way
52
49
  out = this.clone(ctx)
53
50
  }
54
51
  else if (peer.isNil) {
@@ -9,7 +9,6 @@ import type {
9
9
 
10
10
  import {
11
11
  DONE,
12
- SPREAD,
13
12
  } from '../type'
14
13
 
15
14
  import { AontuContext } from '../ctx'
@@ -30,7 +29,6 @@ import {
30
29
  top
31
30
  } from './top'
32
31
 
33
- import { ConjunctVal } from './ConjunctVal'
34
32
  import { NilVal } from './NilVal'
35
33
  import { BagVal } from './BagVal'
36
34
  import { empty } from './Val'
@@ -38,6 +36,7 @@ import { empty } from './Val'
38
36
 
39
37
  class ListVal extends BagVal {
40
38
  isList = true
39
+ _canonCache?: string
41
40
 
42
41
  constructor(
43
42
  spec: {
@@ -51,24 +50,6 @@ class ListVal extends BagVal {
51
50
  throw new AontuError('ListVal spec.peg undefined')
52
51
  }
53
52
 
54
- let spread = (this.peg as any)[SPREAD]
55
- delete (this.peg as any)[SPREAD]
56
-
57
- if (spread) {
58
- if ('&' === spread.o) {
59
-
60
- // TODO: handle existing spread!
61
- this.spread.cj =
62
- Array.isArray(spread.v) ?
63
- 1 < spread.v.length ?
64
- new ConjunctVal({ peg: spread.v }, ctx) :
65
- spread.v[0] :
66
- spread.v
67
-
68
- // let tmv = Array.isArray(spread.v) ? spread.v : [spread.v]
69
- // this.spread.cj = new ConjunctVal({ peg: tmv }, ctx)
70
- }
71
- }
72
53
  }
73
54
 
74
55
 
@@ -86,24 +67,16 @@ class ListVal extends BagVal {
86
67
  let out: ListVal | NilVal = (peer.isTop ? this : new ListVal({ peg: [] }, ctx))
87
68
 
88
69
  out.closed = this.closed
89
- out.optionalKeys = [...this.optionalKeys]
90
- out.spread.cj = this.spread.cj
70
+ out.optionalKeys = 0 < this.optionalKeys.length ? [...this.optionalKeys] : this.optionalKeys
91
71
  out.site = this.site
92
72
 
93
73
  if (peer instanceof ListVal) {
94
74
  if (!this.closed && peer.closed) {
95
- out = peer.unify(this, ctx.clone({ explain: ec(te, 'PMC') })) as ListVal
75
+ out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'PMC') }) : ctx) as ListVal
96
76
  exit = true
97
77
  }
98
78
  else {
99
79
  out.closed = out.closed || peer.closed
100
- out.spread.cj = null == out.spread.cj ? peer.spread.cj : (
101
- null == peer.spread.cj ? out.spread.cj : (
102
- out.spread.cj =
103
- unite(ctx.clone({ explain: ec(te, 'SPR') }),
104
- out.spread.cj, peer.spread.cj, 'list-peer')
105
- )
106
- )
107
80
  }
108
81
  }
109
82
 
@@ -111,29 +84,39 @@ class ListVal extends BagVal {
111
84
  if (!exit) {
112
85
  out.dc = this.dc + 1
113
86
 
114
- let spread_cj = out.spread.cj || TOP
87
+ // Fast path: self-unify with TOP.
88
+ if (peer.isTop) {
89
+ let allChildrenDone = true
90
+ for (let key in this.peg) {
91
+ if (DONE !== this.peg[key]?.dc) {
92
+ allChildrenDone = false
93
+ break
94
+ }
95
+ }
96
+ if (allChildrenDone) {
97
+ out.dc = DONE
98
+ ctx.explain && explainClose(te, out)
99
+ return out
100
+ }
101
+ }
115
102
 
116
- // Always unify children first
103
+ // Unify own children
117
104
  for (let key in this.peg) {
118
105
  const keyctx = ctx.descend(key)
119
- const key_spread_cj = spread_cj.spreadClone(keyctx)
120
106
  const child = this.peg[key]
121
107
 
122
108
  propagateMarks(this, child)
123
109
 
124
110
  out.peg[key] =
125
- undefined === child ? key_spread_cj :
111
+ undefined === child ? top() :
126
112
  child.isNil ? child :
127
- key_spread_cj.isNil ? key_spread_cj :
128
- key_spread_cj.isTop && child.done ? child :
129
- child.isTop && key_spread_cj.done ? key_spread_cj :
130
- unite(keyctx.clone({ explain: ec(te, 'PEG:' + key) }),
131
- child, key_spread_cj, 'list-own')
113
+ child.done ? child :
114
+ unite(te ? keyctx.clone({ explain: ec(te, 'PEG:' + key) }) : keyctx,
115
+ child, top(), 'list-own')
132
116
 
133
117
  done = (done && DONE === out.peg[key].dc)
134
118
  }
135
119
 
136
- const allowedKeys: string[] = this.closed ? Object.keys(this.peg) : []
137
120
  let bad: NilVal | undefined = undefined
138
121
 
139
122
  if (peer instanceof ListVal) {
@@ -141,11 +124,10 @@ class ListVal extends BagVal {
141
124
  te ? ctx.clone({ explain: ec(te, 'PER') }) : ctx,
142
125
  peer, TOP, 'list-peer-list') as ListVal)
143
126
 
144
- // NOTE: peerkey is the index
145
127
  for (let peerkey in upeer.peg) {
146
128
  let peerchild = upeer.peg[peerkey]
147
129
 
148
- if (this.closed && !allowedKeys.includes(peerkey)) {
130
+ if (this.closed && !(peerkey in this.peg)) {
149
131
  bad = makeNilErr(ctx, 'closed', peerchild, undefined)
150
132
  }
151
133
 
@@ -155,19 +137,11 @@ class ListVal extends BagVal {
155
137
 
156
138
  let oval = out.peg[peerkey] =
157
139
  undefined === child ? peerchild :
158
- child.isTop && peerchild.done ? peerchild :
159
- child.isNil ? child :
160
- peerchild.isNil ? peerchild :
161
- unite(te ? peerctx.clone({ explain: ec(te, 'CHD') }) : peerctx,
162
- child, peerchild, 'list-peer')
163
-
164
- if (this.spread.cj) {
165
- let key_spread_cj = spread_cj.spreadClone(peerctx)
166
-
167
- oval = out.peg[peerkey] =
168
- unite(te ? peerctx.clone({ explain: ec(te, 'PSP:' + peerkey) }) : peerctx,
169
- out.peg[peerkey], key_spread_cj, 'list-spread')
170
- }
140
+ child.isTop && peerchild.done ? peerchild :
141
+ child.isNil ? child :
142
+ peerchild.isNil ? peerchild :
143
+ unite(te ? peerctx.clone({ explain: ec(te, 'CHD') }) : peerctx,
144
+ child, peerchild, 'list-peer')
171
145
 
172
146
  propagateMarks(this, oval)
173
147
 
@@ -197,35 +171,18 @@ class ListVal extends BagVal {
197
171
  }
198
172
 
199
173
 
200
- // Spread clone: only deep-clone children that are path-dependent
201
- // (isFunc, isRef). Share all other children directly.
202
- // Spread clone: when all children are ScalarKindVal (simple type
203
- // constraints like `string`, `number`), share them directly to avoid
204
- // N x M allocations. ScalarKindVal is safe to share: it is immutable,
205
- // always done, never path-dependent, and never has marks mutated.
206
- // For anything more complex, fall back to full deep clone.
174
+ // Spread clone: share path-independent children directly, clone
175
+ // only path-dependent ones. See MapVal.spreadClone for rationale.
207
176
  spreadClone(ctx: AontuContext): Val {
208
- let allScalarKind = true
209
- for (let key in this.peg) {
210
- if (!(this.peg[key] as any)?.isScalarKind) {
211
- allScalarKind = false
212
- break
213
- }
214
- }
215
-
216
- if (!allScalarKind) {
217
- return this.clone(ctx)
218
- }
177
+ if (!this.isPathDependent) return this
219
178
 
220
179
  let out = (super.clone(ctx) as ListVal)
221
180
 
222
181
  for (let entry of Object.entries(this.peg)) {
223
- out.peg[entry[0]] = entry[1]
224
- }
225
-
226
- // Must create a new spread object to avoid mutating the original.
227
- out.spread = {
228
- cj: this.spread.cj ? this.spread.cj.spreadClone(ctx) : undefined,
182
+ const child = entry[1] as Val
183
+ out.peg[entry[0]] = child?.isPathDependent
184
+ ? child.clone(ctx, { mark: {} })
185
+ : child
229
186
  }
230
187
 
231
188
  out.closed = this.closed
@@ -241,10 +198,6 @@ class ListVal extends BagVal {
241
198
  out.peg[entry[0]] =
242
199
  (entry[1] as any)?.isVal ? (entry[1] as Val).clone(ctx, spec?.mark ? { mark: spec.mark } : {}) : entry[1]
243
200
  }
244
- if (this.spread.cj) {
245
- out.spread.cj = this.spread.cj.clone(ctx, spec?.mark ? { mark: spec.mark } : {})
246
- }
247
-
248
201
  out.closed = this.closed
249
202
  out.optionalKeys = [...this.optionalKeys]
250
203
 
@@ -254,18 +207,19 @@ class ListVal extends BagVal {
254
207
 
255
208
 
256
209
  get canon() {
210
+ if (this._canonCache !== undefined) return this._canonCache
257
211
  // console.log('LISTVAL-CANON', this.optionalKeys)
258
212
  let keys = Object.keys(this.peg)
259
- return '' +
213
+ const c = '' +
260
214
  // this.errcanon() +
261
215
  '[' +
262
- (this.spread.cj ? '&:' + this.spread.cj.canon +
263
- (0 < keys.length ? ',' : '') : '') +
264
216
  keys
265
217
  .map(k => this.optionalKeys.includes(k) ?
266
218
  k + '?:' + this.peg[k].canon :
267
219
  this.peg[k].canon).join(',') +
268
220
  ']'
221
+ if (this.done) this._canonCache = c
222
+ return c
269
223
  }
270
224
  }
271
225