aontu 0.52.0 → 0.52.1

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 (52) hide show
  1. package/dist/aontu.d.ts +1 -1
  2. package/dist/aontu.js +50 -1
  3. package/dist/aontu.js.map +1 -1
  4. package/dist/ctx.d.ts +3 -0
  5. package/dist/ctx.js +1 -0
  6. package/dist/ctx.js.map +1 -1
  7. package/dist/err.js +10 -1
  8. package/dist/err.js.map +1 -1
  9. package/dist/hints.js +56 -1
  10. package/dist/hints.js.map +1 -1
  11. package/dist/lang.js +235 -15
  12. package/dist/lang.js.map +1 -1
  13. package/dist/lsp.js +4 -3
  14. package/dist/lsp.js.map +1 -1
  15. package/dist/unify.d.ts +2 -1
  16. package/dist/unify.js +55 -3
  17. package/dist/unify.js.map +1 -1
  18. package/dist/val/ConstraintVal.d.ts +42 -1
  19. package/dist/val/ConstraintVal.js +1048 -107
  20. package/dist/val/ConstraintVal.js.map +1 -1
  21. package/dist/val/ExpectVal.d.ts +1 -0
  22. package/dist/val/ExpectVal.js +14 -0
  23. package/dist/val/ExpectVal.js.map +1 -1
  24. package/dist/val/FuncBaseVal.js +7 -1
  25. package/dist/val/FuncBaseVal.js.map +1 -1
  26. package/dist/val/ListVal.js +17 -4
  27. package/dist/val/ListVal.js.map +1 -1
  28. package/dist/val/MapVal.js +9 -0
  29. package/dist/val/MapVal.js.map +1 -1
  30. package/dist/val/OpBaseVal.js +5 -1
  31. package/dist/val/OpBaseVal.js.map +1 -1
  32. package/dist/val/RefVal.js +69 -23
  33. package/dist/val/RefVal.js.map +1 -1
  34. package/dist/val/Val.d.ts +1 -1
  35. package/dist/val/Val.js +0 -3
  36. package/dist/val/Val.js.map +1 -1
  37. package/package.json +10 -10
  38. package/src/aontu.ts +58 -1
  39. package/src/ctx.ts +9 -0
  40. package/src/err.ts +10 -1
  41. package/src/hints.ts +63 -1
  42. package/src/lang.ts +258 -16
  43. package/src/lsp.ts +4 -3
  44. package/src/unify.ts +59 -2
  45. package/src/val/ConstraintVal.ts +1195 -111
  46. package/src/val/ExpectVal.ts +15 -0
  47. package/src/val/FuncBaseVal.ts +8 -2
  48. package/src/val/ListVal.ts +18 -4
  49. package/src/val/MapVal.ts +11 -0
  50. package/src/val/OpBaseVal.ts +6 -2
  51. package/src/val/RefVal.ts +72 -22
  52. package/src/val/Val.ts +8 -1
package/src/lang.ts CHANGED
@@ -108,6 +108,10 @@ import {
108
108
  AboveConstraintVal,
109
109
  BelowConstraintVal,
110
110
  NeqConstraintVal,
111
+ ReConstraintVal,
112
+ LengthConstraintVal,
113
+ UniqueConstraintVal,
114
+ MustConstraintVal,
111
115
  } from './val/ConstraintVal'
112
116
 
113
117
 
@@ -420,6 +424,23 @@ help isolate the syntax error.`,
420
424
  above: AboveConstraintVal,
421
425
  below: BelowConstraintVal,
422
426
  neq: NeqConstraintVal,
427
+
428
+ // G1 phase 2: pattern membership, over the portable subset both
429
+ // host regex engines agree on (nonPortableRe in ConstraintVal.ts).
430
+ re: ReConstraintVal,
431
+
432
+ // G1 phase 3: the sizing atoms. Both are properties of a CONTAINER
433
+ // (or, for length, of a string) rather than comparisons against a
434
+ // value, which is why `unique` is the one built-in taking no
435
+ // argument at all.
436
+ length: LengthConstraintVal,
437
+ unique: UniqueConstraintVal,
438
+
439
+ // G1 phase 5: Band B. `must` is the one atom the algebra does not
440
+ // reason about -- it is checked against the finished value and
441
+ // reported with the author's own message, never simplified and
442
+ // never consulted for emptiness or subsumption.
443
+ must: MustConstraintVal,
423
444
  }
424
445
 
425
446
 
@@ -515,9 +536,47 @@ help isolate the syntax error.`,
515
536
  'func-paren': (r: Rule, ctx: JsonicContext, _op: Op, terms: any) => {
516
537
  let val = terms[1]
517
538
  const fname = terms[0]
539
+
518
540
  if ('' !== fname) {
519
541
  const funcval = funcMap[fname]
520
- const args = terms.slice(1)
542
+ // Arity is known for every built-in, so a surplus or missing
543
+ // argument is a mistake in the SOURCE, refused here where the
544
+ // author can see it (issue #51). It was previously left to each
545
+ // function to notice or not: the two ports disagreed on `upper()`
546
+ // and on `close()`, and `min(1,2)` noticed nothing at all -- it
547
+ // built a constraint that merely refused to generate later, with
548
+ // a message about the map rather than about the call.
549
+ //
550
+ // Counted BEFORE the rawToVal pass below, which is what makes the
551
+ // count possible: a comma group arrives as a RAW array and a
552
+ // written list literal as a ListVal, and rawToVal turns the first
553
+ // into the second.
554
+ const arity = funcArity[fname]
555
+ if (null != arity) {
556
+ const got = writtenArgCount(terms.slice(1))
557
+ if (got < arity[0] || (-1 !== arity[1] && got > arity[1])) {
558
+ // details is assigned AFTER construction: the NilVal
559
+ // constructor does not read it from its spec (only
560
+ // NilVal.make does), so passing it in the spec left the
561
+ // hint's {func}/{want}/{got} placeholders un-injected and
562
+ // printed literally.
563
+ const nil: any = new NilVal({ why: 'func_arity' })
564
+ nil.details = {
565
+ func: fname,
566
+ want: arityText(arity[0], arity[1]),
567
+ got: '' + got,
568
+ }
569
+ return addsite(nil, r, ctx)
570
+ }
571
+ }
572
+ // rawToVal EVERY argument. A degenerate expression can hand this
573
+ // handler raw parse values rather than Vals -- `pref(1-3)` arrives
574
+ // as the plain numbers 1 and -3 -- and a func's peg is unified
575
+ // element by element, so a raw one reached `arg.unify(...)` and
576
+ // threw. The unifier's catch-all turned that into an `internal`
577
+ // verdict: a crash reported as a unification result (issue #49).
578
+ // The Go port has always converted here (asVal in evaluate).
579
+ const args = terms.slice(1).map(rawToVal)
521
580
  val = null == funcval ?
522
581
  new NilVal({ why: 'unknown_function' }) :
523
582
  new funcval({
@@ -526,7 +585,11 @@ help isolate the syntax error.`,
526
585
  }
527
586
  // `a:()` — grouping parens with nothing inside.
528
587
  if (null == val) return incompleteNil(r, ctx)
529
- const out = addsite(val, r, ctx)
588
+ // ... and the same for a GROUPING paren, whose value is passed
589
+ // straight through: `(([]%))` yielded a raw array, and addsite went
590
+ // on to write a site onto it, throwing a TypeError that escaped the
591
+ // unifier entirely ("Cannot set properties of undefined").
592
+ const out = addsite(rawToVal(val), r, ctx)
530
593
  return out
531
594
  },
532
595
  }
@@ -783,13 +846,60 @@ help isolate the syntax error.`,
783
846
  let mo = r.node
784
847
 
785
848
  // An elided value (`a:`) leaves a raw null/undefined that never
786
- // passed through the val rule; make it an explicit NullVal.
849
+ // passed through the val rule. It is REFUSED rather than made a
850
+ // null (issue #48): a key with nothing after the colon is a
851
+ // mistake in the source, and turning it into a value made that
852
+ // mistake indistinguishable from a deliberate `a:null`.
853
+ //
854
+ // A colon chain (`a: b:1`) is not an elision -- the value is the
855
+ // nested pair, which the val rule does produce -- and neither is
856
+ // a trailing comma.
787
857
  for (const k in mo) {
788
858
  if (null == mo[k] && '___merge' !== k) {
789
- mo[k] = addsite(new NullVal({ peg: null }), r, ctx)
859
+ // Pathed at the KEY, not at the enclosing map. addsite takes
860
+ // the rule's path, which here is the map's, so the error
861
+ // would otherwise name the container and leave the reader to
862
+ // work out which key was elided.
863
+ const en: any = addsite(new NilVal({ why: 'elided_value' }), r, ctx)
864
+ en.path = [...(r.k?.path ?? []), k]
865
+ mo[k] = en
866
+
867
+ // An elided value under an OPTIONAL key stops being optional.
868
+ // Optionality is about a value that may be absent at
869
+ // GENERATE; it does not excuse a source that stops after the
870
+ // colon. Left optional, the refusal was dropped with the key
871
+ // and `a?:` generated `{}` -- a silent nothing, which is
872
+ // worse than either the old null or the error.
873
+ const oi = optionalKeys.indexOf(k)
874
+ if (-1 !== oi) {
875
+ optionalKeys.splice(oi, 1)
876
+ }
877
+ }
878
+ }
879
+
880
+ // ... and the OPTIONAL spelling, `a?:`, which does not leave a
881
+ // null behind to be found: its value never reaches the node at
882
+ // all, so the key is simply absent and the map generated without
883
+ // it. A key recorded as optional but missing from the node was
884
+ // written with nothing after its colon.
885
+ for (const k of optionalKeys) {
886
+ if (!(k in mo)) {
887
+ mo[k] = addsite(new NilVal({ why: 'elided_value' }), r, ctx)
790
888
  }
791
889
  }
792
890
 
891
+ // An elided SPREAD (`x:$obj&:` with nothing after the colon)
892
+ // refuses the whole map, not a key (issue #48). A spread is not a
893
+ // child, so a refusal stored in its place has nothing to attach
894
+ // to: `x:&:` has no children for the spread to apply to, and the
895
+ // map would generate as `{}` with the mistake silently gone.
896
+ // Refusing the container is what makes it visible at all.
897
+ const sp: any = (mo as any)[SPREAD]
898
+ if (sp && sp.v.some((sv: any) => null == sv)) {
899
+ r.node = addsite(new NilVal({ why: 'elided_value' }), r, ctx)
900
+ return undefined
901
+ }
902
+
793
903
  // Handle defered conjuncts, e.g. `{x:1 @"foo"}`
794
904
  if (mo.___merge) {
795
905
  let mop = { ...mo }
@@ -826,11 +936,15 @@ help isolate the syntax error.`,
826
936
 
827
937
  let ao = r.node
828
938
 
829
- // An elided element (`[,]`) is a raw null that never passed
830
- // through the val rule; make it an explicit NullVal.
939
+ // An elided ELEMENT (`[,]`, `[1,,2]`) is refused for the same
940
+ // reason as an elided map value (issue #48). A trailing comma
941
+ // (`[1,]`) is not an elision and never reaches here.
831
942
  for (let i = 0; i < ao.length; i++) {
832
943
  if (null == ao[i]) {
833
- ao[i] = addsite(new NullVal({ peg: null }), r, ctx)
944
+ // Pathed at the INDEX, for the same reason as the map case.
945
+ const en: any = addsite(new NilVal({ why: 'elided_value' }), r, ctx)
946
+ en.path = [...(r.k?.path ?? []), '' + i]
947
+ ao[i] = en
834
948
  }
835
949
  }
836
950
 
@@ -865,6 +979,51 @@ help isolate the syntax error.`,
865
979
  }
866
980
 
867
981
 
982
+ // A pair in LIST position writes its value at `node[key]` like any
983
+ // other pair, and the enclosing list's node is an ARRAY -- so a numeric
984
+ // key lands on an index and becomes an element, and may land on an
985
+ // index a real element already holds (`[5,0:1]`). Since the pair must
986
+ // contribute nothing (issue #40), the slot is photographed before the
987
+ // value is parsed and put back afterwards. Restoring beats deleting for
988
+ // exactly the overwrite case: deleting `[5,0:1]`'s index 0 would take
989
+ // the 5 with it, where restoring gives back the list the pair was
990
+ // never part of.
991
+ //
992
+ // `length` is saved too: writing past the end grows an array, and
993
+ // `[5,1?:9]` must be [5] again and not [5, <hole>].
994
+ // Typed as a string bag deliberately: the slot may be named by a
995
+ // non-numeric key (`[x:1]`), which the array type would refuse. Both
996
+ // helpers run only from the elem rule, whose node is the enclosing
997
+ // list, so neither guards against a non-array node — a guard there
998
+ // proved unreachable and dead code is worse than none.
999
+ const asSlots = (r: Rule): Record<string, any> => r.node as any
1000
+
1001
+ const snapshotPairSlot = (r: Rule, key: string) => {
1002
+ const node = asSlots(r)
1003
+ r.u.aontu_pair_slot = {
1004
+ key,
1005
+ had: Object.prototype.hasOwnProperty.call(node, key),
1006
+ was: node[key],
1007
+ len: node.length,
1008
+ }
1009
+ }
1010
+
1011
+ const restorePairSlot = (r: Rule) => {
1012
+ const slot: any = r.u.aontu_pair_slot
1013
+ if (null == slot) {
1014
+ return
1015
+ }
1016
+ const node = asSlots(r)
1017
+ if (slot.had) {
1018
+ node[slot.key] = slot.was
1019
+ }
1020
+ else {
1021
+ delete node[slot.key]
1022
+ }
1023
+ node.length = slot.len
1024
+ }
1025
+
1026
+
868
1027
  jsonic.rule('pair', (rs: RuleSpec) => {
869
1028
  rs
870
1029
  .open([
@@ -955,11 +1114,31 @@ help isolate the syntax error.`,
955
1114
  a: (r) => {
956
1115
  pairkey(r.prev)
957
1116
  r.u.key = r.prev.u.key
958
-
959
- r.parent.u.aontu_optional_keys = (r.parent.u.aontu_optional_keys || [])
960
- r.parent.u.aontu_optional_keys.push('' + r.u.key)
1117
+ snapshotPairSlot(r, '' + r.u.key)
961
1118
  },
962
1119
  g: 'aontu-optional-elem'
1120
+ },
1121
+
1122
+ // A PLAIN pair in list position, `[k:v]`. It contributes no
1123
+ // element either -- a key:value pair is simply not a list element,
1124
+ // which is the rule the optional form above already followed, and
1125
+ // the two spellings must not disagree (issue #40).
1126
+ //
1127
+ // It needed an alt of its own because only a NON-NUMERIC key was
1128
+ // already inert: jsonic writes the pair at `node[key]`, and the
1129
+ // node is an array, so `[x:1]` set a property that never showed up
1130
+ // (`length` stays 0) while `[0:1]` set an INDEX and became an
1131
+ // element -- `[1:2]` even filling the gap with a null. That is the
1132
+ // shape of a JavaScript array, not a decision about the language,
1133
+ // and it made the two ports disagree on generate as well as canon.
1134
+ {
1135
+ s: [OPTKEY, CL], p: 'val',
1136
+ u: { spread: true, done: true, list: true, pair: true },
1137
+ a: (r) => {
1138
+ pairkey(r)
1139
+ snapshotPairSlot(r, '' + r.u.key)
1140
+ },
1141
+ g: 'aontu-plain-pair-elem'
963
1142
  }
964
1143
  ])
965
1144
 
@@ -973,6 +1152,8 @@ help isolate the syntax error.`,
973
1152
  rule.node[SPREAD].v.push(rule.child.node)
974
1153
  }
975
1154
 
1155
+ restorePairSlot(rule)
1156
+
976
1157
  return undefined
977
1158
  })
978
1159
 
@@ -1057,6 +1238,73 @@ function makeModelResolver(options: any) {
1057
1238
  }
1058
1239
 
1059
1240
 
1241
+ // funcArity is the permitted WRITTEN argument count of each built-in, as
1242
+ // [min, max]; a max of -1 is unbounded. Every name in funcMap has an
1243
+ // entry, and the arity is a property of the language rather than of
1244
+ // either port -- go/func.go carries the same table.
1245
+ //
1246
+ // Nearly everything takes exactly one. The four exceptions earn their
1247
+ // place: key() names how many levels UP the path to read, defaulting to
1248
+ // the parent when omitted, neq takes a whole set of exclusions,
1249
+ // unique() is a property of the container rather than a comparison
1250
+ // against anything, so there is nothing for it to take, and must()
1251
+ // takes a check AND the author's message for when it fails.
1252
+ const funcArity: Record<string, [number, number]> = {
1253
+ upper: [1, 1], lower: [1, 1], copy: [1, 1], pref: [1, 1],
1254
+ super: [1, 1], type: [1, 1], hide: [1, 1], close: [1, 1],
1255
+ open: [1, 1], move: [1, 1], path: [1, 1],
1256
+ min: [1, 1], max: [1, 1], above: [1, 1], below: [1, 1], re: [1, 1],
1257
+ length: [1, 1],
1258
+ key: [0, 1],
1259
+ unique: [0, 0],
1260
+ neq: [1, -1],
1261
+ must: [2, 2],
1262
+ }
1263
+
1264
+
1265
+ // writtenArgCount counts the arguments as the AUTHOR wrote them.
1266
+ //
1267
+ // It cannot simply be terms.length: a comma group reaches the func-paren
1268
+ // handler as ONE term holding a raw array, so `upper("a","b")` and
1269
+ // `upper(["a","b"])` both arrive as a single argument. They are still
1270
+ // distinguishable, and that is what makes an arity check possible at
1271
+ // all -- the comma group is a RAW array, while a written list literal
1272
+ // has already been built into a ListVal by the list rule.
1273
+ function writtenArgCount(terms: any[]): number {
1274
+ if (1 === terms.length) {
1275
+ // `terms[0]` is re-read rather than reusing a narrowed local:
1276
+ // Array.isArray narrows an `any` to `any[]`, which then has no
1277
+ // `isVal` to test.
1278
+ const t: any = terms[0]
1279
+ if (Array.isArray(t) && true !== (terms[0] as any).isVal) {
1280
+ return t.length
1281
+ }
1282
+ }
1283
+ return terms.length
1284
+ }
1285
+
1286
+
1287
+ // arityText renders a built-in's permitted count for the error message.
1288
+ // The fixed-arity case says "one" outright rather than counting: every
1289
+ // fixed arity in the table IS one, and a phrasing for a count no entry
1290
+ // carries would be untested prose pretending to be tested.
1291
+ function arityText(lo: number, hi: number): string {
1292
+ if (-1 === hi) {
1293
+ return 'one or more arguments'
1294
+ }
1295
+ if (lo !== hi) {
1296
+ return 'no arguments or one'
1297
+ }
1298
+ if (0 === hi) {
1299
+ return 'no arguments'
1300
+ }
1301
+ if (2 === hi) {
1302
+ return 'exactly two arguments'
1303
+ }
1304
+ return 'exactly one argument'
1305
+ }
1306
+
1307
+
1060
1308
  // rawToVal converts a raw parse node (or raw elements inside one) into
1061
1309
  // the matching Val. Used for implicit top-level lists, whose nodes skip
1062
1310
  // the aontu val rule conversions (mirrors asVal in go/lang.go; like
@@ -1115,12 +1363,6 @@ function rawToVal(n: any): Val {
1115
1363
  return new BooleanVal({ peg: n })
1116
1364
  }
1117
1365
  if ('object' === t) {
1118
- // An expr-plugin internal (operator descriptor) leaking through a
1119
- // degenerate parse (`k2.b K:1`) is not data — reject it rather
1120
- // than emitting raw internals in generated output.
1121
- if (undefined !== (n as any).OP_MARK) {
1122
- return new NilVal({ why: 'parse_unknown' })
1123
- }
1124
1366
  const peg: Record<string, Val> = {}
1125
1367
  for (const k in n) {
1126
1368
  peg[k] = rawToVal(n[k])
package/src/lsp.ts CHANGED
@@ -358,11 +358,12 @@ type CompletionItem = {
358
358
  const COMPLETION_FUNCTION = 3
359
359
  const COMPLETION_KEYWORD = 14
360
360
 
361
- // The seventeen built-in functions. Kept in sync with the engine by
361
+ // The twenty-one built-in functions. Kept in sync with the engine by
362
362
  // `lsp.test.ts`, which asserts each is recognised and no others are.
363
363
  const BUILTIN_FUNCS = [
364
- 'above', 'below', 'close', 'copy', 'hide', 'key', 'lower', 'max',
365
- 'min', 'move', 'neq', 'open', 'path', 'pref', 'super', 'type', 'upper',
364
+ 'above', 'below', 'close', 'copy', 'hide', 'key', 'length', 'lower',
365
+ 'max', 'min', 'move', 'must', 'neq', 'open', 'path', 'pref', 're',
366
+ 'super', 'type', 'unique', 'upper',
366
367
  ]
367
368
 
368
369
  // Scalar-kind and literal keywords.
package/src/unify.ts CHANGED
@@ -26,9 +26,52 @@ import {
26
26
  } from './val/top'
27
27
 
28
28
 
29
- // TODO: FIX: false positive when too many top unifications
29
+ // Per-pass revisit bound: how many times one (Val, path) pair may be
30
+ // re-unified within a single fixpoint pass before the evaluator calls it
31
+ // non-convergence (`unify_cycle`). The old false positive here -- a
32
+ // legal model with more than MAXCYCLE sibling conjunct terms at one
33
+ // path, each re-running the TOP self-unify -- is fixed by the per-pass
34
+ // memo below (_tcc/_tpi); test/spec/budget.tsv drives 1200 sibling
35
+ // terms through both engines as the regression guard.
30
36
  const MAXCYCLE = 999
31
37
 
38
+ // Structural recursion budget: how deep `unite` may nest before the
39
+ // evaluator reports `unify_cycle`. SHARED LANGUAGE SURFACE -- Go's
40
+ // maxUniteDepth (go/unify.go) carries the same number, and
41
+ // test/spec/budget.tsv pins the boundary in both, so changing it is a
42
+ // spec-visible change in both ports at once.
43
+ //
44
+ // Why 1000: the whole shared suite peaks at 603 (the deliberately
45
+ // extreme 1200-sibling-term fixture; ordinary documents are two orders
46
+ // below), and V8 exhausts its call stack somewhere past depth ~1500 in
47
+ // this evaluator. 1000 sits above every real document and below the
48
+ // host limit, so the budget -- not the host -- decides the verdict.
49
+ const MAXDEPTH = 1000
50
+
51
+ // Charge a DIRECT `Val.unify` recursion to the same depth budget that
52
+ // `unite` enforces. Function and operator arguments evaluate through
53
+ // `arg.unify(top(), ...)` rather than through the dispatcher, so without
54
+ // this the counter stays flat while the JavaScript stack keeps growing:
55
+ // a 1500-deep `upper(upper(...))` resolved in TypeScript while Go — which
56
+ // routes its arguments through the counted dispatcher — reported
57
+ // `unify_cycle`. Returns the budget nil instead of running when the
58
+ // budget is spent.
59
+ const withDepth = (
60
+ ctx: AontuContext, a: any, b: any, run: () => any
61
+ ): any => {
62
+ if (MAXDEPTH <= ctx._depth.n) {
63
+ return makeNilErr(ctx, 'unify_cycle', a, b)
64
+ }
65
+ ctx._depth.n++
66
+ try {
67
+ return run()
68
+ }
69
+ finally {
70
+ ctx._depth.n--
71
+ }
72
+ }
73
+
74
+
32
75
  // Vals should only have to unify downwards (in .unify) over Vals they understand.
33
76
  // and for complex Vals, TOP, which means self unify if not yet done
34
77
  const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
@@ -76,12 +119,22 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
76
119
  // NOTE: if this error occurs "unreasonably", attemp to avoid unnecesary unification
77
120
  // See for example PrefVal peg.id equality inspection.
78
121
  const sawCount = ctx.seen[saw] ?? 0
79
- if (MAXCYCLE < sawCount) {
122
+ if (MAXDEPTH <= ctx._depth.n) {
123
+ // Structural recursion budget. Without it, deep nesting exhausts the
124
+ // V8 call stack and the catch-all below reports a RangeError as
125
+ // `internal` — a verdict that depends on the host's stack size
126
+ // rather than on the document, which is exactly what the
127
+ // determinism clause forbids (docs/trust.md). Tripping here instead
128
+ // makes it a stated budget error, like the pass budget.
129
+ out = makeNilErr(ctx, 'unify_cycle', a, b)
130
+ }
131
+ else if (MAXCYCLE < sawCount) {
80
132
  // console.log('SAW', sawCount, saw, a?.id, a?.canon, b?.id, b?.canon, ctx.cc)
81
133
  out = makeNilErr(ctx, 'unify_cycle', a, b)
82
134
  }
83
135
  else {
84
136
  ctx.seen[saw] = sawCount + 1
137
+ ctx._depth.n++
85
138
 
86
139
  try {
87
140
  let unified = false
@@ -191,6 +244,9 @@ const unite = (ctx: AontuContext, a: any, b: any, whence: string) => {
191
244
  ...(err instanceof RangeError ? { overflow: true } : {}),
192
245
  })
193
246
  }
247
+ finally {
248
+ ctx._depth.n--
249
+ }
194
250
  }
195
251
 
196
252
  ctx.explain && explainClose(te, out)
@@ -341,4 +397,5 @@ class Unify {
341
397
  export {
342
398
  Unify,
343
399
  unite,
400
+ withDepth,
344
401
  }