aontu 0.41.0 → 0.43.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 (79) hide show
  1. package/dist/ctx.d.ts +5 -2
  2. package/dist/ctx.js +2 -1
  3. package/dist/ctx.js.map +1 -1
  4. package/dist/lang.d.ts +5 -1
  5. package/dist/lang.js +100 -24
  6. package/dist/lang.js.map +1 -1
  7. package/dist/tsconfig.tsbuildinfo +1 -1
  8. package/dist/type.d.ts +5 -0
  9. package/dist/type.js.map +1 -1
  10. package/dist/unify.js +292 -13
  11. package/dist/unify.js.map +1 -1
  12. package/dist/utility.js +6 -2
  13. package/dist/utility.js.map +1 -1
  14. package/dist/val/BagVal.d.ts +0 -3
  15. package/dist/val/BagVal.js +6 -6
  16. package/dist/val/BagVal.js.map +1 -1
  17. package/dist/val/ConjunctVal.d.ts +1 -1
  18. package/dist/val/ConjunctVal.js +137 -14
  19. package/dist/val/ConjunctVal.js.map +1 -1
  20. package/dist/val/CopyFuncVal.js +3 -2
  21. package/dist/val/CopyFuncVal.js.map +1 -1
  22. package/dist/val/DisjunctVal.js +4 -0
  23. package/dist/val/DisjunctVal.js.map +1 -1
  24. package/dist/val/ExpectVal.js +16 -3
  25. package/dist/val/ExpectVal.js.map +1 -1
  26. package/dist/val/JunctionVal.d.ts +1 -0
  27. package/dist/val/JunctionVal.js +6 -1
  28. package/dist/val/JunctionVal.js.map +1 -1
  29. package/dist/val/KeyFuncVal.js +0 -2
  30. package/dist/val/KeyFuncVal.js.map +1 -1
  31. package/dist/val/ListVal.d.ts +1 -0
  32. package/dist/val/ListVal.js +33 -66
  33. package/dist/val/ListVal.js.map +1 -1
  34. package/dist/val/MapVal.d.ts +3 -2
  35. package/dist/val/MapVal.js +67 -95
  36. package/dist/val/MapVal.js.map +1 -1
  37. package/dist/val/MoveFuncVal.d.ts +2 -1
  38. package/dist/val/MoveFuncVal.js +78 -13
  39. package/dist/val/MoveFuncVal.js.map +1 -1
  40. package/dist/val/PathFuncVal.js +25 -4
  41. package/dist/val/PathFuncVal.js.map +1 -1
  42. package/dist/val/{RefVal.d.ts → PathVal.d.ts} +4 -3
  43. package/dist/val/{RefVal.js → PathVal.js} +75 -77
  44. package/dist/val/PathVal.js.map +1 -0
  45. package/dist/val/PlusOpVal.d.ts +1 -1
  46. package/dist/val/PrefVal.js +18 -5
  47. package/dist/val/PrefVal.js.map +1 -1
  48. package/dist/val/SpreadVal.d.ts +20 -0
  49. package/dist/val/SpreadVal.js +194 -0
  50. package/dist/val/SpreadVal.js.map +1 -0
  51. package/dist/val/Val.d.ts +2 -1
  52. package/dist/val/Val.js +7 -8
  53. package/dist/val/Val.js.map +1 -1
  54. package/dist/val/VarVal.js +2 -2
  55. package/dist/val/VarVal.js.map +1 -1
  56. package/package.json +5 -6
  57. package/src/ctx.ts +16 -3
  58. package/src/lang.ts +113 -23
  59. package/src/type.ts +5 -0
  60. package/src/unify.ts +310 -13
  61. package/src/utility.ts +5 -2
  62. package/src/val/BagVal.ts +6 -7
  63. package/src/val/ConjunctVal.ts +131 -13
  64. package/src/val/CopyFuncVal.ts +3 -2
  65. package/src/val/DisjunctVal.ts +6 -0
  66. package/src/val/ExpectVal.ts +18 -4
  67. package/src/val/JunctionVal.ts +5 -1
  68. package/src/val/KeyFuncVal.ts +0 -3
  69. package/src/val/ListVal.ts +38 -88
  70. package/src/val/MapVal.ts +75 -124
  71. package/src/val/MoveFuncVal.ts +79 -14
  72. package/src/val/PathFuncVal.ts +29 -4
  73. package/src/val/PathVal.ts +435 -0
  74. package/src/val/PrefVal.ts +19 -6
  75. package/src/val/{RefVal.ts → RefVal.ts.old} +30 -19
  76. package/src/val/SpreadVal.ts +275 -0
  77. package/src/val/Val.ts +9 -9
  78. package/src/val/VarVal.ts +2 -2
  79. package/dist/val/RefVal.js.map +0 -1
package/src/unify.ts CHANGED
@@ -10,6 +10,9 @@ import { DONE } from './type'
10
10
  import { makeNilErr } from './err'
11
11
 
12
12
  import { NilVal } from './val/NilVal'
13
+ import { StringVal } from './val/StringVal'
14
+ import { PathVal } from './val/PathVal'
15
+ import { KeyFuncVal } from './val/KeyFuncVal'
13
16
 
14
17
  import {
15
18
  Lang
@@ -26,8 +29,9 @@ import {
26
29
  } from './val/top'
27
30
 
28
31
 
32
+
29
33
  // TODO: FIX: false positive when too many top unifications
30
- const MAXCYCLE = 999
34
+ const MAXCYCLE = 9999
31
35
 
32
36
  // Vals should only have to unify downwards (in .unify) over Vals they understand.
33
37
  // and for complex Vals, TOP, which means self unify if not yet done
@@ -50,12 +54,19 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
50
54
  if (a.done && b.done) {
51
55
  if (a.id === b.id) return a
52
56
  if (a.constructor === b.constructor && a.peg === b.peg
53
- && !a.isNil && !b.isNil
54
- && !a.isMap && !a.isList
55
- && !a.isConjunct && !a.isDisjunct
56
- && !a.isRef && !a.isPref && !a.isFunc && !a.isExpect) {
57
+ && !a.isNil && !b.isNil
58
+ && !a.isConjunct && !a.isDisjunct
59
+ && !a.isPath && !a.isPref && !a.isFunc && !a.isExpect) {
57
60
  return a
58
61
  }
62
+
63
+ // Id-keyed cache: reuse results for the exact same Val pair.
64
+ const uc = ctx._uniteCache
65
+ if (uc !== undefined) {
66
+ const ucKey = a.id + '|' + b.id
67
+ const ucHit = uc.get(ucKey)
68
+ if (ucHit !== undefined) return ucHit
69
+ }
59
70
  }
60
71
  }
61
72
  }
@@ -67,21 +78,25 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
67
78
 
68
79
  // Cycle-detection key. Use numeric path index for speed; fall back to
69
80
  // full string key when debug is enabled so the saw value is human-readable.
70
- const saw = ctx.opts.debug
71
- ? (a ? a.id + (a.done ? '' : '*') : '') + '~' +
81
+ let saw: string
82
+ if (ctx.opts.debug) {
83
+ saw = (a ? a.id + (a.done ? '' : '*') : '') + '~' +
72
84
  (b ? b.id + (b.done ? '' : '*') : '') + '@' + ctx.pathstr
73
- : (a ? a.id + (a.done ? 'd' : '') : 0) + '~' +
85
+ }
86
+ else {
87
+ saw = (a ? a.id + (a.done ? 'd' : '') : 0) + '~' +
74
88
  (b ? b.id + (b.done ? 'd' : '') : 0) + '~' + ctx.pathidx
89
+ }
75
90
 
76
91
  // NOTE: if this error occurs "unreasonably", attemp to avoid unnecesary unification
77
92
  // See for example PrefVal peg.id equality inspection.
78
- const sawCount = ctx.seen[saw] ?? 0
93
+ const sawCount = ctx.seen.get(saw) ?? 0
79
94
  if (MAXCYCLE < sawCount) {
80
95
  // console.log('SAW', sawCount, saw, a?.id, a?.canon, b?.id, b?.canon, ctx.cc)
81
96
  out = makeNilErr(ctx, 'unify_cycle', a, b)
82
97
  }
83
98
  else {
84
- ctx.seen[saw] = sawCount + 1
99
+ ctx.seen.set(saw, sawCount + 1)
85
100
 
86
101
  try {
87
102
  let unified = false
@@ -118,7 +133,7 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
118
133
  out = update(b, a)
119
134
  why = 'bn'
120
135
  }
121
- else if (a.isConjunct || a.isExpect) {
136
+ else if (a.isConjunct || a.isExpect || a.isSpread) {
122
137
  out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'AC') }) : ctx)
123
138
  unified = true
124
139
  why = 'a*'
@@ -126,10 +141,11 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
126
141
  else if (
127
142
  b.isConjunct
128
143
  || b.isDisjunct
129
- || b.isRef
144
+ || b.isPath
130
145
  || b.isPref
131
146
  || b.isFunc
132
147
  || b.isExpect
148
+ || b.isSpread
133
149
  ) {
134
150
  out = b.unify(a, te ? ctx.clone({ explain: ec(te, 'BW') }) : ctx)
135
151
  unified = true
@@ -169,6 +185,11 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
169
185
 
170
186
  ctx.explain && explainClose(te, out)
171
187
 
188
+ // Store in id-keyed cache when both operands were done.
189
+ if (a?.done && b?.done && out?.done && ctx._uniteCache !== undefined) {
190
+ ctx._uniteCache.set(a.id + '|' + b.id, out)
191
+ }
192
+
172
193
  return out
173
194
  }
174
195
 
@@ -179,6 +200,269 @@ function update(x: Val, _y: Val) {
179
200
  }
180
201
 
181
202
 
203
+ // Resolve all PathVals using the pre-collected paths list.
204
+ // Mutates the tree in place, replacing each PathVal with its cloned target.
205
+ function resolvePaths(root: Val, ctx: AontuContext, paths: PathVal[]) {
206
+ for (const pv of paths) {
207
+ if (pv.done) continue
208
+
209
+ // Resolve: find target, following chains.
210
+ // Suppress errors during pre-resolution — intermediate PathVals
211
+ // (from multi-dot expressions like ..a.q) may fail here but the
212
+ // final PathVal resolves correctly during unification.
213
+ const savedErr = ctx.err
214
+ ctx.err = []
215
+ let found: any = pv.find(ctx)
216
+ ctx.err = savedErr
217
+ if (found == null || found.isNil) continue
218
+
219
+ // Skip if target or container contains a mark-setting function
220
+ // (type/hide/move) — let unification resolve it first.
221
+ if (hasMarkFunc(found)) continue
222
+ if (hasMarkFuncAtPath(root, pv.path)) continue
223
+
224
+ while (found instanceof PathVal) {
225
+ if (found.done && found._resolved) {
226
+ found = found._resolved
227
+ break
228
+ }
229
+ const next = found.find(ctx)
230
+ if (next == null || next.isNil) break
231
+ found.dc = DONE
232
+ found._resolved = next
233
+ found = next
234
+ }
235
+
236
+ // If found value is a key() function, set its path to the
237
+ // destination so it evaluates at the right position.
238
+ if (found.isKeyFunc) {
239
+ found.path = pv.path
240
+ }
241
+
242
+ pv.dc = DONE
243
+ pv._resolved = found
244
+
245
+ // Replace PathVal in tree using its path
246
+ replaceAtPath(root, pv.path, pv, found)
247
+
248
+ // Walk the placed value to resolve any PathVals cloned into it
249
+ resolveNestedPaths(found, ctx)
250
+ }
251
+ }
252
+
253
+
254
+ // Check if a value contains a type() or hide() function.
255
+ // Check if the value AT the path position is inside a mark-setting function.
256
+ function hasMarkFuncAtPath(root: Val, path: string[]): boolean {
257
+ let node: any = root
258
+ for (let i = 0; i < path.length; i++) {
259
+ const part = path[i]
260
+ if (node.isMap || node.isList) {
261
+ node = node.peg[part]
262
+ }
263
+ else if (node.isConjunct || node.isDisjunct) {
264
+ let found = null
265
+ const stack = [...node.peg]
266
+ while (stack.length > 0) {
267
+ const term = stack.pop()
268
+ if (term?.isConjunct || term?.isDisjunct) {
269
+ stack.push(...term.peg)
270
+ }
271
+ else if ((term?.isMap || term?.isList) && term.peg[part] != null) {
272
+ found = term.peg[part]
273
+ break
274
+ }
275
+ }
276
+ node = found
277
+ }
278
+ else {
279
+ return false
280
+ }
281
+ if (node == null) return false
282
+ }
283
+ return hasMarkFunc(node)
284
+ }
285
+
286
+
287
+ function hasMarkFunc(val: any): boolean {
288
+ if (val == null || !val.isVal) return false
289
+ if (val.isTypeFunc || val.isHideFunc || val.isMoveFunc) return true
290
+ if (val.isConjunct || val.isDisjunct) {
291
+ for (const t of val.peg) {
292
+ if (hasMarkFunc(t)) return true
293
+ }
294
+ }
295
+ return false
296
+ }
297
+
298
+
299
+ // Resolve any PathVals found inside a value (e.g. cloned subtrees).
300
+ // Iterative stack-based walk — no recursion.
301
+ function resolveNestedPaths(val: any, ctx: AontuContext) {
302
+ const stack: any[] = [val]
303
+ while (stack.length > 0) {
304
+ const v = stack.pop()
305
+ if (v == null || !v.isVal) continue
306
+
307
+ if (v.isMap || v.isList) {
308
+ for (const k in v.peg) {
309
+ const child = v.peg[k]
310
+ if (child instanceof PathVal && !child.done) {
311
+ const found = child.find(ctx)
312
+ if (found != null && !found.isNil) {
313
+ v.peg[k] = found
314
+ child.dc = DONE
315
+ child._resolved = found
316
+ stack.push(found)
317
+ }
318
+ }
319
+ else if (child?.isVal) {
320
+ stack.push(child)
321
+ }
322
+ }
323
+ }
324
+ else if (v.isConjunct || v.isDisjunct) {
325
+ for (let i = 0; i < v.peg.length; i++) {
326
+ const child = v.peg[i]
327
+ if (child instanceof PathVal && !child.done) {
328
+ const found = child.find(ctx)
329
+ if (found != null && !found.isNil) {
330
+ v.peg[i] = found
331
+ child.dc = DONE
332
+ child._resolved = found
333
+ stack.push(found)
334
+ }
335
+ }
336
+ else if (child?.isVal) {
337
+ stack.push(child)
338
+ }
339
+ }
340
+ }
341
+ else if (v.isFeature) {
342
+ if (v.peg instanceof PathVal && !v.peg.done) {
343
+ const found = v.peg.find(ctx)
344
+ if (found != null && !found.isNil) {
345
+ v.peg.dc = DONE
346
+ v.peg._resolved = found
347
+ v.peg = found
348
+ stack.push(found)
349
+ }
350
+ }
351
+ else if (Array.isArray(v.peg)) {
352
+ for (let i = 0; i < v.peg.length; i++) {
353
+ if (v.peg[i]?.isVal) stack.push(v.peg[i])
354
+ }
355
+ }
356
+ else if (v.peg?.isVal) {
357
+ stack.push(v.peg)
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+
364
+ // Replace target Val in the tree, using path to navigate and a stack
365
+ // to search through junctions, features, and nested structures.
366
+ // No recursion — uses a single loop with an explicit stack.
367
+ function replaceAtPath(root: Val, path: string[], target: Val, replacement: Val,
368
+ intoSpreads: boolean = true): boolean {
369
+ let node: any = root
370
+
371
+ // Descend through map/list using path segments.
372
+ // When a non-map/list is encountered (junction, feature),
373
+ // push its children onto the search stack and stop descending.
374
+ let pi = 0
375
+ for (; pi < path.length; pi++) {
376
+ if (node.isMap || node.isList) {
377
+ const child = node.peg[path[pi]]
378
+ if (child == null) break
379
+
380
+ // Last segment: check for direct replacement
381
+ if (pi === path.length - 1) {
382
+ if (child === target) {
383
+ node.peg[path[pi]] = replacement
384
+ return true
385
+ }
386
+ // Target not at this position — search within child
387
+ node = child
388
+ pi++
389
+ break
390
+ }
391
+
392
+ node = child
393
+ }
394
+ else {
395
+ // Hit a non-navigable node — search it
396
+ break
397
+ }
398
+ }
399
+
400
+ // If path fully consumed with no match, or hit a junction/feature,
401
+ // search the current node using a stack.
402
+ const stack: any[] = [node]
403
+
404
+ while (stack.length > 0) {
405
+ const val = stack.pop()
406
+ if (val == null || !val.isVal) continue
407
+
408
+ if (val.isMap || val.isList) {
409
+ for (const k in val.peg) {
410
+ if (val.peg[k] === target) {
411
+ val.peg[k] = replacement
412
+ return true
413
+ }
414
+ if (val.peg[k]?.isVal) stack.push(val.peg[k])
415
+ }
416
+ }
417
+ else if (val.isConjunct || val.isDisjunct) {
418
+ for (let i = 0; i < val.peg.length; i++) {
419
+ if (val.peg[i] === target) {
420
+ val.peg[i] = replacement
421
+ return true
422
+ }
423
+ stack.push(val.peg[i])
424
+ }
425
+ }
426
+ else if (val.isFeature) {
427
+ if (val.peg === target) {
428
+ val.peg = replacement
429
+ return true
430
+ }
431
+ if (intoSpreads || !val.isSpread) {
432
+ if (Array.isArray(val.peg)) {
433
+ for (let i = 0; i < val.peg.length; i++) {
434
+ if (val.peg[i] === target) {
435
+ val.peg[i] = replacement
436
+ return true
437
+ }
438
+ if (val.peg[i]?.isVal) stack.push(val.peg[i])
439
+ }
440
+ }
441
+ else if (val.peg?.isVal) {
442
+ stack.push(val.peg)
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ return false
449
+ }
450
+
451
+
452
+ // Resolve all KeyFuncVals (not inside spreads) to StringVals.
453
+ // Uses the KeyFuncVal's path to determine the key name.
454
+ function resolveKeys(root: Val, keys: KeyFuncVal[]) {
455
+ for (const kv of keys) {
456
+ const resolved = kv.resolve(null as any, kv.peg)
457
+ if (resolved instanceof StringVal) {
458
+ resolved.dc = DONE
459
+ resolved.path = kv.path
460
+ replaceAtPath(root, kv.path, kv, resolved, false)
461
+ }
462
+ }
463
+ }
464
+
465
+
182
466
  class Unify {
183
467
  root: Val
184
468
  res: Val
@@ -229,15 +513,28 @@ class Unify {
229
513
  uctx.err = this.err
230
514
  uctx.explain = this.explain
231
515
 
516
+ // Path resolution phase: replace all PathVals with cloned targets.
517
+ // Pure structural replacement — no unification.
518
+ resolvePaths(res, uctx, this.lang.paths)
519
+ uctx = uctx.clone({ root: res })
520
+
521
+ // Key resolution phase: replace key() functions (not in spreads)
522
+ // with their resolved StringVal.
523
+ resolveKeys(res, this.lang.keys)
524
+
232
525
  const explain = null == ctx?.explain ? undefined : ctx?.explain
233
526
  const te = explain && explainOpen(uctx, explain, 'root', res)
234
527
 
235
528
  // NOTE: if true === res.done already, then this loop never needs to run.
529
+ // RefVals defer on cc=0 and while ctx.sc > 0.
530
+ // SpreadVal.unify maintains ctx.sc via increment/decrement.
236
531
  let maxcc = 9 // 99
237
532
  for (; this.cc < maxcc && DONE !== res.dc; this.cc++) {
238
533
  // console.log('CC', this.cc, res.canon)
239
534
  uctx.cc = this.cc
240
- uctx.seen = {}
535
+ uctx.seen = new Map()
536
+ uctx._refCloneCache = new Map()
537
+ uctx._uniteCache = new Map()
241
538
  res = unite(te ? uctx.clone({ explain: ec(te, 'run') }) : uctx, res, top(), 'unify')
242
539
 
243
540
  if (0 < uctx.err.length) {
package/src/utility.ts CHANGED
@@ -11,8 +11,11 @@ function propagateMarks(source: Val, target: Val): void {
11
11
  if (source.isTop || target.isTop) {
12
12
  return
13
13
  }
14
- for (let name in source.mark) {
15
- (target.mark as any)[name] = (target.mark as any)[name] || (source.mark as any)[name]
14
+ const sm = source.mark as any
15
+ if (!sm.type && !sm.hide) return
16
+ const tm = target.mark as any
17
+ for (let name in sm) {
18
+ tm[name] = tm[name] || sm[name]
16
19
  }
17
20
  }
18
21
 
package/src/val/BagVal.ts CHANGED
@@ -29,10 +29,6 @@ abstract class BagVal extends FeatureVal {
29
29
  closed: boolean = false
30
30
  optionalKeys: string[] = []
31
31
 
32
- spread = {
33
- cj: (undefined as Val | undefined),
34
- }
35
-
36
32
  constructor(
37
33
  spec: ValSpec,
38
34
  ctx?: AontuContext
@@ -42,7 +38,6 @@ abstract class BagVal extends FeatureVal {
42
38
 
43
39
  clone(ctx: AontuContext, spec?: ValSpec): Val {
44
40
  const bag = super.clone(ctx, spec) as BagVal
45
- bag.spread = this.spread
46
41
  return bag
47
42
  }
48
43
 
@@ -92,11 +87,15 @@ abstract class BagVal extends FeatureVal {
92
87
  || child.isMap
93
88
  || child.isList
94
89
  || child.isPref
95
- || child.isRef
90
+ || child.isPath
96
91
  || child.isDisjunct
92
+ || child.isConjunct
97
93
  || child.isNil
98
94
  ) {
99
- let cval = child.gen(ctx)
95
+ // Optional keys: use separate error context so gen errors
96
+ // don't propagate when the key is dropped.
97
+ const genctx = optional ? ctx.clone({ err: [], collect: true }) : ctx
98
+ let cval = child.gen(genctx)
100
99
 
101
100
  if (optional && (undefined === cval || empty(cval))) {
102
101
  continue
@@ -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,7 +152,12 @@ 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] :
160
+ (hasMapPeers && hasSpreads(this.peg[vI])) ? this.peg[vI] :
100
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
@@ -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,17 +217,28 @@ 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
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)
@@ -159,7 +246,7 @@ class ConjunctVal extends JunctionVal {
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