aontu 0.58.0 → 0.59.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.
package/src/aontu.ts CHANGED
@@ -38,7 +38,7 @@ export type { LintFinding, FormatReport, FormatOptions } from './format'
38
38
  // Kept in step with package.json by the `version` npm lifecycle script,
39
39
  // which runs on `npm version` / `npm run repo-bump`. version.test.ts
40
40
  // fails if the two ever drift.
41
- const VERSION = '0.58.0'
41
+ const VERSION = '0.59.0'
42
42
 
43
43
 
44
44
  // A module file's VALUE, as far as it goes. COLLECTED, not raised: a
@@ -103,6 +103,7 @@ class Aontu {
103
103
  ctx(cfg?: AontuContextConfig): AontuContext {
104
104
  cfg = cfg ?? {}
105
105
  cfg.fs = cfg.fs ?? this.opts.fs
106
+ ;(cfg as any).errfs = (cfg as any).errfs ?? (this.opts as any).errfs
106
107
  // The trust profile rides the instance (its resolver is built once,
107
108
  // in the Lang constructor); the context needs it too, for the
108
109
  // budgets (G5, docs/trust.md).
package/src/cli.ts CHANGED
@@ -603,7 +603,17 @@ function runFile(file: string, mode: Mode, trust: TrustArg): number {
603
603
  }
604
604
 
605
605
  const path = resolve(file)
606
- const aontu = new Aontu({ path, ...trustOpts(trust, dirname(path)) })
606
+ // `fs` IS WHAT MAKES A FRAME EXCERPT THE FILE IT NAMES. Without it,
607
+ // err.ts's resolveSrc falls back to the ENTRY text, so a frame whose
608
+ // arrow says `lib/types.aon:2:6` printed the entry's line 2 under it
609
+ // -- a real file name over another file's line, which
610
+ // docs/reference-api.md forbids in the same words it uses to require
611
+ // the name.
612
+ const aontu = new Aontu({
613
+ path,
614
+ errfs: { existsSync, readFileSync },
615
+ ...trustOpts(trust, dirname(path)),
616
+ })
607
617
  const res = evalSource(aontu, src, mode)
608
618
  ;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
609
619
  return res.ok ? 0 : 1
package/src/ctx.ts CHANGED
@@ -35,6 +35,7 @@ type AontuContextConfig = {
35
35
  // (Val.origin, Val.emitted), so one flag turns the whole record on.
36
36
  reads?: Set<string>
37
37
  fs?: any
38
+ errfs?: any
38
39
  path?: string[]
39
40
  root?: Val
40
41
  seen?: Record<string, number>
@@ -77,6 +78,8 @@ class AontuContext {
77
78
  vars: Record<string, Val> = {}
78
79
  src?: string
79
80
  fs?: FST
81
+ // The error renderer's own reader; see type.ts's `errfs`.
82
+ errfs?: FST
80
83
 
81
84
  seenI: number
82
85
  seen: Record<string, number>
@@ -199,6 +202,7 @@ class AontuContext {
199
202
  this.explain = Array.isArray(cfg.explain) ? cfg.explain : null
200
203
 
201
204
  this.fs = cfg.fs ?? null
205
+ this.errfs = cfg.errfs ?? null
202
206
 
203
207
  // Multiple unify passes will keep incrementing Val counter.
204
208
  this.vc = null == cfg.vc ? 1_000_000_000 : cfg.vc
@@ -320,6 +324,7 @@ class AontuContext {
320
324
  this.err = this.opts.err ?? this.err
321
325
  this.deps = this.opts.deps ?? this.deps
322
326
  this.fs = this.opts.fs ?? this.fs
327
+ this.errfs = (this.opts as any).errfs ?? this.errfs
323
328
  this.explain = this.opts.explain ?? this.explain
324
329
 
325
330
  this.src = ('string' === typeof this.opts.src ? this.opts.src : undefined) ?? this.src
package/src/err.ts CHANGED
@@ -208,9 +208,12 @@ function resolveSrc(v: Val, errctx: ErrContext | undefined) {
208
208
  }
209
209
  else {
210
210
  try {
211
- const fileExists = errctx?.fs?.existsSync(url)
211
+ // errfs first: the CLI gives the renderer a reader without
212
+ // giving the resolver one (type.ts, `errfs`).
213
+ const reader = errctx?.errfs ?? errctx?.fs
214
+ const fileExists = reader?.existsSync(url)
212
215
  if (fileExists) {
213
- src = errctx?.fs?.readFileSync(url, 'utf8') ?? undefined
216
+ src = reader?.readFileSync(url, 'utf8') ?? undefined
214
217
  }
215
218
  }
216
219
  catch (fe: any) {
@@ -228,7 +231,7 @@ function resolveSrc(v: Val, errctx: ErrContext | undefined) {
228
231
  }
229
232
  else if (errctx) {
230
233
  src = 'SOURCE-NOT-FOUND:' + (null != url ? (' ' + url) : '') +
231
- (null == errctx?.fs ? ' (NO-FS)' : '')
234
+ (null == (errctx?.errfs ?? errctx?.fs) ? ' (NO-FS)' : '')
232
235
  }
233
236
  }
234
237
 
package/src/lang.ts CHANGED
@@ -807,6 +807,38 @@ help isolate the syntax error.`,
807
807
  // Handle defered conjuncts, where MapVal does not yet
808
808
  // exist, by creating ConjunctVal later.
809
809
  else {
810
+ // AN INCLUDE UNIFIES IN PLACE. multisource calls this hook at
811
+ // the `@`'s own source position, so `prev` holds exactly the
812
+ // pairs written BEFORE it. Folding the loaded map's keys in
813
+ // here -- host-so-far first, arriving value second -- is what
814
+ // inlining the loaded bytes at the `@` does, and mirrors
815
+ // go/lang.go's Map.Merge, which multisource-go drives one key
816
+ // at a time. A non-map load has no keys to fold and stays a
817
+ // deferred conjunct arm.
818
+ if (true === (cval as any)?.isMap) {
819
+ const lm: any = cval
820
+ for (const k of Object.keys(lm.peg)) {
821
+ const own = (prev as any)[k]
822
+ ;(prev as any)[k] = (null == own) ? lm.peg[k] :
823
+ (own?.isVal
824
+ ? new ConjunctVal({ peg: [own, lm.peg[k]] })
825
+ : lm.peg[k])
826
+ }
827
+ // The loaded map's spread joins THIS map's spread list at
828
+ // the `@`'s position: the parse pushes each `&:` onto the
829
+ // node as it is read, so `prev[SPREAD].v` already holds the
830
+ // spreads written before the `@` and nothing after it.
831
+ if (null != lm.spread?.cj) {
832
+ ;(prev as any)[SPREAD] =
833
+ ((prev as any)[SPREAD] || { o: '&', v: [] })
834
+ ;(prev as any)[SPREAD].v.push(lm.spread.cj)
835
+ }
836
+ prev.___optional = (prev.___optional || [])
837
+ for (const k of lm.optionalKeys) { prev.___optional.push(k) }
838
+ prev.___alias = (prev.___alias || [])
839
+ for (const k of lm.aliasKeys) { prev.___alias.push(k) }
840
+ return prev
841
+ }
810
842
  prev.___merge = (prev.___merge || [])
811
843
  prev.___merge.push(curr)
812
844
  return prev
@@ -1436,7 +1468,8 @@ help isolate the syntax error.`,
1436
1468
  // nested pair, which the val rule does produce -- and neither is
1437
1469
  // a trailing comma.
1438
1470
  for (const k in mo) {
1439
- if (null == mo[k] && '___merge' !== k) {
1471
+ if (null == mo[k] && '___merge' !== k &&
1472
+ '___optional' !== k && '___alias' !== k) {
1440
1473
  // Pathed at the KEY, not at the enclosing map. addsite takes
1441
1474
  // the rule's path, which here is the map's, so the error
1442
1475
  // would otherwise name the container and leave the reader to
@@ -1497,6 +1530,19 @@ help isolate the syntax error.`,
1497
1530
  mo[key] = en
1498
1531
  }
1499
1532
 
1533
+ // Marks carried over from a map include folded in the merge
1534
+ // hook above, applied here where the MapVal is built.
1535
+ if (mo.___optional || mo.___alias) {
1536
+ for (const k of (mo.___optional || [])) {
1537
+ if (!optionalKeys.includes(k)) { optionalKeys.push(k) }
1538
+ }
1539
+ for (const k of (mo.___alias || [])) {
1540
+ if (!aliasKeys.includes(k)) { aliasKeys.push(k) }
1541
+ }
1542
+ delete mo.___optional
1543
+ delete mo.___alias
1544
+ }
1545
+
1500
1546
  // Handle defered conjuncts, e.g. `{x:1 @"foo"}`
1501
1547
  if (mo.___merge) {
1502
1548
  let mop = { ...mo }
package/src/type.ts CHANGED
@@ -61,6 +61,14 @@ type AontuOptions = {
61
61
  debug?: boolean
62
62
  trace?: boolean
63
63
  fs?: FST
64
+ // errfs READS FOR THE ERROR RENDERER ONLY, never for resolution.
65
+ // A frame excerpts the file its arrow names, which means reading
66
+ // that file -- but handing the same reader to `fs` would change
67
+ // which leg the include RESOLVER takes (its file leg reads through
68
+ // the host filesystem when one is given, so a package hit can
69
+ // become a file hit), and a diagnostic must not move the boundary
70
+ // it is describing.
71
+ errfs?: FST
64
72
  deps?: any
65
73
  log?: any
66
74
  idcount?: number
@@ -103,7 +111,8 @@ type ValList = Val[]
103
111
 
104
112
  type ErrContext = {
105
113
  src?: string,
106
- fs?: FST
114
+ fs?: FST,
115
+ errfs?: FST
107
116
  } /* node:coverage ignore next 24 */
108
117
 
109
118
  export type {
@@ -101,6 +101,27 @@ class DisjunctVal extends JunctionVal {
101
101
  // before return.
102
102
  const savedErr = ctx.err
103
103
  const savedTrialMode = ctx._trialMode
104
+ // THE SANDBOX MUST NOT OUTLIVE THE TRIAL AS AN OWN PROPERTY.
105
+ // Contexts are made with Object.create(parent) and CACHED per
106
+ // (parent, key) by AontuContext.descend, so `err` and `_trialMode`
107
+ // are normally INHERITED -- and assigning them here, the restore
108
+ // included, creates own properties that shadow the ancestor on
109
+ // every later pass. A child that has run a trial of its own then
110
+ // cannot see the trial its PARENT is running: makeNilErr allocates
111
+ // a real NilVal instead of TRIAL_NIL, the refusal lands on the
112
+ // meet's real error list, and the losing member is kept as though
113
+ // it had survived -- with the nil inside it. A vet run then
114
+ // reported the conflicts of arms that should have dropped out, and
115
+ // called the document invalid where it is incomplete.
116
+ //
117
+ // So the restore DELETES what was inherited rather than writing it
118
+ // back. Both arms of both guards are live: the root meet context
119
+ // has an own `err`, a descended child does not, and `_trialMode`
120
+ // is own exactly when a disjunction is tried inside another
121
+ // disjunction's trial on the same context.
122
+ const ownErr = Object.prototype.hasOwnProperty.call(ctx, 'err')
123
+ const ownTrialMode =
124
+ Object.prototype.hasOwnProperty.call(ctx, '_trialMode')
104
125
  // A MEMBER'S TYPE FLOW IS PART OF THAT MEMBER. `refer(t)`/`rel(t)`
105
126
  // assert `t` on ANOTHER node, and the assertion may only take
106
127
  // effect if the member that makes it survives: committing every
@@ -194,8 +215,18 @@ class DisjunctVal extends JunctionVal {
194
215
  }
195
216
  }
196
217
  finally {
197
- ctx._trialMode = savedTrialMode
198
- ctx.err = savedErr
218
+ if (ownTrialMode) {
219
+ ctx._trialMode = savedTrialMode
220
+ }
221
+ else {
222
+ delete (ctx as any)._trialMode
223
+ }
224
+ if (ownErr) {
225
+ ctx.err = savedErr
226
+ }
227
+ else {
228
+ delete (ctx as any).err
229
+ }
199
230
  ;(ctx as any).referflows = savedFlows
200
231
  }
201
232
 
@@ -46,6 +46,13 @@ import { hasPlace, fillPlace } from '../val/PlaceVal'
46
46
  function trialUnify(ctx: AontuContext, a: Val, b: Val): Val | undefined {
47
47
  const savedErr = ctx.err
48
48
  const savedTrial = ctx._trialMode
49
+ // Restored by DELETION where they were inherited, for the reason
50
+ // DisjunctVal.unify's own sandbox gives at length: contexts are
51
+ // Object.create(parent) and cached per (parent, key), so writing
52
+ // these back leaves own properties that shadow the ancestor and make
53
+ // a later trial invisible to the value running inside it.
54
+ const ownErr = Object.prototype.hasOwnProperty.call(ctx, 'err')
55
+ const ownTrial = Object.prototype.hasOwnProperty.call(ctx, '_trialMode')
49
56
  const trialErr: any[] = []
50
57
 
51
58
  ctx.err = trialErr
@@ -56,8 +63,18 @@ function trialUnify(ctx: AontuContext, a: Val, b: Val): Val | undefined {
56
63
  out = unite(ctx, a, b, 'trial')
57
64
  }
58
65
  finally {
59
- ctx.err = savedErr
60
- ctx._trialMode = savedTrial
66
+ if (ownErr) {
67
+ ctx.err = savedErr
68
+ }
69
+ else {
70
+ delete (ctx as any).err
71
+ }
72
+ if (ownTrial) {
73
+ ctx._trialMode = savedTrial
74
+ }
75
+ else {
76
+ delete (ctx as any)._trialMode
77
+ }
61
78
  }
62
79
 
63
80
  return 0 < trialErr.length || out.isNil ? undefined : out
@@ -158,22 +175,28 @@ class FuncBaseVal extends FeatureVal {
158
175
 
159
176
 
160
177
  // THE PER-DESTINATION INSTANTIATION RULE (ADR-005). The default
161
- // clone shares the argument array AND the argument Vals — pinned
162
- // sharing for the move()/copy() ghost artifacts (test/spec/func.tsv,
163
- // ghost-*-innard-canon) — but a clone that is a template INSTANCE
164
- // must own the full inner structure: with the args shared,
165
- // `pack($.names, close({name: key()}))` resolved key() once inside
166
- // the one shared inner map and stamped the FIRST child's key on
167
- // every child (use-cases/BUGS.md §8). The `dup` spec flag asks for
168
- // that depth; everything else keeps the sharing it has always had.
178
+ // clone shares the argument array AND the argument Vals — the
179
+ // residuation clone, which stays at one position and wants the
180
+ // sharing, and the reference copy of a target that still holds a
181
+ // staged call, which pins the move()/copy() ghost artifacts
182
+ // (test/spec/func.tsv, ghost-*-innard-canon; ADR-025) — but a clone
183
+ // that is an INSTANCE must own the full inner structure: with the
184
+ // args shared, `pack($.names, close({name: key()}))` resolved key()
185
+ // once inside the one shared inner map and stamped the FIRST
186
+ // child's key on every child (use-cases/BUGS.md §8). The `dup` spec
187
+ // flag asks for that depth; everything else keeps the sharing it
188
+ // has always had.
169
189
  clone(ctx: AontuContext, spec?: ValSpec): Val {
170
190
  const out = super.clone(ctx, spec) as FuncBaseVal
171
191
  if (true === spec?.dup && Array.isArray(this.peg)) {
172
192
  // Every argument is a Val by construction (the parser builds
173
193
  // them; make() rebuilds from driven Vals), as the Go twin's
174
- // []Val typing states outright. The instantiation sites then
175
- // normalise every path in the clone (repathInstance), so the
176
- // argument-shaped parse paths never leak into an instance.
194
+ // []Val typing states outright. The generator and spread
195
+ // instantiation sites then normalise every path in the clone
196
+ // (repathInstance), so the argument-shaped parse paths never
197
+ // leak into an instance; the reference copy takes the paths
198
+ // this rebasing gives, which is what an absolute address in a
199
+ // copied model already expects (ADR-014).
177
200
  out.peg = this.peg.map((a: Val) => a.clone(ctx, { dup: true }))
178
201
  }
179
202
  return out
@@ -335,7 +335,15 @@ class ListVal extends BagVal {
335
335
  { mark: spec?.mark, dup: spec?.dup } : {}
336
336
  for (let entry of Object.entries(this.peg)) {
337
337
  out.peg[entry[0]] =
338
- (entry[1] as any)?.isVal ? (entry[1] as Val).clone(ctx, childspec) : entry[1]
338
+ (entry[1] as any)?.isVal ? (entry[1] as Val).clone(ctx, {
339
+ ...childspec,
340
+ // AN ELEMENT IS A POSITION, exactly as a map's child is
341
+ // (MapVal.clone). Without this an explicitly pathed clone
342
+ // rebased the list and left every element at its source
343
+ // path -- the Go twin descends by index (go/clone.go, the
344
+ // *ListVal arm of clonePathKind).
345
+ path: [...out.path, entry[0]],
346
+ }) : entry[1]
339
347
  }
340
348
  if (this.spread.cj) {
341
349
  out.spread.cj = this.spread.cj.clone(ctx, childspec)
@@ -133,7 +133,11 @@ function hasPlace(v: Val): boolean {
133
133
  // generator's to fill with its OWN source children when it fires.
134
134
  function fillPlace(v: Val, fill: Val, ctx: AontuContext): Val {
135
135
  if (true === (v as any).isPlace) {
136
- return fill
136
+ // A FILL IS A POSITION. The hole knows where it sits in the
137
+ // instance; the datum arriving in it does not, and inserted as it
138
+ // stands it keeps the paths it had at its SOURCE, so every finding
139
+ // under it names a path that does not exist.
140
+ return fill.clone(ctx, { path: [...(v as any).path] })
137
141
  }
138
142
 
139
143
  const peg: any = (v as any).peg
package/src/val/RefVal.ts CHANGED
@@ -653,7 +653,31 @@ class RefVal extends FeatureVal {
653
653
  const lifted = true !== (ctx as any).argsnap
654
654
  || true === out.mark.type || true === out.mark.hide
655
655
 
656
- out = out.clone(ctx)
656
+ // A REFERENCE'S COPY OWNS ITS ARGUMENTS (ADR-025). The copy
657
+ // is a per-destination instance exactly as a spread's or a
658
+ // generator's is, so ADR-005's rule holds here too: nothing
659
+ // path-dependent may be shared between two destinations, or
660
+ // the first destination's resolution answers for them all.
661
+ // The shallow clone shared a call's ARGUMENTS -- so
662
+ // `items: [&: $.entities.User]` over a `close({...})` target
663
+ // gave every element the one inner map, whose path each
664
+ // element rebased in turn, and the constraint that failed at
665
+ // element 2 reported element 0's path (use-cases GAP 8).
666
+ //
667
+ // A STAGED CALL IS THE EXCEPTION, because it has not decided
668
+ // yet. Its arguments are still being driven AT ITS OWN SITE
669
+ // (the staging rule, G8 phase 0), and a copy that owned them
670
+ // would drive its own set at the referring position instead:
671
+ // the relative `.side_effect` in a `match()` would read the
672
+ // referring field's siblings (use-cases/09-agent-tools), and
673
+ // an alias naming an `emit` rule table -- a template, which
674
+ // is exactly a value copied before it resolves -- would read
675
+ // its recursive `%w` as a self-reference. A staged call
676
+ // ANYWHERE in the target counts: what is referenced is
677
+ // usually the conjunct the call sits in, not the call.
678
+ // The copy shares what the source is still settling, and
679
+ // owns the rest.
680
+ out = out.clone(ctx, { dup: !out.holdsStaged })
657
681
 
658
682
  if (lifted) {
659
683
  walk(out, (_key: string | number | undefined, val: Val) => {
package/src/val/Val.ts CHANGED
@@ -44,9 +44,12 @@ type ValSpec = {
44
44
  // depth explicitly: `dup: true` makes FuncBaseVal, PrefVal and
45
45
  // OpBaseVal clone their inner Vals too, and the bag/junction clones
46
46
  // carry the flag down. Set by pack/each template instantiation,
47
- // filter condition testing, and spread application (MapVal/
48
- // ListVal.spreadClone) — never by the residuation or ref-resolution
49
- // clones, whose sharing is pinned behaviour.
47
+ // filter condition testing, spread application (MapVal/
48
+ // ListVal.spreadClone), and REFERENCE RESOLUTION of a target that
49
+ // holds no staged call, whose copy is a per-destination instance
50
+ // too (ADR-025) — never by the residuation clone, whose sharing is
51
+ // pinned behaviour, and never by a copy of something still being
52
+ // settled at its own site, which is what keeps the ghost rows.
50
53
  dup?: boolean,
51
54
 
52
55
 
@@ -440,6 +443,33 @@ abstract class Val {
440
443
  }
441
444
 
442
445
 
446
+ // A STAGED CALL STANDING ANYWHERE IN THIS VALUE (the `staged` flag,
447
+ // G8 phase 0). Such a call has not decided: its arguments are still
448
+ // being driven AT ITS OWN SITE, so a REFERENCE's copy shares it
449
+ // rather than owning a set of arguments it would drive at the
450
+ // referring position instead (RefVal.find, ADR-025). Not cached:
451
+ // unlike isPathDependent this is a fact about the value's current
452
+ // state, and the whole point is that it stops being true.
453
+ get holdsStaged(): boolean {
454
+ if (true === (this as any).staged) {
455
+ return true
456
+ }
457
+ const peg: any = this.peg
458
+ if (Array.isArray(peg)) {
459
+ for (let i = 0; i < peg.length; i++) {
460
+ if (true === peg[i]?.isVal && peg[i].holdsStaged) return true
461
+ }
462
+ }
463
+ else if (null != peg && 'object' === typeof peg) {
464
+ for (const k in peg) {
465
+ if (true === peg[k]?.isVal && peg[k].holdsStaged) return true
466
+ }
467
+ }
468
+ const spreadCj = (this as any).spread?.cj as Val | undefined
469
+ return true === spreadCj?.isVal && (spreadCj as any).holdsStaged
470
+ }
471
+
472
+
443
473
  // PUT A MINTED VALUE WHERE THIS ONE STANDS: the site travels, and so
444
474
  // does provenance, because the two answer one question. A narrowed
445
475
  // disjunction, a lifted kind, a resolved reference -- each is a