aontu 0.48.2 → 0.48.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aontu",
3
- "version": "0.48.2",
3
+ "version": "0.48.3",
4
4
  "main": "dist/aontu.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/aontu.d.ts",
@@ -47,12 +47,13 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "dependencies": {
50
- "@tabnas/debug": "0.2.5",
51
- "@tabnas/directive": "0.2.1",
52
- "@tabnas/expr": "0.2.3",
53
- "@tabnas/jsonic": "0.3.1",
54
- "@tabnas/multisource": "0.3.2",
55
- "@tabnas/path": "0.2.1"
50
+ "@tabnas/debug": "0.2.9",
51
+ "@tabnas/directive": "0.4.2",
52
+ "@tabnas/expr": "0.4.2",
53
+ "@tabnas/jsonic": "0.4.3",
54
+ "@tabnas/multisource": "0.4.4",
55
+ "@tabnas/parser": "0.6.0",
56
+ "@tabnas/path": "0.2.3"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/node": "26.1.1",
package/src/lang.ts CHANGED
@@ -100,6 +100,20 @@ let AontuJsonic: Plugin = function AontuLang(jsonic: Jsonic) {
100
100
 
101
101
  jsonic.use(asPlugin(Path))
102
102
 
103
+ // Only # line comments are valid Aontu syntax (see
104
+ // docs/reference-language.md; go/lang.go sets the same). Clear the
105
+ // underlying jsonic comment markers entirely, then define # directly,
106
+ // so the comment set is hash-only regardless of those defaults.
107
+ jsonic.options({ comment: { def: null } })
108
+ jsonic.options({
109
+ comment: {
110
+ lex: true,
111
+ def: {
112
+ hash: { line: true, start: '#', lex: true },
113
+ },
114
+ },
115
+ })
116
+
103
117
  // TODO: refactor Val constructor
104
118
  // let addsite = (v: Val, p: string[]) => (v.path = [...(p || [])], v)
105
119
  let addsite = (v: Val, r: Rule, ctx: JsonicContext) => {
@@ -218,26 +232,42 @@ help isolate the syntax error.`,
218
232
  }
219
233
 
220
234
 
235
+ // A dangling operator (`a:1|`, `a:$`, `a:*` at end of input) leaves
236
+ // null/undefined unfilled terms. Junction ops drop them (so `a:1&`
237
+ // and `a:1|` are just 1); ops missing a required operand become an
238
+ // incomplete_expression nil, surfaced by generate() as a "Cannot
239
+ // resolve value" error (the Go port mirrors this in lang.go).
240
+ const dropUnfilled = (terms: any) => terms.filter((t: any) => null != t)
241
+
242
+ const incompleteNil = (r: Rule, ctx: JsonicContext) =>
243
+ addsite(new NilVal({ why: 'incomplete_expression' }), r, ctx)
244
+
221
245
  let opmap: any = {
222
246
  'conjunct-infix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) =>
223
- addsite(new ConjunctVal({ peg: terms }), r, ctx),
247
+ addsite(new ConjunctVal({ peg: dropUnfilled(terms) }), r, ctx),
224
248
 
225
249
  'disjunct-infix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) =>
226
- addsite(new DisjunctVal({ peg: terms }), r, ctx),
250
+ addsite(new DisjunctVal({ peg: dropUnfilled(terms) }), r, ctx),
227
251
 
228
252
  'dot-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
253
+ terms = dropUnfilled(terms)
254
+ if (0 === terms.length) return incompleteNil(r, ctx)
229
255
  return addsite(new RefVal({ peg: terms, prefix: true }), r, ctx)
230
256
  },
231
257
 
232
258
  'dot-infix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
233
- // // console.log('DOT-INFIX-OP', terms)
259
+ terms = dropUnfilled(terms)
260
+ if (0 === terms.length) return incompleteNil(r, ctx)
234
261
  return addsite(new RefVal({ peg: terms }), r, ctx)
235
262
  },
236
263
 
237
- 'star-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) =>
238
- addsite(new PrefVal({ peg: terms[0] }), r, ctx),
264
+ 'star-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
265
+ if (null == terms[0]) return incompleteNil(r, ctx)
266
+ return addsite(new PrefVal({ peg: terms[0] }), r, ctx)
267
+ },
239
268
 
240
269
  'dollar-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
270
+ if (null == terms[0]) return incompleteNil(r, ctx)
241
271
  // $.a.b absolute path
242
272
  if (terms[0] instanceof RefVal) {
243
273
  terms[0].absolute = true
@@ -247,19 +277,34 @@ help isolate the syntax error.`,
247
277
  },
248
278
 
249
279
  'plus-infix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
280
+ if (null == terms[0] || null == terms[1]) return incompleteNil(r, ctx)
250
281
  return addsite(new PlusOpVal({ peg: [terms[0], terms[1]] }), r, ctx)
251
282
  },
252
283
 
253
284
  'negative-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
254
285
  let val = terms[0]
255
- val.peg = -1 * val.peg
286
+ if (null == val) return incompleteNil(r, ctx)
287
+ // Negating a non-numeric operand (`k-x` splits into k, -x) is an
288
+ // error nil, not NaN (mirrors negate() in go/lang.go).
289
+ if (!(val instanceof IntegerVal) && !(val instanceof NumberVal)) {
290
+ return addsite(new NilVal({ why: 'negative' }), r, ctx)
291
+ }
292
+ // Build a fresh Val rather than mutating in place: the expr plugin
293
+ // can evaluate the same node twice (e.g. inside `*-1` or a
294
+ // disjunct member), and an in-place `peg = -peg` applied twice
295
+ // silently un-negates the number.
296
+ let peg = -1 * val.peg
256
297
  // Normalize -0 to 0 (keeps the AST and canon free of negative zero).
257
- if (0 === val.peg) val.peg = 0
258
- return addsite(val, r, ctx)
298
+ if (0 === peg) peg = 0
299
+ const out = val instanceof IntegerVal
300
+ ? new IntegerVal({ peg })
301
+ : new NumberVal({ peg })
302
+ return addsite(out, r, ctx)
259
303
  },
260
304
 
261
305
  'positive-prefix': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
262
306
  let val = terms[0]
307
+ if (null == val) return incompleteNil(r, ctx)
263
308
  return addsite(val, r, ctx)
264
309
  },
265
310
 
@@ -275,6 +320,8 @@ help isolate the syntax error.`,
275
320
  peg: args
276
321
  })
277
322
  }
323
+ // `a:()` — grouping parens with nothing inside.
324
+ if (null == val) return incompleteNil(r, ctx)
278
325
  const out = addsite(val, r, ctx)
279
326
  return out
280
327
  },
@@ -375,6 +422,24 @@ help isolate the syntax error.`,
375
422
  const OPTKEY = [TX, ST, NR]
376
423
 
377
424
 
425
+ jsonic.rule('expr', (rs: RuleSpec) => {
426
+ rs.close([
427
+ // A `&` followed by `:` after an expression value belongs to the
428
+ // enclosing map as a spread, not to the expression as a conjunct
429
+ // — backtrack both tokens so the expression completes (and
430
+ // evaluates to a Val) and the map's spread alts take over. This
431
+ // is what makes `k1:$flag &:boolean` parse: without it the expr
432
+ // plugin consumes the `&` as an infix conjunct, chokes on the
433
+ // `:`, and leaves the raw unevaluated expr node in the map
434
+ // (mirrors the expr-rule PrependClose in go/lang.go). The
435
+ // `n: { expr: 0 }` reset matches the plugin's own expr-end alts —
436
+ // the evaluation after-close only fires when the counter is 0.
437
+ { s: [CJ, CL], b: 2, n: { expr: 0 }, g: 'expr,expr-end,spread' },
438
+ ])
439
+ return rs
440
+ })
441
+
442
+
378
443
  jsonic.rule('val', (rs: RuleSpec) => {
379
444
 
380
445
  rs
@@ -419,8 +484,13 @@ help isolate the syntax error.`,
419
484
  valnode = addsite(new StringVal({ peg: r.node }), r, ctx)
420
485
  }
421
486
  else if ('number' === valtype) {
487
+ // An overflowing literal (1e999) lexes to Infinity; that is an
488
+ // error value, not a number (mirrors not_number in go/lang.go).
489
+ if (!Number.isFinite(r.node)) {
490
+ valnode = addsite(new NilVal({ why: 'not_number' }), r, ctx)
491
+ }
422
492
  // 1.0 in source is *not* an integer
423
- if (Number.isInteger(r.node) && !r.o0.src.includes('.')) {
493
+ else if (Number.isInteger(r.node) && !r.o0.src.includes('.')) {
424
494
  valnode = addsite(new IntegerVal({ peg: r.node, src: r.o0.src }), r, ctx)
425
495
  }
426
496
  else {
@@ -466,6 +536,14 @@ help isolate the syntax error.`,
466
536
 
467
537
  let mo = r.node
468
538
 
539
+ // An elided value (`a:`) leaves a raw null/undefined that never
540
+ // passed through the val rule; make it an explicit NullVal.
541
+ for (const k in mo) {
542
+ if (null == mo[k] && '___merge' !== k) {
543
+ mo[k] = addsite(new NullVal({ peg: null }), r, ctx)
544
+ }
545
+ }
546
+
469
547
  // Handle defered conjuncts, e.g. `{x:1 @"foo"}`
470
548
  if (mo.___merge) {
471
549
  let mop = { ...mo }
@@ -502,6 +580,14 @@ help isolate the syntax error.`,
502
580
 
503
581
  let ao = r.node
504
582
 
583
+ // An elided element (`[,]`) is a raw null that never passed
584
+ // through the val rule; make it an explicit NullVal.
585
+ for (let i = 0; i < ao.length; i++) {
586
+ if (null == ao[i]) {
587
+ ao[i] = addsite(new NullVal({ peg: null }), r, ctx)
588
+ }
589
+ }
590
+
505
591
  if (ao.___merge) {
506
592
  let aop = [...ao]
507
593
  delete (aop as any).___merge
@@ -593,7 +679,12 @@ help isolate the syntax error.`,
593
679
  })
594
680
 
595
681
  .close([
596
- { s: [CJ, CL], c: (r) => r.lte('dmap', 1), r: 'pair', b: 2, g: 'spread,json,pair' },
682
+ // A following `&:` starts a sibling spread pair in the current
683
+ // map: directly inside a braced map (pk<=0) at any depth, or in
684
+ // the implicit top-level map (dmap<=1). Inside an implicit
685
+ // colon-chain map (pk>0) it bubbles up instead (second alt), so
686
+ // `a:b:1 &:2` attaches the spread to a's map, not b's.
687
+ { s: [CJ, CL], c: (r) => r.lte('pk', 0) || r.lte('dmap', 1), r: 'pair', b: 2, g: 'spread,json,pair' },
597
688
  { s: [CJ, CL], b: 2, g: 'spread,json,more' }
598
689
  ])
599
690
 
@@ -689,6 +780,13 @@ function makeModelResolver(options: any) {
689
780
  ) {
690
781
 
691
782
  let path = 'string' === typeof spec ? spec : spec?.peg
783
+
784
+ // A bare `@` with no path (`a:@`) has nothing to resolve; report
785
+ // not-found instead of crashing in the underlying resolvers.
786
+ if (null == path || '' === path) {
787
+ return { found: false, path: '' + (path ?? ''), search: [] }
788
+ }
789
+
692
790
  let search: any = []
693
791
  let res = memResolver(path, popts, rule, ctx, jsonic)
694
792
  res.path = path
@@ -718,6 +816,49 @@ function makeModelResolver(options: any) {
718
816
  }
719
817
 
720
818
 
819
+ // rawToVal converts a raw parse node (or raw elements inside one) into
820
+ // the matching Val. Used for implicit top-level lists, whose nodes skip
821
+ // the aontu val rule conversions (mirrors asVal in go/lang.go; like
822
+ // there, source text is unavailable, so an integral number is an
823
+ // integer).
824
+ function rawToVal(n: any): Val {
825
+ if (null == n) {
826
+ return new NullVal({ peg: null })
827
+ }
828
+ if (true === n.isVal) {
829
+ return n
830
+ }
831
+ if (Array.isArray(n)) {
832
+ return new ListVal({ peg: n.map(rawToVal) })
833
+ }
834
+ const t = typeof n
835
+ if ('string' === t) {
836
+ return new StringVal({ peg: n })
837
+ }
838
+ if ('number' === t) {
839
+ return Number.isInteger(n) ?
840
+ new IntegerVal({ peg: n }) : new NumberVal({ peg: n })
841
+ }
842
+ if ('boolean' === t) {
843
+ return new BooleanVal({ peg: n })
844
+ }
845
+ if ('object' === t) {
846
+ // An expr-plugin internal (operator descriptor) leaking through a
847
+ // degenerate parse (`k2.b K:1`) is not data — reject it rather
848
+ // than emitting raw internals in generated output.
849
+ if (undefined !== (n as any).OP_MARK) {
850
+ return new NilVal({ why: 'parse_unknown' })
851
+ }
852
+ const peg: Record<string, Val> = {}
853
+ for (const k in n) {
854
+ peg[k] = rawToVal(n[k])
855
+ }
856
+ return new MapVal({ peg })
857
+ }
858
+ return new NilVal({ why: 'parse_unknown' })
859
+ }
860
+
861
+
721
862
  class Lang {
722
863
  jsonic: Jsonic
723
864
  opts: AontuOptions
@@ -784,6 +925,14 @@ class Lang {
784
925
 
785
926
  try {
786
927
  val = this.jsonic(src, jm)
928
+
929
+ // An implicit top-level list (`a b`, `1,2`) is built by the core
930
+ // jsonic grammar without passing through the aontu val/list rules,
931
+ // so the root (and its elements) arrive as raw JS values. Convert
932
+ // them the same way the Go port's asVal post-walk does.
933
+ if (null != val && true !== (val as any).isVal) {
934
+ val = rawToVal(val)
935
+ }
787
936
  }
788
937
  catch (e: any) {
789
938
  if (e instanceof JsonicError || 'JsonicError' === e.constructor.name) {
package/src/tsconfig.json CHANGED
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "composite": true,
4
3
  "esModuleInterop": true,
5
4
  "module": "nodenext",
6
5
  "noEmitOnError": true,
package/src/unify.ts CHANGED
@@ -128,6 +128,7 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
128
128
  || b.isDisjunct
129
129
  || b.isRef
130
130
  || b.isPref
131
+ || b.isVar
131
132
  || b.isFunc
132
133
  || b.isExpect
133
134
  ) {
package/src/val/BagVal.ts CHANGED
@@ -86,6 +86,18 @@ abstract class BagVal extends FeatureVal {
86
86
 
87
87
  const optional = this.optionalKeys.includes('' + p)
88
88
 
89
+ // Lists append compactly: a skipped element (hidden, dropped
90
+ // optional) must not leave a hole/null at its index (matches the
91
+ // Go port, which also drops skipped elements).
92
+ const put = (v: any) => {
93
+ if (this.isMap) {
94
+ out[p] = v
95
+ }
96
+ else {
97
+ out.push(v)
98
+ }
99
+ }
100
+
89
101
  // Optional unresolved disjuncts are not an error, just dropped.
90
102
  if (child.isDisjunct && optional) {
91
103
  const dctx = ctx.clone({ err: [], collect: true })
@@ -96,7 +108,7 @@ abstract class BagVal extends FeatureVal {
96
108
  continue
97
109
  }
98
110
 
99
- out[p] = cval
111
+ put(cval)
100
112
  }
101
113
 
102
114
  else if (child.isScalar
@@ -122,7 +134,15 @@ abstract class BagVal extends FeatureVal {
122
134
  continue
123
135
  }
124
136
 
125
- out[p] = cval
137
+ // A child that generates nothing contributes nothing: setting
138
+ // `undefined` would leave husk entries like {"q k": undefined}
139
+ // (the Go port also drops such children). Any real failure has
140
+ // already been recorded on ctx and raises below.
141
+ if (undefined === cval) {
142
+ continue
143
+ }
144
+
145
+ put(cval)
126
146
  }
127
147
  else if (child.isNil) {
128
148
  ctx.adderr(child)
@@ -41,25 +41,42 @@ class PlusOpVal extends OpBaseVal {
41
41
 
42
42
 
43
43
  operate(_ctx: AontuContext, args: Val[]) {
44
- let a: any = this.primatize(args[0]?.peg)
45
- let b: any = this.primatize(args[1]?.peg)
44
+ // Only concrete scalar operands are valid: anything else (kinds,
45
+ // maps, lists, null, top, funcs) must not coerce — the JS `+` would
46
+ // leak internals like "[object Object]" into output. A non-scalar
47
+ // operand leaves the op unresolved, which generate() reports.
48
+ const prim = (v: any) => {
49
+ // A pref operand contributes its preferred value (`pref(1)+2`).
50
+ while (v?.isPref) {
51
+ v = v.peg
52
+ }
53
+ const p = v?.isVal && v.isScalar ? v.peg : undefined
54
+ const t = typeof p
55
+ return 'string' === t || 'number' === t || 'boolean' === t ? p : undefined
56
+ }
57
+ let a: any = prim(args[0])
58
+ let b: any = prim(args[1])
59
+
60
+ if (undefined === a || undefined === b) {
61
+ return undefined
62
+ }
63
+
64
+ const at = typeof a
65
+ const bt = typeof b
46
66
 
47
67
  let peg = undefined
48
- if (undefined === a && undefined !== b) {
49
- peg = b
68
+ if ('boolean' === at && 'boolean' === bt) {
69
+ peg = a || b
70
+ }
71
+ else if ('string' === at || 'string' === bt) {
72
+ peg = String(a) + String(b)
50
73
  }
51
- else if (undefined === b && undefined !== a) {
52
- peg = a
74
+ else if ('boolean' === at || 'boolean' === bt) {
75
+ // boolean mixed with a number does not coerce (no JS 0/1).
76
+ return undefined
53
77
  }
54
78
  else {
55
- const at = typeof a
56
- const bt = typeof b
57
- if ('boolean' === at && 'boolean' === bt) {
58
- peg = a || b
59
- }
60
- else {
61
- peg = a + b
62
- }
79
+ peg = a + b
63
80
  }
64
81
 
65
82
  let pegtype = typeof peg
package/src/val/RefVal.ts CHANGED
@@ -269,7 +269,10 @@ class RefVal extends FeatureVal {
269
269
  return
270
270
  }
271
271
  else {
272
- part = '' + part.peg
272
+ // The resolved variable IS a path segment: $seg.r with
273
+ // seg="x" reads ...x.r (previously the coerced value was
274
+ // dropped, silently reading the path without it).
275
+ parts.push('' + part.peg)
273
276
  }
274
277
  }
275
278
  }
package/src/val/VarVal.ts CHANGED
@@ -38,6 +38,10 @@ import {
38
38
  explainClose,
39
39
  } from '../utility'
40
40
 
41
+ import { unite } from '../unify'
42
+
43
+ import { top } from './top'
44
+
41
45
 
42
46
  // TODO: KEY, SELF, PARENT are reserved names - error
43
47
 
@@ -67,7 +71,12 @@ class VarVal extends FeatureVal {
67
71
  nameVal = this.peg
68
72
  }
69
73
  else {
70
- nameVal = this.peg.unify(peer, te ? ctx.clone({ explain: ec(te, 'PEG') }) : ctx)
74
+ // Resolve the NAME against TOP only the peer applies to the
75
+ // variable's resolved VALUE below, not to its name (so
76
+ // `k1:$flag &:boolean` checks the boolean value of $flag
77
+ // against the spread, instead of unifying the string "flag"
78
+ // with it).
79
+ nameVal = this.peg.unify(top(), te ? ctx.clone({ explain: ec(te, 'PEG') }) : ctx)
71
80
  }
72
81
  }
73
82
  else {
@@ -107,6 +116,15 @@ class VarVal extends FeatureVal {
107
116
  else {
108
117
  out = makeNilErr(ctx, 'invalid_var_kind', this, peer)
109
118
  }
119
+
120
+ // A non-TOP peer (e.g. a spread constraint unified against the
121
+ // var) applies to the RESOLVED value rather than being silently
122
+ // dropped (mirrors VarVal.Unify in go/varval — the resolved
123
+ // value unites with the peer).
124
+ if (!out.isNil && null != peer && !peer.isTop) {
125
+ out = unite(te ? ctx.clone({ explain: ec(te, 'VAL') }) : ctx,
126
+ out, peer, 'var-val')
127
+ }
110
128
  }
111
129
  else {
112
130
  out = makeNilErr(ctx, 'var[' + typeof nameVal + ']', this, peer)
package/src/val/top.ts CHANGED
@@ -3,10 +3,13 @@
3
3
 
4
4
  import { TopVal } from './TopVal'
5
5
 
6
- const TOP = new TopVal({})
7
-
8
6
 
7
+ // A fresh instance per call: unify mutates Vals in place (paths, marks,
8
+ // site via addsite for a literal `top` in source), so a shared TOP
9
+ // singleton is silently corrupted by one parse and poisons every later
10
+ // parse in the same process (e.g. a hide() walk marking the shared TOP
11
+ // hid every subsequent unresolved top).
9
12
  export function top(): TopVal {
10
- return TOP
13
+ return new TopVal({})
11
14
  }
12
15