aontu 0.40.0 → 0.41.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 (57) hide show
  1. package/dist/aontu.js +7 -0
  2. package/dist/aontu.js.map +1 -1
  3. package/dist/ctx.d.ts +9 -0
  4. package/dist/ctx.js +54 -8
  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/tsconfig.tsbuildinfo +1 -1
  9. package/dist/unify.js +85 -47
  10. package/dist/unify.js.map +1 -1
  11. package/dist/val/ConjunctVal.js +2 -2
  12. package/dist/val/ConjunctVal.js.map +1 -1
  13. package/dist/val/DisjunctVal.js +36 -10
  14. package/dist/val/DisjunctVal.js.map +1 -1
  15. package/dist/val/ExpectVal.js +2 -2
  16. package/dist/val/ExpectVal.js.map +1 -1
  17. package/dist/val/FuncBaseVal.js +2 -2
  18. package/dist/val/FuncBaseVal.js.map +1 -1
  19. package/dist/val/IntegerVal.js +1 -1
  20. package/dist/val/IntegerVal.js.map +1 -1
  21. package/dist/val/ListVal.js +7 -3
  22. package/dist/val/ListVal.js.map +1 -1
  23. package/dist/val/MapVal.js +20 -12
  24. package/dist/val/MapVal.js.map +1 -1
  25. package/dist/val/NilVal.d.ts +2 -1
  26. package/dist/val/NilVal.js +15 -1
  27. package/dist/val/NilVal.js.map +1 -1
  28. package/dist/val/OpBaseVal.js +1 -1
  29. package/dist/val/OpBaseVal.js.map +1 -1
  30. package/dist/val/PrefVal.js +3 -3
  31. package/dist/val/PrefVal.js.map +1 -1
  32. package/dist/val/RefVal.js +1 -1
  33. package/dist/val/RefVal.js.map +1 -1
  34. package/dist/val/Val.d.ts +2 -0
  35. package/dist/val/Val.js +88 -43
  36. package/dist/val/Val.js.map +1 -1
  37. package/dist/val/VarVal.js +1 -1
  38. package/dist/val/VarVal.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/aontu.ts +9 -1
  41. package/src/ctx.ts +79 -8
  42. package/src/err.ts +37 -17
  43. package/src/tsconfig.json +2 -1
  44. package/src/unify.ts +86 -57
  45. package/src/val/ConjunctVal.ts +2 -2
  46. package/src/val/DisjunctVal.ts +37 -11
  47. package/src/val/ExpectVal.ts +2 -2
  48. package/src/val/FuncBaseVal.ts +2 -2
  49. package/src/val/IntegerVal.ts +1 -1
  50. package/src/val/ListVal.ts +7 -3
  51. package/src/val/MapVal.ts +20 -12
  52. package/src/val/NilVal.ts +17 -1
  53. package/src/val/OpBaseVal.ts +1 -1
  54. package/src/val/PrefVal.ts +3 -3
  55. package/src/val/RefVal.ts +1 -1
  56. package/src/val/Val.ts +139 -48
  57. package/src/val/VarVal.ts +1 -1
package/src/unify.ts CHANGED
@@ -32,6 +32,34 @@ const MAXCYCLE = 999
32
32
  // Vals should only have to unify downwards (in .unify) over Vals they understand.
33
33
  // and for complex Vals, TOP, which means self unify if not yet done
34
34
  const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
35
+ // Fast paths that don't recurse and so don't need cycle-detection:
36
+ // short-circuit before the saw-key build and seen-map lookup (which
37
+ // together cost ~2.5µs per call). Only return early when the result
38
+ // is already `done` — a non-done result would need the trailing
39
+ // top() unify below.
40
+ //
41
+ // A6a: same ref, already done
42
+ // A6b: different ref but same id + both done
43
+ // P1: exact-equal scalars that are already done (14% of calls
44
+ // in foo-sdk, ~100% with a.done=true)
45
+ if (a !== undefined && a !== null) {
46
+ if (a === b) {
47
+ if (a.done) return a
48
+ }
49
+ else if (b !== undefined && b !== null) {
50
+ if (a.done && b.done) {
51
+ if (a.id === b.id) return a
52
+ 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
+ return a
58
+ }
59
+ }
60
+ }
61
+ }
62
+
35
63
  const te = ctx.explain && explainOpen(ctx, ctx.explain, 'unite', a, b)
36
64
 
37
65
  let out = a
@@ -56,63 +84,67 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
56
84
  ctx.seen[saw] = sawCount + 1
57
85
 
58
86
  try {
59
-
60
87
  let unified = false
61
88
 
62
- if (b && (!a || a.isTop)) {
89
+ // Dispatch ladder. Structure note:
90
+ // - `a == null` is degenerate (shouldn't happen in practice:
91
+ // the top-level call seeds with a real Val). Kept for safety.
92
+ // - TOP is the unit element: unifying with it returns the
93
+ // other side. Handle both sides.
94
+ // - Otherwise route by Val type. Complex Vals (Conjunct,
95
+ // Disjunct, Ref, Pref, Func, Expect) have their own unify
96
+ // that knows how to absorb the peer; prefer `a.unify` when
97
+ // `a` is complex, else `b.unify` when `b` is complex. If
98
+ // neither is complex and it's not a plain-scalar match, fall
99
+ // through to the generic `a.unify` (concrete Val classes
100
+ // each handle their own peer case).
101
+ if (a == null) {
63
102
  out = b
64
103
  why = 'b'
65
104
  }
66
-
67
- else if (a && (!b || b.isTop)) {
105
+ else if (b == null || b.isTop) {
68
106
  out = a
69
107
  why = 'a'
70
108
  }
71
-
72
- else if (a && b && !b.isTop) {
73
- if (a.isNil) {
74
- out = update(a, b)
75
- why = 'an'
76
- }
77
- else if (b.isNil) {
78
- out = update(b, a)
79
- why = 'bn'
80
- }
81
- else if (a.isConjunct) {
82
- out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'CJ') }) : ctx)
83
- unified = true
84
- why = 'acj'
85
- }
86
- else if (a.isExpect) {
87
- out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'AE') }) : ctx)
88
- unified = true
89
- why = 'ae'
90
- }
91
- else if (
92
- b.isConjunct
93
- || b.isDisjunct
94
- || b.isRef
95
- || b.isPref
96
- || b.isFunc
97
- || b.isExpect
98
- ) {
99
-
100
- out = b.unify(a, te ? ctx.clone({ explain: ec(te, 'BW') }) : ctx)
101
- unified = true
102
- why = 'bv'
103
- }
104
-
105
- // Exactly equal scalars.
106
- else if (a.constructor === b.constructor && a.peg === b.peg) {
107
- out = update(a, b)
108
- why = 'up'
109
- }
110
-
111
- else {
112
- out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'GN') }) : ctx)
113
- unified = true
114
- why = 'ab'
115
- }
109
+ else if (a.isTop) {
110
+ out = b
111
+ why = 'b'
112
+ }
113
+ else if (a.isNil) {
114
+ out = update(a, b)
115
+ why = 'an'
116
+ }
117
+ else if (b.isNil) {
118
+ out = update(b, a)
119
+ why = 'bn'
120
+ }
121
+ else if (a.isConjunct || a.isExpect) {
122
+ out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'AC') }) : ctx)
123
+ unified = true
124
+ why = 'a*'
125
+ }
126
+ else if (
127
+ b.isConjunct
128
+ || b.isDisjunct
129
+ || b.isRef
130
+ || b.isPref
131
+ || b.isFunc
132
+ || b.isExpect
133
+ ) {
134
+ out = b.unify(a, te ? ctx.clone({ explain: ec(te, 'BW') }) : ctx)
135
+ unified = true
136
+ why = 'bv'
137
+ }
138
+ // Exactly equal scalars (not caught by early fast-path e.g.
139
+ // because a or b isn't .done yet).
140
+ else if (a.constructor === b.constructor && a.peg === b.peg) {
141
+ out = update(a, b)
142
+ why = 'up'
143
+ }
144
+ else {
145
+ out = a.unify(b, te ? ctx.clone({ explain: ec(te, 'GN') }) : ctx)
146
+ unified = true
147
+ why = 'ab'
116
148
  }
117
149
 
118
150
  if (!out || !out.unify) {
@@ -120,19 +152,16 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
120
152
  why += 'N'
121
153
  }
122
154
 
123
- // console.log('UNITE-DONE', out.id, out.canon, out.done)
124
-
125
- // if (DONE !== out.dc && !unified) {
155
+ // Any non-done top-level result self-unifies with TOP to ensure
156
+ // its children finish converging. Skipped when `unified` is true
157
+ // because the branch that set `out = X.unify(Y, ctx)` already
158
+ // ran that Val's own unify logic.
126
159
  if (!out.done && !unified) {
127
- let nout = out.unify(top(), te ? ctx.clone({ explain: ec(te, 'ND') }) : ctx)
128
- out = nout
160
+ out = out.unify(top(), te ? ctx.clone({ explain: ec(te, 'ND') }) : ctx)
129
161
  why += 'T'
130
162
  }
131
-
132
- // console.log('UNITE', why, a?.id, a?.canon, a?.done, b?.id, b?.canon, b?.done, '->', out?.id, out?.canon, out?.done)
133
163
  }
134
164
  catch (err: any) {
135
- // console.log(err)
136
165
  // TODO: handle unexpected
137
166
  out = makeNilErr(ctx, 'internal', a, b)
138
167
  }
@@ -97,7 +97,7 @@ class ConjunctVal extends JunctionVal {
97
97
  // console.log('CONJUNCT-TERM', this.id, vI, this.peg[vI].canon)
98
98
 
99
99
  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')
100
+ unite(te ? ctx.clone({ explain: ec(te, 'OWN') }) : ctx, this.peg[vI], peer, 'cj-own')
101
101
 
102
102
  upeer[vI].mark.type = newtype = newtype || upeer[vI].mark.type
103
103
  upeer[vI].mark.hide = newhide = newhide || upeer[vI].mark.hide
@@ -153,7 +153,7 @@ class ConjunctVal extends JunctionVal {
153
153
 
154
154
 
155
155
  else {
156
- val = unite(ctx.clone({ explain: ec(te, 'DEF') }), t0, t1, 'cj-peer-t0t1')
156
+ val = unite(te ? ctx.clone({ explain: ec(te, 'DEF') }) : ctx, t0, t1, 'cj-peer-t0t1')
157
157
  // console.log('CONJUNCT-T', t0.canon, t1?.canon, '->', val.canon)
158
158
  done = done && DONE === val.dc
159
159
  newtype = this.mark.type || val.mark.type
@@ -30,7 +30,7 @@ import {
30
30
  top
31
31
  } from './top'
32
32
 
33
- import { NilVal } from '../val/NilVal'
33
+ import { NilVal, TRIAL_NIL } from '../val/NilVal'
34
34
  import { PrefVal } from '../val/PrefVal'
35
35
  import { JunctionVal } from '../val/JunctionVal'
36
36
 
@@ -79,21 +79,46 @@ class DisjunctVal extends JunctionVal {
79
79
 
80
80
  let oval: Val[] = []
81
81
 
82
- // Conjunction (&) distributes over disjunction (|)
82
+ // Conjunction (&) distributes over disjunction (|).
83
+ //
84
+ // Each member is tried against peer in isolation: if that trial
85
+ // produces any errors, the member fails and is marked with a NilVal.
86
+ // Previously this used `ctx?.clone({err: []})` per member - a
87
+ // per-iteration context clone (two Object.creates) just to hold a
88
+ // throwaway error array. For schemas with many disjunctions
89
+ // (e.g. `*true | boolean`, `method: GET | PUT | ...`) this was the
90
+ // single largest source of clones in the unify hot path.
91
+ //
92
+ // Swap-and-restore avoids the clone: the existing ctx's err array
93
+ // is saved, replaced with a fresh array for each trial, then
94
+ // restored. ctx mutation is scoped to this loop and fully undone
95
+ // before return.
96
+ const savedErr = ctx.err
97
+ const savedTrialMode = ctx._trialMode
98
+ // C1-inner: tell `makeNilErr` to return TRIAL_NIL in this scope
99
+ // instead of allocating per-failure NilVals. Save/restore so
100
+ // nested DisjunctVal trials (and the outer non-trial code) are
101
+ // not affected.
102
+ ctx._trialMode = true
83
103
  for (let vI = 0; vI < this.peg.length; vI++) {
84
104
  const v = this.peg[vI]
85
- const cloneCtx = ctx?.clone({ err: [] })
105
+ const trialErr: any[] = []
106
+ ctx.err = trialErr
86
107
 
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)
108
+ oval[vI] = unite(te ? ctx.clone({ explain: ec(te, 'DIST:' + vI) }) : ctx, v, peer, 'dj-peer')
90
109
 
91
- if (0 < cloneCtx?.err.length) {
92
- oval[vI] = makeNilErr(cloneCtx, '|:empty-dist', this)
110
+ if (0 < trialErr.length) {
111
+ // C1: failed-trial marker is never user-visible — it just
112
+ // signals "this disjunct member doesn't match" and is
113
+ // filtered out before the result is built. Use the shared
114
+ // sentinel instead of allocating a fresh NilVal per trial.
115
+ oval[vI] = TRIAL_NIL
93
116
  }
94
117
 
95
118
  done = done && DONE === oval[vI].dc
96
119
  }
120
+ ctx._trialMode = savedTrialMode
121
+ ctx.err = savedErr
97
122
 
98
123
  // // // console.log('DISJUNCT-unify-B', this.id, oval.map(v => v.canon))
99
124
 
@@ -107,12 +132,13 @@ class DisjunctVal extends JunctionVal {
107
132
 
108
133
  // // // console.log('DISJUNCT-unify-C', this.id, oval.map(v => v.id + '=' + v.canon))
109
134
 
110
- // TODO: not an error Nil!
111
- let remove = new NilVal()
135
+ // Dedup: duplicate Vals in the disjunct are replaced with the
136
+ // trial sentinel, which is filtered out a few lines below.
137
+ // (No need for a fresh NilVal — any isNil value gets filtered.)
112
138
  for (let vI = 0; vI < oval.length; vI++) {
113
139
  for (let kI = vI + 1; kI < oval.length; kI++) {
114
140
  if (oval[kI].same(oval[vI])) {
115
- oval[kI] = remove
141
+ oval[kI] = TRIAL_NIL
116
142
  }
117
143
  }
118
144
  }
@@ -49,10 +49,10 @@ 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
54
  const peeru =
55
- unite(ctx.clone({ explain: ec(te, 'EXPECT') }), this.peer, this.peg, 'expect-self')
55
+ unite(te ? ctx.clone({ explain: ec(te, 'EXPECT') }) : ctx, this.peer, this.peg, 'expect-self')
56
56
 
57
57
  if (peeru.isGenable) {
58
58
  out = peeru
@@ -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 &&
@@ -92,7 +92,7 @@ class ListVal extends BagVal {
92
92
 
93
93
  if (peer instanceof ListVal) {
94
94
  if (!this.closed && peer.closed) {
95
- out = peer.unify(this, ctx.clone({ explain: ec(te, 'PMC') })) as ListVal
95
+ out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'PMC') }) : ctx) as ListVal
96
96
  exit = true
97
97
  }
98
98
  else {
@@ -100,7 +100,7 @@ class ListVal extends BagVal {
100
100
  out.spread.cj = null == out.spread.cj ? peer.spread.cj : (
101
101
  null == peer.spread.cj ? out.spread.cj : (
102
102
  out.spread.cj =
103
- unite(ctx.clone({ explain: ec(te, 'SPR') }),
103
+ unite(te ? ctx.clone({ explain: ec(te, 'SPR') }) : ctx,
104
104
  out.spread.cj, peer.spread.cj, 'list-peer')
105
105
  )
106
106
  )
@@ -127,7 +127,7 @@ class ListVal extends BagVal {
127
127
  key_spread_cj.isNil ? key_spread_cj :
128
128
  key_spread_cj.isTop && child.done ? child :
129
129
  child.isTop && key_spread_cj.done ? key_spread_cj :
130
- unite(keyctx.clone({ explain: ec(te, 'PEG:' + key) }),
130
+ unite(te ? keyctx.clone({ explain: ec(te, 'PEG:' + key) }) : keyctx,
131
131
  child, key_spread_cj, 'list-own')
132
132
 
133
133
  done = (done && DONE === out.peg[key].dc)
@@ -205,6 +205,10 @@ class ListVal extends BagVal {
205
205
  // always done, never path-dependent, and never has marks mutated.
206
206
  // For anything more complex, fall back to full deep clone.
207
207
  spreadClone(ctx: AontuContext): Val {
208
+ // B1: share directly when the spread tree has no path-dependent
209
+ // leaves. See MapVal.spreadClone for rationale.
210
+ if (!this.isPathDependent) return this
211
+
208
212
  let allScalarKind = true
209
213
  for (let key in this.peg) {
210
214
  if (!(this.peg[key] as any)?.isScalarKind) {
package/src/val/MapVal.ts CHANGED
@@ -90,7 +90,7 @@ class MapVal extends BagVal {
90
90
 
91
91
  if (peer instanceof MapVal) {
92
92
  if (!this.closed && peer.closed) {
93
- out = peer.unify(this, ctx.clone({ explain: ec(te, 'PMC') })) as MapVal
93
+ out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'PMC') }) : ctx) as MapVal
94
94
  exit = true
95
95
  }
96
96
 
@@ -105,7 +105,7 @@ class MapVal extends BagVal {
105
105
  && peerkeys.join('~') < selfkeys.join('~')
106
106
  )
107
107
  ) {
108
- out = peer.unify(this, ctx.clone({ explain: ec(te, 'SPC') })) as MapVal
108
+ out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'SPC') }) : ctx) as MapVal
109
109
  exit = true
110
110
  }
111
111
 
@@ -115,7 +115,7 @@ class MapVal extends BagVal {
115
115
  out.spread.cj = null == out.spread.cj ? peer.spread.cj : (
116
116
  null == peer.spread.cj ? out.spread.cj : (
117
117
  out.spread.cj =
118
- unite(ctx.clone({ explain: ec(te, 'SPR') }),
118
+ unite(te ? ctx.clone({ explain: ec(te, 'SPR') }) : ctx,
119
119
  out.spread.cj, peer.spread.cj, 'map-self')
120
120
  )
121
121
  )
@@ -148,7 +148,7 @@ class MapVal extends BagVal {
148
148
  key_spread_cj.isNil ? key_spread_cj :
149
149
  key_spread_cj.isTop && child.done ? child :
150
150
  child.isTop && key_spread_cj.done ? key_spread_cj :
151
- unite(keyctx.clone({ explain: ec(te, 'KEY:' + key) }),
151
+ unite(te ? keyctx.clone({ explain: ec(te, 'KEY:' + key) }) : keyctx,
152
152
  child, key_spread_cj, 'map-own')
153
153
 
154
154
  done = (done && DONE === out.peg[key].dc)
@@ -230,15 +230,23 @@ class MapVal extends BagVal {
230
230
  }
231
231
 
232
232
 
233
- // Spread clone: only deep-clone children that are path-dependent
234
- // (isFunc, isRef). Share all other children directly to avoid
235
- // N x M allocations for maps with N keys and M spread fields.
236
- // Spread clone: when all children are ScalarKindVal (simple type
237
- // constraints like `string`, `number`), share them directly to avoid
238
- // N x M allocations. ScalarKindVal is safe to share: it is immutable,
239
- // always done, never path-dependent, and never has marks mutated.
240
- // For anything more complex, fall back to full deep clone.
233
+ // Spread clone: return a Val usable as the per-key spread constraint.
234
+ //
235
+ // Three tiers:
236
+ // 1. tree is path-independent (no RefVal/KeyFuncVal/PathFuncVal/
237
+ // MoveFuncVal/SuperFuncVal anywhere below): return `this` directly.
238
+ // Nothing in the unify path mutates the spread root, and no
239
+ // child depends on its own stored .path, so sharing is safe.
240
+ // 2. top-level children are all ScalarKindVal: shallow clone
241
+ // (share children, fresh MapVal wrapper).
242
+ // 3. otherwise: full deep clone via `this.clone(ctx)`.
243
+ //
244
+ // Tier 1 handles the foo-sdk common case of simple type-constraint
245
+ // spreads like `&:{active: *true | boolean, version: *'0.0.1' | string}`,
246
+ // which are cloned thousands of times per run.
241
247
  spreadClone(ctx: AontuContext): Val {
248
+ if (!this.isPathDependent) return this
249
+
242
250
  let allScalarKind = true
243
251
  for (let key in this.peg) {
244
252
  if (!(this.peg[key] as any)?.isScalarKind) {
package/src/val/NilVal.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  import { Val, EMPTY_ERR } from './Val'
18
18
 
19
- import { AontuError } from '../err'
19
+ import { AontuError, descErr } from '../err'
20
20
 
21
21
 
22
22
  class NilVal extends Val {
@@ -142,6 +142,9 @@ class NilVal extends Val {
142
142
  ctx.adderr(this)
143
143
 
144
144
  if (!ctx.collect) {
145
+ if (null == this.msg || '' === this.msg) {
146
+ descErr(this, ctx)
147
+ }
145
148
  const err = new AontuError(this.msg, [this])
146
149
  throw err
147
150
  }
@@ -162,6 +165,19 @@ class NilVal extends Val {
162
165
  }
163
166
 
164
167
 
168
+ // Shared sentinel for transient "this unification branch failed"
169
+ // markers. Used by DisjunctVal.unify to flag failed member trials
170
+ // (and to dedup results) without allocating a fresh NilVal per
171
+ // failure. The sentinel is filtered out before the disjunct result
172
+ // is constructed, so its .why / .site / .primary fields are never
173
+ // inspected by user-visible code.
174
+ //
175
+ // Do NOT use this sentinel for errors that may surface: those need
176
+ // real NilVals with proper site/path info for descErr formatting.
177
+ const TRIAL_NIL = new NilVal({ why: '|:trial-nil' })
178
+
179
+
165
180
  export {
166
181
  NilVal,
182
+ TRIAL_NIL,
167
183
  }
@@ -117,7 +117,7 @@ class OpBaseVal extends FeatureVal {
117
117
  }
118
118
  else {
119
119
  out = result.done && peer.isTop ? result :
120
- unite(ctx.clone({ explain: ec(te, 'RES') }), result, peer, 'op')
120
+ unite(te ? ctx.clone({ explain: ec(te, 'RES') }) : ctx, result, peer, 'op')
121
121
  }
122
122
 
123
123
  out.dc = DONE === out.dc ? DONE : this.dc + 1
@@ -62,7 +62,7 @@ class PrefVal extends FeatureVal {
62
62
  let why = ''
63
63
 
64
64
  if (!this.peg.done) {
65
- const resolved = unite(ctx.clone({ explain: ctx.explain && ec(te, 'RES') }),
65
+ const resolved = unite(te ? ctx.clone({ explain: ec(te, 'RES') }) : ctx,
66
66
  this.peg, top(), 'pref/resolve')
67
67
  // console.log('PREF-RESOLVED', this.peg.canon, '->', resolved)
68
68
  this.peg = resolved
@@ -96,7 +96,7 @@ class PrefVal extends FeatureVal {
96
96
  // peer.peg.id, peer.peg, peer.peg.done,
97
97
  // )
98
98
 
99
- let peg = unite(ctx.clone({ explain: ctx.explain && ec(te, 'PREF-PEER') }),
99
+ let peg = unite(te ? ctx.clone({ explain: ec(te, 'PREF-PEER') }) : ctx,
100
100
  this.peg, peer.peg, 'pref-peer/' + this.id)
101
101
  out = new PrefVal({ peg }, ctx)
102
102
  // console.log('PREF-RANK-SAME-OUT', peg, peg.done, out, out.done)
@@ -106,7 +106,7 @@ class PrefVal extends FeatureVal {
106
106
  else if (!peer.isTop) {
107
107
  why += 'super-'
108
108
 
109
- out = unite(ctx.clone({ explain: ctx.explain && ec(te, 'SUPER') }),
109
+ out = unite(te ? ctx.clone({ explain: ec(te, 'SUPER') }) : ctx,
110
110
  this.superpeg, peer, 'pref-super/' + this.id)
111
111
  if (out.same(this.superpeg)) {
112
112
  out = this.peg
package/src/val/RefVal.ts CHANGED
@@ -177,7 +177,7 @@ class RefVal extends FeatureVal {
177
177
  }
178
178
  }
179
179
  else {
180
- out = unite(ctx.clone({ explain: ec(te, 'RES') }), resolved, peer, 'ref')
180
+ out = unite(te ? ctx.clone({ explain: ec(te, 'RES') }) : ctx, resolved, peer, 'ref')
181
181
  why = 'u'
182
182
  }
183
183