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
@@ -49,8 +49,21 @@ import {
49
49
  explainOpen,
50
50
  explainClose,
51
51
  propagateMarks,
52
+ items,
52
53
  } from '../utility'
53
54
 
55
+ import { empty } from './Val'
56
+
57
+ import { cmpCodePoint } from '../keyorder'
58
+
59
+ import { ConjunctVal } from './ConjunctVal'
60
+
61
+ import { top } from './top'
62
+
63
+ import { unite, withDepth } from '../unify'
64
+
65
+ import { IntegerVal } from './IntegerVal'
66
+
54
67
  import { makeNilErr } from '../err'
55
68
 
56
69
  import { FeatureVal } from './FeatureVal'
@@ -77,16 +90,322 @@ type Bound = {
77
90
  open: boolean // true for above/below, false for min/max
78
91
  }
79
92
 
93
+ type ReAtom = {
94
+ v: any // the stored pattern StringVal (canon renders the literal)
95
+ src: string // the pattern text AS WRITTEN — canon and dedup use this,
96
+ // never the normalised form, because canon round-trips
97
+ // source and G6's hash will be taken over canon
98
+ norm: string // the engine-neutral rewrite actually compiled (ADR-003)
99
+ re: RegExp // compiled by the host engine, from `norm`
100
+ }
101
+
102
+ type MustAtom = {
103
+ v: any // the value the peer must unify with (any Aontu value)
104
+ msg: any // the author's message StringVal (canon renders the literal)
105
+ }
106
+
80
107
  type ConstraintState = {
81
108
  domain?: 'number' | 'string'
82
109
  kind?: any // numeric leaf marker (Integer | Float | ...) or undefined
83
110
  lo?: Bound
84
111
  hi?: Bound
85
112
  neqs: any[] // excluded scalars, identity per leaf+value
113
+ res: ReAtom[] // accumulated patterns, sorted by source (never simplified)
114
+ count?: ConstraintState // the COUNT residual (length()), itself a residual
115
+ // over the integer domain -- the count atom reuses
116
+ // this same algebra recursively
117
+ uniq: boolean // members must be pairwise distinct (unique())
118
+ musts: MustAtom[] // Band B checks, kept in written order, never simplified
119
+ clash?: boolean // a kind disagreement inside a length() argument, recorded
120
+ // rather than raised: the argument's own meet has no
121
+ // ctx to report through, so emptiness carries the news
86
122
  invalid?: string // why-code when the atom's arguments were unusable
87
123
  }
88
124
 
89
125
 
126
+ // THE PATTERN SUBSET, AND HOW IT IS ENFORCED (G1 phase 2; ADR-003).
127
+ //
128
+ // `re(p)` must mean the same thing in both engines and cost about the
129
+ // same, and the two host engines guarantee neither: TypeScript compiles
130
+ // with JavaScript's backtracking RegExp, Go with RE2 — a different
131
+ // language in a different complexity class, over a different alphabet.
132
+ //
133
+ // The enforcement is NORMALISATION, not refusal (ADR-003). Every
134
+ // construct whose expansion is engine-defined is rewritten here, by
135
+ // this function, into an explicit form that cannot be read two ways;
136
+ // only the rewritten pattern reaches a host engine. The alternative —
137
+ // refusing everything that might differ — was tried first and leaked
138
+ // three times, because its correctness depended on this comment knowing
139
+ // every difference between two large engines.
140
+ //
141
+ // Aontu therefore DEFINES the abbreviations rather than inheriting
142
+ // either host's. The definitions are deliberately the small ASCII ones,
143
+ // because a config value containing U+00A0 is a mistake to catch, not a
144
+ // space to accept silently:
145
+ //
146
+ // \d [0-9] \D [^0-9]
147
+ // \w [0-9A-Za-z_] \W [^0-9A-Za-z_]
148
+ // \s [ \t\n\r\f\v] \S [^ \t\n\r\f\v]
149
+ // . [^\n]
150
+ // \A ^ \z $
151
+ //
152
+ // Neither host agreed with all of these before rewriting: JavaScript's
153
+ // \s also matches U+00A0 and other Unicode spaces, RE2's omits \v, and
154
+ // the two `.` sets differ by \r and the Unicode line separators. After
155
+ // rewriting, both engines see one explicit class and cannot disagree.
156
+ //
157
+ // What is still REFUSED, and why refusal is right for these:
158
+ //
159
+ // 1. Constructs one engine simply lacks — backreferences and
160
+ // lookaround (not in RE2, and not expressible as regular
161
+ // expressions at all), POSIX classes, `\p{...}`, `\x{...}`, `\u`.
162
+ // There is nothing to normalise them TO.
163
+ // 2. `(?` other than `(?:` — named groups are spelled differently
164
+ // (`(?P<n>` in RE2, `(?<n>` in JavaScript) and inline flags change
165
+ // the meaning of everything after them.
166
+ // 3. A quantifier applied to a group containing a quantifier or an
167
+ // alternation. This one is about TIME, not meaning: `(a+)+$`
168
+ // against twenty-nine characters takes 45 SECONDS in JavaScript
169
+ // and 0.065s under RE2, and a regex match is counted by no
170
+ // evaluator budget, so an untrusted schema could otherwise stall
171
+ // the TypeScript evaluator indefinitely (docs/trust.md clause 2).
172
+ // Normalisation cannot fix a complexity difference; only owning the
173
+ // matcher could, which ADR-003 records as the future option.
174
+ //
175
+ // The alphabet is fixed separately, by compiling with the `u` flag:
176
+ // JavaScript otherwise matches UTF-16 code units where RE2 matches code
177
+ // points, so `^.$` accepted U+1D11E in Go and refused it in TypeScript.
178
+ //
179
+ // This function is mirrored statement for statement in go/constraint.go,
180
+ // and `test/spec/files/regex-corpus.txt` pins that the two produce
181
+ // byte-identical output for every pattern in a generated corpus.
182
+
183
+ // The normative expansions. These are Aontu's definitions, not either
184
+ // host's; both hosts are rewritten to them.
185
+ const RE_CLASS_DIGIT = '0-9'
186
+ const RE_CLASS_WORD = '0-9A-Za-z_'
187
+ const RE_CLASS_SPACE = ' \\t\\n\\r\\f\\v'
188
+
189
+ // Metacharacters that may be escaped to mean themselves, in both
190
+ // engines. `-` is handled separately: it is legal escaped only INSIDE a
191
+ // character class, because RE2 accepts `a\-b` and JavaScript's unicode
192
+ // mode makes it a syntax error.
193
+ const RE_ESCAPE_PUNCT = '\\.+*?()[]{}|^$/'
194
+
195
+ // Escapes passed through unchanged: the control characters, the ASCII
196
+ // word boundary, and `\xHH`. Each was probed in both engines.
197
+ const RE_ESCAPE_PASS = 'tnrfv'
198
+
199
+ function isHexDigit(c: string | undefined): boolean {
200
+ return null != c && (
201
+ ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F'))
202
+ }
203
+
204
+
205
+ // normaliseEscape rewrites one `\<n>` into its engine-neutral form.
206
+ // Returns [emitted, why, extra]: `why` non-empty means refused, and
207
+ // `extra` counts source characters consumed beyond the backslash and n.
208
+ function normaliseEscape(
209
+ n: string | undefined, src: string, i: number, inClass: boolean
210
+ ): [string, string, number] {
211
+ if (null == n) {
212
+ return ['', 'a trailing backslash', 0]
213
+ }
214
+ if ('1' <= n && n <= '9') {
215
+ return ['', 'a backreference (\\' + n + '): RE2 has no equivalent, and a' +
216
+ ' pattern with one is not a regular expression', 0]
217
+ }
218
+ if ('k' === n) {
219
+ return ['', 'a named backreference (\\k): RE2 has no equivalent', 0]
220
+ }
221
+ if ('u' === n) {
222
+ return ['', 'a \\u escape, which RE2 spells \\x{...}: write the character' +
223
+ ' itself, or \\xHH for a byte', 0]
224
+ }
225
+ if ('p' === n || 'P' === n) {
226
+ return ['', 'a Unicode class (\\' + n + '), which JavaScript reads as a' +
227
+ ' literal "' + n + '" without a flag Aontu does not set', 0]
228
+ }
229
+ if ('Z' === n) {
230
+ return ['', '\\Z, which RE2 does not accept and JavaScript reads as a' +
231
+ ' literal "Z": write $ for end of text', 0]
232
+ }
233
+ if ('x' === n) {
234
+ if ('{' === src[i + 2]) {
235
+ return ['', 'a \\x{...} escape, which JavaScript spells \\u: write the' +
236
+ ' character itself', 0]
237
+ }
238
+ if (!isHexDigit(src[i + 2]) || !isHexDigit(src[i + 3])) {
239
+ return ['', 'an \\x escape without two hex digits', 0]
240
+ }
241
+ return ['\\x' + src[i + 2] + src[i + 3], '', 2]
242
+ }
243
+
244
+ // The abbreviations, rewritten to Aontu's definitions. Inside a class
245
+ // the expansion splices without its brackets (`[\dx]` -> `[0-9x]`).
246
+ if ('d' === n || 'w' === n || 's' === n) {
247
+ const set = 'd' === n ? RE_CLASS_DIGIT :
248
+ 'w' === n ? RE_CLASS_WORD : RE_CLASS_SPACE
249
+ return [inClass ? set : '[' + set + ']', '', 0]
250
+ }
251
+ if ('D' === n || 'W' === n || 'S' === n) {
252
+ if (inClass) {
253
+ // `[^...]` cannot be spliced into an enclosing class: the negation
254
+ // would apply to the whole class rather than this member.
255
+ return ['', 'a negated abbreviation (\\' + n + ') inside a character' +
256
+ ' class, which cannot be expanded in place: write the characters out', 0]
257
+ }
258
+ const set = 'D' === n ? RE_CLASS_DIGIT :
259
+ 'W' === n ? RE_CLASS_WORD : RE_CLASS_SPACE
260
+ return ['[^' + set + ']', '', 0]
261
+ }
262
+
263
+ // Anchors. `\A`/`\z` are RE2 spellings that JavaScript reads as
264
+ // literals, so they are rewritten rather than refused. Inside a class
265
+ // an anchor is meaningless, and `[\b]` is a BACKSPACE in JavaScript.
266
+ if ('A' === n || 'z' === n || 'b' === n || 'B' === n) {
267
+ if (inClass) {
268
+ return ['', '\\' + n + ' inside a character class, where the two' +
269
+ ' engines do not agree what it means', 0]
270
+ }
271
+ return ['A' === n ? '^' : 'z' === n ? '$' : '\\' + n, '', 0]
272
+ }
273
+
274
+ if ('-' === n) {
275
+ return inClass ? ['\\-', '', 0] :
276
+ ['', '\\- outside a character class: it is a range separator inside' +
277
+ ' one and a syntax error outside one (write a bare -)', 0]
278
+ }
279
+ if (RE_ESCAPE_PASS.includes(n) || RE_ESCAPE_PUNCT.includes(n)) {
280
+ return ['\\' + n, '', 0]
281
+ }
282
+ return ['', '\\' + n + ', an escape whose meaning the two engines do not' +
283
+ ' share', 0]
284
+ }
285
+
286
+
287
+ // normaliseRe rewrites a pattern into the engine-neutral subset.
288
+ // Returns [normalised, why]: a non-empty `why` means the pattern is
289
+ // outside the subset and names the construct.
290
+ function normaliseRe(src: string): [string, string] {
291
+ let inClass = false
292
+ const out: string[] = []
293
+
294
+ // One frame per open group, recording whether it contains a quantifier
295
+ // or an alternation. A group carrying either may not itself be
296
+ // quantified; containment is transitive, so a frame hands its flags up
297
+ // to its parent when it closes.
298
+ const groups: { q: boolean, alt: boolean }[] = []
299
+ const mark = (k: 'q' | 'alt') => {
300
+ if (0 < groups.length) {
301
+ groups[groups.length - 1][k] = true
302
+ }
303
+ }
304
+
305
+ for (let i = 0; i < src.length; i++) {
306
+ const c = src[i]
307
+
308
+ if ('\\' === c) {
309
+ const [emit, why, extra] = normaliseEscape(src[i + 1], src, i, inClass)
310
+ if ('' !== why) {
311
+ return ['', why]
312
+ }
313
+ out.push(emit)
314
+ i += 1 + extra
315
+ continue
316
+ }
317
+
318
+ // A POSIX class opener, anywhere: the form lives inside an ordinary
319
+ // class (`[[:alpha:]]`), and refusing it everywhere is one rule
320
+ // rather than two.
321
+ if ('[' === c && ':' === src[i + 1]) {
322
+ return ['', 'a POSIX class ([:...:]), which JavaScript does not have']
323
+ }
324
+
325
+ if (inClass) {
326
+ if (']' === c) {
327
+ inClass = false
328
+ }
329
+ out.push(c)
330
+ continue
331
+ }
332
+
333
+ if ('[' === c) {
334
+ // `[]` is a never-matching class in JavaScript and a parse error in
335
+ // RE2; `[^]` is the same disagreement one character along.
336
+ const first = '^' === src[i + 1] ? src[i + 2] : src[i + 1]
337
+ if (']' === first) {
338
+ return ['', 'an empty character class, which RE2 refuses']
339
+ }
340
+ inClass = true
341
+ out.push(c)
342
+ continue
343
+ }
344
+
345
+ if ('.' === c) {
346
+ out.push('[^\\n]')
347
+ continue
348
+ }
349
+
350
+ if ('(' === c) {
351
+ if ('?' === src[i + 1]) {
352
+ if (':' !== src[i + 2]) {
353
+ return ['', 'a (?...) group other than the non-capturing (?:']
354
+ }
355
+ out.push('(?:')
356
+ i += 2
357
+ }
358
+ else {
359
+ out.push(c)
360
+ }
361
+ groups.push({ q: false, alt: false })
362
+ continue
363
+ }
364
+
365
+ if (')' === c) {
366
+ const g = groups.pop()
367
+ if (null == g) {
368
+ return ['', 'an unbalanced group']
369
+ }
370
+ const nx = src[i + 1]
371
+ const quantified = '*' === nx || '+' === nx || '?' === nx || '{' === nx
372
+ if (quantified && (g.q || g.alt)) {
373
+ return ['', 'a quantifier applied to a group containing ' +
374
+ (g.q ? 'another quantifier' : 'an alternation') +
375
+ ', which backtracks exponentially in JavaScript']
376
+ }
377
+ if (g.q) mark('q')
378
+ if (g.alt) mark('alt')
379
+ out.push(c)
380
+ continue
381
+ }
382
+
383
+ if ('|' === c) {
384
+ mark('alt')
385
+ out.push(c)
386
+ continue
387
+ }
388
+
389
+ if ('*' === c || '+' === c || '?' === c || '{' === c) {
390
+ mark('q')
391
+ out.push(c)
392
+ continue
393
+ }
394
+
395
+ out.push(c)
396
+ }
397
+
398
+ if (inClass) {
399
+ return ['', 'an unterminated character class']
400
+ }
401
+ if (0 < groups.length) {
402
+ return ['', 'an unclosed group']
403
+ }
404
+
405
+ return [out.join(''), '']
406
+ }
407
+
408
+
90
409
  // True for a scalar Val the algebra can order: a numeric leaf or a
91
410
  // string. (Booleans and null have no order and no bounds.)
92
411
  function numericLeaf(v: any): boolean {
@@ -128,6 +447,27 @@ function leafMarker(v: any): any {
128
447
  }
129
448
 
130
449
 
450
+ // Conjunct sort order for an atom that must see the WHOLE value.
451
+ // Every other value sorts below the container default (99999), so such
452
+ // an atom is the LAST term to fold: `a:length(2) a:{x:1} a:{y:2}` must
453
+ // count the MERGED map, and a constraint that folded at 50000 would
454
+ // count `{x:1}` alone and refuse the layering that is the whole point
455
+ // of the language.
456
+ //
457
+ // The order atoms keep the low slot: `min(2) & 1 & 2` may decide as
458
+ // soon as it sees a scalar, because meeting more scalars can only
459
+ // narrow. Meeting more containers GROWS the member set, which is why
460
+ // the two orders differ.
461
+ //
462
+ // Three atoms are late: `length` and `unique` (they count members), and
463
+ // `must` (an evaluate-only check against the finished value).
464
+ const LATE_CJO = 150000
465
+
466
+ function lateAtom(atom: string): boolean {
467
+ return 'length' === atom || 'unique' === atom || 'must' === atom
468
+ }
469
+
470
+
131
471
  class ConstraintVal extends FeatureVal {
132
472
  isConstraint = true
133
473
  cjo = 50000
@@ -137,7 +477,17 @@ class ConstraintVal extends FeatureVal {
137
477
  lo?: Bound
138
478
  hi?: Bound
139
479
  neqs: any[] = []
480
+ res: ReAtom[] = []
481
+ count?: ConstraintState
482
+ uniq = false
483
+ musts: MustAtom[] = []
484
+ // An atom whose arguments have not settled yet (G1 phase 4). Held
485
+ // until unify has a ctx to resolve them through; never present on a
486
+ // residual.
487
+ pending?: { atom: string, args: any[] }
488
+ clash?: boolean
140
489
  invalid?: string
490
+ invalidWhy?: string
141
491
 
142
492
  constructor(
143
493
  spec: ValSpec & { atom?: string, state?: ConstraintState },
@@ -151,20 +501,51 @@ class ConstraintVal extends FeatureVal {
151
501
  this.lo = spec.state.lo
152
502
  this.hi = spec.state.hi
153
503
  this.neqs = spec.state.neqs
504
+ // A state built by an embedder (or by a per-port test) may predate
505
+ // the pattern field; an absent one means "no patterns", not undefined.
506
+ this.res = spec.state.res ?? []
507
+ this.count = spec.state.count
508
+ this.uniq = spec.state.uniq ?? false
509
+ this.musts = spec.state.musts ?? []
154
510
  this.invalid = spec.state.invalid
155
511
  }
156
512
  else if (spec.atom) {
157
- this.fromAtom(spec.atom, (spec.peg as any[]) ?? [])
513
+ const args = atomArgs(spec.atom, (spec.peg as any[]) ?? [])
514
+ // An argument that is not yet concrete — a reference, an
515
+ // arithmetic expression, a conjunct of atoms — makes the whole
516
+ // atom PENDING rather than invalid (G1 phase 4). It is resolved
517
+ // in unify, where there is a ctx to resolve through, and the
518
+ // residual is built from the settled arguments. Only a settled
519
+ // argument of the wrong shape is an `invalid-arg`.
520
+ if (args.some((a: any) => true !== a?.done)) {
521
+ this.pending = { atom: spec.atom, args }
522
+ }
523
+ else {
524
+ this.fromAtom(spec.atom, args)
525
+ }
158
526
  }
159
527
 
160
- // A residual constraint is stable, like a ScalarKindVal.
161
- this.dc = DONE
528
+ if (null != this.count || this.uniq || 0 < this.musts.length ||
529
+ (null != this.pending && lateAtom(this.pending.atom))) {
530
+ this.cjo = LATE_CJO
531
+ }
532
+
533
+ // A residual constraint is stable, like a ScalarKindVal — but a
534
+ // pending atom is not a residual yet, and must be re-entered on
535
+ // later passes until its arguments settle.
536
+ if (null == this.pending) {
537
+ this.dc = DONE
538
+ }
539
+ else {
540
+ this.notdone()
541
+ }
162
542
  }
163
543
 
164
544
 
165
- // Normalise one atom call (min/max/above/below/neq) into state.
166
- // Arguments must be concrete orderable scalars in phase 1;
167
- // reference-valued arguments are phase 4 (residuation).
545
+ // Normalise one atom call into state. Every argument here is already
546
+ // SETTLED — the constructor routes an unsettled one to `pending` —
547
+ // so a shape this cannot use is a genuine `invalid-arg`, not a
548
+ // not-yet.
168
549
  private fromAtom(atom: string, args: any[]) {
169
550
  // Mark the residual invalid and report so (a plain boolean, so no
170
551
  // void value is consumed by the callers' `return` statements).
@@ -173,17 +554,40 @@ class ConstraintVal extends FeatureVal {
173
554
  return true
174
555
  }
175
556
 
176
- if ('neq' === atom) {
177
- // Multiple arguments arrive from the func-paren grammar as one
178
- // entry holding the comma group: a raw array of Vals (or an
179
- // implicit ListVal via some spellings). `neq(3,1,2)` therefore
180
- // has peg [[3,1,2]], and `neq([3,1,2])` means the same thing.
181
- if (1 === args.length && Array.isArray(args[0])) {
182
- args = args[0]
557
+ // `unique()` takes no argument: it is a property of the container,
558
+ // not a comparison against a value. Arity is checked at parse, so a
559
+ // written argument never reaches here.
560
+ if ('unique' === atom) {
561
+ this.uniq = true
562
+ return
563
+ }
564
+
565
+ // `must(c, msg)` is Band B: an evaluate-only check against the
566
+ // finished value, carrying the author's own message. It is KEPT,
567
+ // never simplified and never consulted for emptiness or
568
+ // subsumption — Band B is opaque by construction, which is exactly
569
+ // what makes it the honest channel for a rule the algebra cannot
570
+ // reason about (docs/reference-language.md, "Band B: `must`").
571
+ if ('must' === atom) {
572
+ if (2 !== args.length) {
573
+ return bad('arg')
574
+ }
575
+ if (!stringLeaf(args[1])) {
576
+ return bad('invalid-arg')
183
577
  }
184
- else if (1 === args.length && true === (args[0] as any)?.isList) {
185
- args = (args[0] as any).peg
578
+ // A check carrying a nil can never be satisfied, so it is refused
579
+ // as an ARGUMENT rather than left to fail against every value with
580
+ // the author's message attached -- which would blame the data for
581
+ // a mistake in the check. `must([1-x],m)` is the reachable case: a
582
+ // degenerate expression leaves a nil inside the written list.
583
+ if (holdsNil(args[0])) {
584
+ return bad('invalid-arg')
186
585
  }
586
+ this.musts = [{ v: args[0], msg: args[1] }]
587
+ return
588
+ }
589
+
590
+ if ('neq' === atom) {
187
591
  if (0 === args.length) {
188
592
  return bad('arg')
189
593
  }
@@ -204,6 +608,74 @@ class ConstraintVal extends FeatureVal {
204
608
  return bad('arg')
205
609
  }
206
610
  const a = args[0]
611
+
612
+ // `re` is the one atom whose argument is not an ORDER point: a
613
+ // pattern is a membership test, so it takes the string domain
614
+ // outright rather than inferring a domain from the argument's leaf.
615
+ if ('re' === atom) {
616
+ if (!stringLeaf(a)) {
617
+ return bad('invalid-arg')
618
+ }
619
+ const src = a.peg as string
620
+ // Normalise BEFORE compiling: the host engines only ever see a
621
+ // pattern that cannot be read two ways (ADR-003).
622
+ const [norm, why] = normaliseRe(src)
623
+ if ('' !== why) {
624
+ this.invalidWhy = why
625
+ return bad('constraint_pattern')
626
+ }
627
+ let re: RegExp
628
+ try {
629
+ // The `u` flag is REQUIRED for parity, not an optimisation.
630
+ // Without it JavaScript matches UTF-16 code units while RE2
631
+ // matches code points, so `re("^.$")` accepted U+1D11E in Go
632
+ // and refused it in TypeScript (and `^..$` did the reverse).
633
+ // With it, `.` and every quantifier count code points in both.
634
+ // It also makes JavaScript refuse the identity escapes this
635
+ // scanner rejects by hand, which is defence in depth rather
636
+ // than a substitute: RE2 accepts some of them, so the scanner
637
+ // is what keeps the two ports agreeing.
638
+ re = new RegExp(norm, 'u')
639
+ }
640
+ catch (e: any) {
641
+ // The host engine refuses what the subset scanner passed — a
642
+ // malformed quantifier, an unbalanced group. Same refusal under
643
+ // the same code: the author gets one rule, not two. The message
644
+ // is the host's, so it is NOT pinned by a shared row; the code
645
+ // and the located frame are.
646
+ this.invalidWhy = 'not a valid pattern'
647
+ return bad('constraint_pattern')
648
+ }
649
+ this.domain = 'string'
650
+ this.res = [{ v: a, src, norm, re }]
651
+ return
652
+ }
653
+
654
+ // `length` is the other non-ORDER atom: its argument constrains the
655
+ // COUNT, not the value, so it is itself a residual over the integer
656
+ // domain (docs/reference-language.md, "`length` semantics"). It is
657
+ // resolved HERE, at construction, by walking the written argument
658
+ // rather than by unifying it: the func-paren handler builds atoms
659
+ // without an AontuContext, so there is nothing to fold a nested
660
+ // conjunct through. Walking is enough because a length argument is by
661
+ // definition a meet of concrete Band A atoms; anything else (a
662
+ // reference, an expression) is refused rather than deferred, the
663
+ // same discipline min/max apply to their own arguments.
664
+ if ('length' === atom) {
665
+ const arg = countArgState(a)
666
+ if (null == arg) {
667
+ return bad('invalid-arg')
668
+ }
669
+ const inner = meetCount(countBase(), arg)
670
+ this.count = inner
671
+ // `length(min(5)&max(3))` is unsatisfiable with no peer in sight, so
672
+ // it is refused at composition time like any other empty meet.
673
+ if (stateEmpty(inner)) {
674
+ return bad('constraint')
675
+ }
676
+ return
677
+ }
678
+
207
679
  const domain = numericLeaf(a) ? 'number' : stringLeaf(a) ? 'string' : undefined
208
680
  if (null == domain) {
209
681
  return bad('invalid-arg')
@@ -228,8 +700,12 @@ class ConstraintVal extends FeatureVal {
228
700
  // residual is stable and the ladder is total.
229
701
  let out: Val
230
702
 
231
- if (null != this.invalid) {
232
- out = makeNilErr(ctx, this.invalid, this, undefined, 'constrain')
703
+ if (null != this.pending) {
704
+ out = this.settle(peer, ctx)
705
+ }
706
+ else if (null != this.invalid) {
707
+ out = makeNilErr(ctx, this.invalid, this, undefined, 'constrain',
708
+ null == this.invalidWhy ? undefined : { reason: this.invalidWhy })
233
709
  }
234
710
  else if (null == peer || (peer as any).isTop) {
235
711
  out = this
@@ -246,9 +722,20 @@ class ConstraintVal extends FeatureVal {
246
722
  else if ((peer as any).isScalar) {
247
723
  out = this.admit(peer, ctx)
248
724
  }
725
+ else if ((peer as any).isMap || (peer as any).isList) {
726
+ out = this.admitContainer(peer, ctx)
727
+ }
728
+ /* node:coverage ignore next 12 */
249
729
  else {
250
- // Maps, lists, and every other non-scalar shape: no order, no
251
- // membership — a conflict of the constraint family.
730
+ // Every other shape: no order, no membership, nothing to count —
731
+ // a conflict of the constraint family. The ladder above is total
732
+ // in practice: every remaining Val kind either sorts BELOW a
733
+ // constraint in a conjunct (conjunct, disjunct, pref, ref) and so
734
+ // drives the meet from its own side, or resolves to a
735
+ // scalar/container before a constraint sees it (func, op, var,
736
+ // expect). The arm is kept because "in practice" depends on the
737
+ // cjo table, and a future value class would land here rather than
738
+ // falling out of unify with no result.
252
739
  out = this.fail(ctx, peer)
253
740
  }
254
741
 
@@ -258,61 +745,221 @@ class ConstraintVal extends FeatureVal {
258
745
  }
259
746
 
260
747
 
748
+ // Resolve a pending atom's arguments and, once they have all settled,
749
+ // become the residual they describe (G1 phase 4).
750
+ //
751
+ // This is the residuation discipline FuncBaseVal already follows for
752
+ // its own operands: push each unsettled argument one step by unifying
753
+ // it with `top`, and if any is still moving, mark not-done and defer
754
+ // — either as this same pending atom (against a `top` peer) or
755
+ // wrapped with the peer in a conjunct the next pass will re-enter.
756
+ // Nothing is decided from a half-resolved argument, which is what
757
+ // keeps `min($.lo)` from reporting a conflict against a bound that
758
+ // has not arrived yet.
759
+ private settle(peer: Val, ctx: AontuContext): Val {
760
+ const TOP = top()
761
+ const pend = this.pending as { atom: string, args: any[] }
762
+
763
+ let settled = true
764
+ const args: any[] = []
765
+ for (const arg of pend.args) {
766
+ let next = arg
767
+ if (true !== arg?.done) {
768
+ // Charged to the depth budget: this recurses without going
769
+ // through `unite`, so the counter would otherwise stay flat
770
+ // while the stack grows (the rule FuncBaseVal follows).
771
+ next = withDepth(ctx, arg, TOP, () => arg.unify(TOP, ctx))
772
+ }
773
+ settled = settled && true === next?.done
774
+ args.push(next)
775
+ }
776
+
777
+ if (settled) {
778
+ // Build the residual the atom always meant, at this atom's site,
779
+ // then let the ordinary ladder meet it with the peer.
780
+ const built = new ConstraintVal({ peg: args, atom: pend.atom }, ctx)
781
+ built.path = this.path
782
+ built.site.row = this.site.row
783
+ built.site.col = this.site.col
784
+ built.site.url = this.site.url
785
+ propagateMarks(this, built)
786
+ return built.unify(peer, ctx)
787
+ }
788
+
789
+ this.notdone()
790
+
791
+ // A fresh pending atom carrying the partially-resolved arguments, so
792
+ // the next pass starts from the progress this one made rather than
793
+ // re-resolving from source.
794
+ const again = new ConstraintVal({ peg: args, atom: pend.atom }, ctx)
795
+ again.path = this.path
796
+ again.site.row = this.site.row
797
+ again.site.col = this.site.col
798
+ again.site.url = this.site.url
799
+ propagateMarks(this, again)
800
+
801
+ if (null == peer || (peer as any).isTop) {
802
+ return again
803
+ }
804
+ if ((peer as any).isNil) {
805
+ return peer
806
+ }
807
+ return new ConjunctVal({ peg: [again, peer] }, ctx)
808
+ }
809
+
810
+
261
811
  // Membership: the peer scalar passes every part of the residual, or
262
812
  // the whole meet is a located conflict.
263
813
  private admit(peer: any, ctx: AontuContext): Val {
264
- const domainOf = numericLeaf(peer) ? 'number' :
265
- stringLeaf(peer) ? 'string' : undefined
266
-
267
- if (domainOf !== this.domain) {
814
+ // No scalar has members, so a `unique()` residual admits none.
815
+ if (this.uniq) {
268
816
  return this.fail(ctx, peer)
269
817
  }
270
- if (null != this.kind && leafMarker(peer) !== this.kind) {
818
+ if (!stateAdmits(this, peer)) {
271
819
  return this.fail(ctx, peer)
272
820
  }
273
- const d = this.domain as 'number' | 'string'
274
- if (null != this.lo) {
275
- const c = cmpVal(d, peer, this.lo.v)
276
- if (c < 0 || (0 === c && this.lo.open)) {
821
+ if (null != this.count) {
822
+ // Only a string among the scalars has a length, and it is counted
823
+ // in CODE POINTS -- not UTF-16 units (this host's native count)
824
+ // and not bytes (Go's). Iterating a string yields code points.
825
+ if (!stringLeaf(peer)) {
277
826
  return this.fail(ctx, peer)
278
827
  }
279
- }
280
- if (null != this.hi) {
281
- const c = cmpVal(d, peer, this.hi.v)
282
- if (c > 0 || (0 === c && this.hi.open)) {
828
+ if (!stateAdmits(this.count, countVal([...peer.peg].length))) {
283
829
  return this.fail(ctx, peer)
284
830
  }
285
831
  }
286
- for (const n of this.neqs) {
287
- if (sameScalar(peer, n)) {
288
- return this.fail(ctx, peer)
832
+ const bad = this.checkMusts(peer, ctx)
833
+ if (null != bad) {
834
+ return bad
835
+ }
836
+ return peer
837
+ }
838
+
839
+
840
+ // Band B, applied to a finished value. Each check is a plain
841
+ // unification against a CLONE of the peer: `must` reports, it never
842
+ // contributes: whatever the check would have added to the value is
843
+ // discarded, and `peer` is returned untouched by the callers.
844
+ //
845
+ // The trial runs in an isolated collect context so a failing check
846
+ // leaves nothing on the caller's error list — only the located nil
847
+ // this returns, carrying the author's own message.
848
+ private checkMusts(peer: any, ctx: AontuContext): Val | undefined {
849
+ for (const m of this.musts) {
850
+ const trial = ctx.clone({ err: [], collect: true })
851
+ const got = unite(trial, m.v.clone(trial), peer.clone(trial), 'must')
852
+ if (true === (got as any)?.isNil || 0 < trial.err.length) {
853
+ return makeNilErr(ctx, 'must', this, peer, undefined, {
854
+ message: m.msg.peg,
855
+ expected: m.v.canon,
856
+ actual: peer.canon,
857
+ })
858
+ }
859
+ }
860
+ return undefined
861
+ }
862
+
863
+
864
+ // Membership for a container peer. Only the SIZING atoms have anything
865
+ // to say about a map or a list; every other atom is scalar-domain and
866
+ // refuses one.
867
+ //
868
+ // The members that count are the members that GENERATE
869
+ // (docs/reference-language.md, "`length` semantics"), and rather than
870
+ // mirror generation's filter — type/hide marks, optional keys that
871
+ // drop, empty optional values — this asks generation itself, in an
872
+ // isolated collect context so nothing leaks into the caller's errors.
873
+ // A mirror would be a second copy of a filter that has already grown
874
+ // subtle, free to drift from it; asking is correct by construction.
875
+ private admitContainer(peer: any, ctx: AontuContext): Val {
876
+ // A scalar-domain residual has no reading over a container.
877
+ if (null != this.domain) {
878
+ return this.fail(ctx, peer)
879
+ }
880
+ // Not yet settled: the container, or an optional child, may still
881
+ // resolve, so the member set is not final. Defer rather than decide
882
+ // — the same discipline OpBaseVal follows for a non-concrete operand.
883
+ if (!containerSettled(peer)) {
884
+ this.dc = 0
885
+ return new ConjunctVal({ peg: [this, peer] }, ctx)
886
+ }
887
+
888
+ const bad = this.checkMusts(peer, ctx)
889
+ if (null != bad) {
890
+ return bad
891
+ }
892
+
893
+ if (!this.uniq && null == this.count) {
894
+ return peer
895
+ }
896
+
897
+ const members = emittedMembers(peer, ctx)
898
+ if (null == members) {
899
+ // The container cannot generate at all; its own error is the one
900
+ // worth reporting, so pass it through untouched.
901
+ return peer
902
+ }
903
+
904
+ if (null != this.count && !stateAdmits(this.count, countVal(members.length))) {
905
+ return this.fail(ctx, peer)
906
+ }
907
+
908
+ if (this.uniq) {
909
+ // Members compare by CANONICAL FORM, which reduces to scalar
910
+ // identity for scalars (so [1, 1.0] is distinct) and gives
911
+ // structural equality for container members without a second
912
+ // rule. A generated value could not express the first: `1` and
913
+ // `1.0` generate the same JSON number.
914
+ const seen = new Set<string>()
915
+ for (const m of members) {
916
+ const key = m.canon
917
+ if (seen.has(key)) {
918
+ return this.fail(ctx, peer)
919
+ }
920
+ seen.add(key)
289
921
  }
290
922
  }
923
+
291
924
  return peer
292
925
  }
293
926
 
294
927
 
295
928
  // Meet with a kind: `number` (or `string` on the string domain) is
296
- // already implied; a numeric LEAF narrows the residual; anything
297
- // else has an empty intersection with the constraint's domain.
929
+ // already implied by an ORDER atom's argument; a numeric LEAF narrows
930
+ // the residual; anything else has an empty intersection with the
931
+ // constraint's domain.
932
+ //
933
+ // A sizing residual (`length`, `unique`) has no domain of its own -- a
934
+ // count says nothing about what is counted -- so a kind here SETS one
935
+ // rather than merely agreeing with it: `string & length(3)` is a
936
+ // three-character string, and `number & length(3)` is empty because a
937
+ // number has no length (stateEmpty decides that, not this).
298
938
  private meetKind(peer: any, ctx: AontuContext): Val {
299
939
  const marker = peer.peg
940
+ const merged = this.cloneState()
300
941
 
301
- if (Number === marker) {
302
- return 'number' === this.domain ? this : this.fail(ctx, peer)
303
- }
304
- if (String === marker) {
305
- return 'string' === this.domain ? this : this.fail(ctx, peer)
942
+ if (Number === marker || String === marker) {
943
+ const d = Number === marker ? 'number' : 'string'
944
+ if (d === this.domain) {
945
+ return this
946
+ }
947
+ if (null != this.domain) {
948
+ return this.fail(ctx, peer)
949
+ }
950
+ merged.domain = d
951
+ return this.finish(merged, ctx, peer)
306
952
  }
953
+
307
954
  const isLeaf = Integer === marker || Float === marker ||
308
955
  BigInteger === marker || BigDecimal === marker
309
- if (!isLeaf || 'number' !== this.domain) {
956
+ if (!isLeaf || 'string' === this.domain) {
310
957
  return this.fail(ctx, peer)
311
958
  }
312
959
  if (null != this.kind && this.kind !== marker) {
313
960
  return this.fail(ctx, peer)
314
961
  }
315
- const merged = this.cloneState()
962
+ merged.domain = 'number'
316
963
  merged.kind = marker
317
964
  return this.finish(merged, ctx, peer)
318
965
  }
@@ -322,7 +969,8 @@ class ConstraintVal extends FeatureVal {
322
969
  // union, kind union — then the eager emptiness rules.
323
970
  private meetConstraint(peer: ConstraintVal, ctx: AontuContext): Val {
324
971
  if (null != peer.invalid) {
325
- return makeNilErr(ctx, peer.invalid, peer, undefined, 'constrain')
972
+ return makeNilErr(ctx, peer.invalid, peer, undefined, 'constrain',
973
+ null == peer.invalidWhy ? undefined : { reason: peer.invalidWhy })
326
974
  }
327
975
  if (null != this.domain && null != peer.domain && this.domain !== peer.domain) {
328
976
  return this.fail(ctx, peer)
@@ -338,6 +986,18 @@ class ConstraintVal extends FeatureVal {
338
986
  merged.lo = tighter(d, this.lo, peer.lo, true)
339
987
  merged.hi = tighter(d, this.hi, peer.hi, false)
340
988
  merged.neqs = dedupSorted(d, [...this.neqs, ...peer.neqs])
989
+ merged.res = dedupSortedRes([...this.res, ...peer.res])
990
+ // `length(c1) & length(c2)` is `length(c1 & c2)`: the count atom reuses
991
+ // numeric algebra recursively, over the counts rather than the
992
+ // values.
993
+ merged.count = null == this.count ? peer.count :
994
+ null == peer.count ? this.count : meetCount(this.count, peer.count)
995
+ // `unique()` is idempotent: two of them are one.
996
+ merged.uniq = this.uniq || peer.uniq
997
+ // Band B checks accumulate in written order and are never merged,
998
+ // deduplicated or reordered: each carries its own author message,
999
+ // and two checks with the same shape may still say different things.
1000
+ merged.musts = [...this.musts, ...peer.musts]
341
1001
 
342
1002
  return this.finish(merged, ctx, peer)
343
1003
  }
@@ -345,51 +1005,8 @@ class ConstraintVal extends FeatureVal {
345
1005
 
346
1006
  // Build the merged residual, applying the eager emptiness rules.
347
1007
  private finish(state: ConstraintState, ctx: AontuContext, peer: Val): Val {
348
- const d = state.domain as 'number' | 'string'
349
-
350
- if (null != state.lo && null != state.hi) {
351
- const c = cmpVal(d, state.hi.v, state.lo.v)
352
- if (c < 0 || (0 === c && (state.lo.open || state.hi.open))) {
353
- return this.fail(ctx, peer)
354
- }
355
- }
356
-
357
- const integral = Integer === state.kind || BigInteger === state.kind
358
-
359
- // Integral gap: an integer-narrowed interval containing no whole
360
- // number is empty (integer & above(1) & below(2)).
361
- if (integral && null != state.lo && null != state.hi) {
362
- const lo = scaledOfNumeric(state.lo.v)
363
- const hi = scaledOfNumeric(state.hi.v)
364
- if (!lo.inf && !hi.inf) {
365
- // Smallest admissible integer above/at the lower bound.
366
- let n = scaledFloor(lo)
367
- if (!scaledIsIntegral(lo) || state.lo.open) {
368
- n += 1n
369
- }
370
- // Largest admissible integer below/at the upper bound.
371
- let m = scaledFloor(hi)
372
- if (state.hi.open && scaledIsIntegral(hi)) {
373
- m -= 1n
374
- }
375
- if (m < n) {
376
- return this.fail(ctx, peer)
377
- }
378
- }
379
- }
380
-
381
- // Point deletion under a narrowed leaf: a closed point interval
382
- // whose single value of the narrowed leaf is excluded is empty
383
- // (integer & min(3) & max(3) & neq(3)). Without a narrowing the
384
- // point survives in the other leaves.
385
- if (null != state.kind && null != state.lo && null != state.hi &&
386
- !state.lo.open && !state.hi.open &&
387
- 0 === cmpVal(d, state.lo.v, state.hi.v)) {
388
- for (const n of state.neqs) {
389
- if (leafMarker(n) === state.kind && 0 === cmpNumeric(n, state.lo.v)) {
390
- return this.fail(ctx, peer)
391
- }
392
- }
1008
+ if (stateEmpty(state)) {
1009
+ return this.fail(ctx, peer)
393
1010
  }
394
1011
 
395
1012
  const out = new ConstraintVal({ peg: [], state }, ctx)
@@ -422,6 +1039,10 @@ class ConstraintVal extends FeatureVal {
422
1039
  lo: this.lo,
423
1040
  hi: this.hi,
424
1041
  neqs: [...this.neqs],
1042
+ res: [...this.res],
1043
+ count: this.count,
1044
+ uniq: this.uniq,
1045
+ musts: [...this.musts],
425
1046
  invalid: this.invalid,
426
1047
  }
427
1048
  }
@@ -437,33 +1058,29 @@ class ConstraintVal extends FeatureVal {
437
1058
  out.lo = this.lo
438
1059
  out.hi = this.hi
439
1060
  out.neqs = [...this.neqs]
1061
+ out.res = [...this.res]
1062
+ out.count = this.count
1063
+ out.uniq = this.uniq
1064
+ out.musts = [...this.musts]
1065
+ out.pending = this.pending
1066
+ out.cjo = this.cjo
440
1067
  out.invalid = this.invalid
1068
+ out.invalidWhy = this.invalidWhy
441
1069
  return out
442
1070
  }
443
1071
 
444
1072
 
445
1073
  // The fixed canonical atom order: kind, lower, upper, neq (arguments
446
- // sorted). No spaces; reparses to a conjunct that normalises back to
447
- // this exact residual.
1074
+ // sorted), re, length, unique. No spaces; reparses to a conjunct that
1075
+ // normalises back to this exact residual.
448
1076
  get canon() {
449
- const parts: string[] = []
450
- if (null != this.kind) {
451
- parts.push((this.kind as any).name.toLowerCase())
452
- }
453
- if (null != this.lo) {
454
- parts.push((this.lo.open ? 'above(' : 'min(') + this.lo.v.canon + ')')
455
- }
456
- if (null != this.hi) {
457
- parts.push((this.hi.open ? 'below(' : 'max(') + this.hi.v.canon + ')')
458
- }
459
- if (0 < this.neqs.length) {
460
- parts.push('neq(' + this.neqs.map((n: any) => n.canon).join(',') + ')')
1077
+ if (null != this.pending) {
1078
+ // A pending atom has no residual yet, so canon renders the call as
1079
+ // written — the same shape FuncBaseVal renders while deferring.
1080
+ return this.pending.atom +
1081
+ '(' + this.pending.args.map((a: any) => a.canon).join(',') + ')'
461
1082
  }
462
- if (0 === parts.length) {
463
- // Raw invalid atom: render the call so the error frame shows it.
464
- return 'constraint()'
465
- }
466
- return parts.join('&')
1083
+ return canonState(this)
467
1084
  }
468
1085
 
469
1086
 
@@ -517,7 +1134,442 @@ function dedupSorted(domain: 'number' | 'string', neqs: any[]): any[] {
517
1134
  }
518
1135
 
519
1136
 
520
- // The five atom classes registered in the parser's funcMap: each is a
1137
+ // Is this value, or anything inside it, a nil? A written argument that
1138
+ // holds one can never be satisfied, so the atom refuses it as an
1139
+ // argument rather than reporting a mystery failure against every peer.
1140
+ function holdsNil(v: any): boolean {
1141
+ if (null == v || true !== v.isVal) {
1142
+ return false
1143
+ }
1144
+ if (true === v.isNil) {
1145
+ return true
1146
+ }
1147
+ const peg = v.peg
1148
+ if (Array.isArray(peg)) {
1149
+ return peg.some((c: any) => holdsNil(c))
1150
+ }
1151
+ if (null != peg && 'object' === typeof peg) {
1152
+ for (const k in peg) {
1153
+ if (holdsNil(peg[k])) {
1154
+ return true
1155
+ }
1156
+ }
1157
+ }
1158
+ return false
1159
+ }
1160
+
1161
+
1162
+ // The written arguments of an atom, flattened.
1163
+ //
1164
+ // A multi-argument call arrives from the func-paren grammar as ONE
1165
+ // entry holding the comma group, and `neq([3,1,2])` means the same as
1166
+ // `neq(3,1,2)`. The group is always a ListVal by the time it reaches
1167
+ // here -- the func-paren handler rawToVals every argument (issue #49) --
1168
+ // so there is no raw-array case to unwrap. Flattening happens before
1169
+ // the settled check, because an unsettled member hiding inside the
1170
+ // group would otherwise make the atom look ready.
1171
+ function atomArgs(atom: string, args: any[]): any[] {
1172
+ if (('neq' === atom || 'must' === atom) && 1 === args.length &&
1173
+ true === (args[0] as any)?.isList) {
1174
+ return (args[0] as any).peg
1175
+ }
1176
+ return args
1177
+ }
1178
+
1179
+
1180
+ // The canonical rendering of a residual, in the fixed atom order:
1181
+ // kind, lower bound, upper bound, neq, re, length, unique. Taken over the
1182
+ // STATE rather than the Val because `length`'s argument is a residual too,
1183
+ // and renders by exactly the same rules.
1184
+ function canonState(s: ConstraintState): string {
1185
+ const parts: string[] = []
1186
+ if (null != s.kind) {
1187
+ parts.push((s.kind as any).name.toLowerCase())
1188
+ }
1189
+ // An ORDER atom's argument implies the domain, so it is not spelled
1190
+ // out. A SIZING residual carries no order, and there `string` is the
1191
+ // only thing saying what is being sized -- drop it and the reparse
1192
+ // would admit lists and maps too.
1193
+ else if ('string' === s.domain &&
1194
+ null == s.lo && null == s.hi && 0 === s.neqs.length && 0 === s.res.length) {
1195
+ parts.push('string')
1196
+ }
1197
+ if (null != s.lo) {
1198
+ parts.push((s.lo.open ? 'above(' : 'min(') + s.lo.v.canon + ')')
1199
+ }
1200
+ if (null != s.hi) {
1201
+ parts.push((s.hi.open ? 'below(' : 'max(') + s.hi.v.canon + ')')
1202
+ }
1203
+ if (0 < s.neqs.length) {
1204
+ parts.push('neq(' + s.neqs.map((n: any) => n.canon).join(',') + ')')
1205
+ }
1206
+ for (const r of s.res) {
1207
+ parts.push('re(' + r.v.canon + ')')
1208
+ }
1209
+ if (null != s.count) {
1210
+ // Rendered UNABRIDGED, implied parts and all: `length(3)` canonicalises
1211
+ // to `length(integer&min(3)&max(3))` because that IS the residual the
1212
+ // count must satisfy, and canon is a normal form (G6 hashes it),
1213
+ // not a pretty-printer. Abbreviating would mean a second set of
1214
+ // rules for when the implied `integer & min(0)` may be dropped.
1215
+ parts.push('length(' + canonState(s.count) + ')')
1216
+ }
1217
+ if (s.uniq) {
1218
+ parts.push('unique()')
1219
+ }
1220
+ for (const m of s.musts) {
1221
+ parts.push('must(' + m.v.canon + ',' + m.msg.canon + ')')
1222
+ }
1223
+ if (0 === parts.length) {
1224
+ // Raw invalid atom: render the call so the error frame shows it.
1225
+ return 'constraint()'
1226
+ }
1227
+ return parts.join('&')
1228
+ }
1229
+
1230
+
1231
+ // Does this residual's ORDER and MEMBERSHIP part admit the scalar? The
1232
+ // sizing atoms are deliberately not consulted: `admit` applies them to
1233
+ // the peer's length, and the count check applies this same function to
1234
+ // the count.
1235
+ function stateAdmits(s: ConstraintState, peer: any): boolean {
1236
+ const domainOf = numericLeaf(peer) ? 'number' :
1237
+ stringLeaf(peer) ? 'string' : undefined
1238
+
1239
+ if (null == s.domain) {
1240
+ // A sizing residual has no domain, and admits any scalar the sizing
1241
+ // atoms can then rule on. Booleans and null are not among them:
1242
+ // they have no order, no length and no members.
1243
+ if (null == domainOf) {
1244
+ return false
1245
+ }
1246
+ return true
1247
+ }
1248
+ if (domainOf !== s.domain) {
1249
+ return false
1250
+ }
1251
+ if (null != s.kind && leafMarker(peer) !== s.kind) {
1252
+ return false
1253
+ }
1254
+ const d = s.domain
1255
+ if (null != s.lo) {
1256
+ const c = cmpVal(d, peer, s.lo.v)
1257
+ if (c < 0 || (0 === c && s.lo.open)) {
1258
+ return false
1259
+ }
1260
+ }
1261
+ if (null != s.hi) {
1262
+ const c = cmpVal(d, peer, s.hi.v)
1263
+ if (c > 0 || (0 === c && s.hi.open)) {
1264
+ return false
1265
+ }
1266
+ }
1267
+ for (const n of s.neqs) {
1268
+ if (sameScalar(peer, n)) {
1269
+ return false
1270
+ }
1271
+ }
1272
+ // Every accumulated pattern must match: the meet of two `re` atoms
1273
+ // is conjunction, and matching is UNANCHORED in both engines
1274
+ // (JS RegExp.test, Go regexp.MatchString), so `re("el")` admits
1275
+ // "hello". Anchor with ^ and $ to mean the whole string.
1276
+ for (const r of s.res) {
1277
+ if (!r.re.test(peer.peg)) {
1278
+ return false
1279
+ }
1280
+ }
1281
+ return true
1282
+ }
1283
+
1284
+
1285
+ // The eager emptiness rules. Each is EXACT -- it reports empty only
1286
+ // where no value could satisfy the residual -- so the algebra stays
1287
+ // sound; the incompleteness it accepts is documented in
1288
+ // docs/reference-language.md, "Emptiness".
1289
+ function stateEmpty(s: ConstraintState): boolean {
1290
+ // Two disagreeing kind narrowings inside a length argument, recorded by
1291
+ // meetCount because that meet has no ctx to fail through.
1292
+ if (s.clash) {
1293
+ return true
1294
+ }
1295
+
1296
+ const d = s.domain as 'number' | 'string'
1297
+
1298
+ // Empty interval.
1299
+ if (null != s.lo && null != s.hi) {
1300
+ const c = cmpVal(d, s.hi.v, s.lo.v)
1301
+ if (c < 0 || (0 === c && (s.lo.open || s.hi.open))) {
1302
+ return true
1303
+ }
1304
+ }
1305
+
1306
+ const integral = Integer === s.kind || BigInteger === s.kind
1307
+
1308
+ // Integral gap: an integer-narrowed interval containing no whole
1309
+ // number is empty (integer & above(1) & below(2)).
1310
+ if (integral && null != s.lo && null != s.hi) {
1311
+ const lo = scaledOfNumeric(s.lo.v)
1312
+ const hi = scaledOfNumeric(s.hi.v)
1313
+ if (!lo.inf && !hi.inf) {
1314
+ // Smallest admissible integer above/at the lower bound.
1315
+ let n = scaledFloor(lo)
1316
+ if (!scaledIsIntegral(lo) || s.lo.open) {
1317
+ n += 1n
1318
+ }
1319
+ // Largest admissible integer below/at the upper bound.
1320
+ let m = scaledFloor(hi)
1321
+ if (s.hi.open && scaledIsIntegral(hi)) {
1322
+ m -= 1n
1323
+ }
1324
+ if (m < n) {
1325
+ return true
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ // Point deletion under a narrowed leaf: a closed point interval
1331
+ // whose single value of the narrowed leaf is excluded is empty
1332
+ // (integer & min(3) & max(3) & neq(3)). Without a narrowing the
1333
+ // point survives in the other leaves.
1334
+ if (null != s.kind && null != s.lo && null != s.hi &&
1335
+ !s.lo.open && !s.hi.open &&
1336
+ 0 === cmpVal(d, s.lo.v, s.hi.v)) {
1337
+ for (const n of s.neqs) {
1338
+ if (leafMarker(n) === s.kind && 0 === cmpNumeric(n, s.lo.v)) {
1339
+ return true
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ // Sizing over the number domain: a number has neither a length nor
1345
+ // members, so `integer & length(3)` and `min(2) & unique()` admit
1346
+ // nothing. Uniqueness over the string domain is empty for the same
1347
+ // reason -- a string's members are not values the algebra compares.
1348
+ if ('number' === d && (null != s.count || s.uniq)) {
1349
+ return true
1350
+ }
1351
+ if ('string' === d && s.uniq) {
1352
+ return true
1353
+ }
1354
+
1355
+ // An empty count residual makes the whole thing empty: no container
1356
+ // and no string has a length no integer can take.
1357
+ if (null != s.count && stateEmpty(s.count)) {
1358
+ return true
1359
+ }
1360
+
1361
+ return false
1362
+ }
1363
+
1364
+
1365
+ // The base every `length` argument meets: a count is a non-negative
1366
+ // integer, whatever else the argument says (docs/reference-language.md,
1367
+ // "`length` semantics" -- `length(c)` is empty iff `c & integer & min(0)`
1368
+ // is).
1369
+ function countBase(): ConstraintState {
1370
+ return {
1371
+ domain: 'number',
1372
+ kind: Integer,
1373
+ lo: { v: countVal(0), open: false },
1374
+ neqs: [],
1375
+ res: [],
1376
+ musts: [],
1377
+ uniq: false,
1378
+ }
1379
+ }
1380
+
1381
+
1382
+ // A count as a Val, so the count residual can be applied by exactly the
1383
+ // same membership function as any other numeric residual.
1384
+ function countVal(n: number): any {
1385
+ return new IntegerVal({ peg: n })
1386
+ }
1387
+
1388
+
1389
+ // The pure meet of two count residuals. Both are number-domain and
1390
+ // carry no pattern or sizing atom of their own, so the merge is the
1391
+ // interval/exclusion part alone. A kind disagreement becomes a `clash`
1392
+ // rather than an error: this meet runs at construction, where there is
1393
+ // no AontuContext to raise through, and an empty residual carries the
1394
+ // same news to `unify`.
1395
+ function meetCount(a: ConstraintState, b: ConstraintState): ConstraintState {
1396
+ return {
1397
+ domain: 'number',
1398
+ kind: a.kind ?? b.kind,
1399
+ lo: tighter('number', a.lo, b.lo, true),
1400
+ hi: tighter('number', a.hi, b.hi, false),
1401
+ neqs: dedupSorted('number', [...a.neqs, ...b.neqs]),
1402
+ res: [],
1403
+ musts: [],
1404
+ uniq: false,
1405
+ clash: true === a.clash || true === b.clash ||
1406
+ (null != a.kind && null != b.kind && a.kind !== b.kind),
1407
+ }
1408
+ }
1409
+
1410
+
1411
+ // Read a WRITTEN `length` argument as a count residual, or undefined when
1412
+ // it is not one. Accepted: an integer literal (an exact count), a
1413
+ // numeric kind, a Band A residual over the number domain, and any
1414
+ // conjunct of those. A conjunct never reaches here any more: an
1415
+ // unsettled argument is held as `pending` and folded before the count
1416
+ // reads it (G1 phase 4), so `length(min(2)&max(5))` arrives as the
1417
+ // single residual it folds to.
1418
+ //
1419
+ // Anything else is refused rather than deferred. A reference or an
1420
+ // expression would have to residuate, and the sizing atoms do not
1421
+ // residuate on their ARGUMENT -- only on the peer whose members are
1422
+ // still settling.
1423
+ function countArgState(arg: any): ConstraintState | undefined {
1424
+ if (numericLeaf(arg)) {
1425
+ return {
1426
+ domain: 'number',
1427
+ lo: { v: arg, open: false },
1428
+ hi: { v: arg, open: false },
1429
+ neqs: [], res: [], musts: [], uniq: false,
1430
+ }
1431
+ }
1432
+
1433
+ if (true === arg?.isConstraint) {
1434
+ const c = arg as ConstraintVal
1435
+ // A pattern, a sizing atom or a string bound inside a count is not
1436
+ // a count constraint at all, and neither is a broken one.
1437
+ if (null != c.invalid || 0 < c.res.length || c.uniq || null != c.count ||
1438
+ 'number' !== c.domain) {
1439
+ return undefined
1440
+ }
1441
+ return {
1442
+ domain: 'number',
1443
+ kind: c.kind,
1444
+ lo: c.lo,
1445
+ hi: c.hi,
1446
+ neqs: [...c.neqs],
1447
+ res: [], musts: [], uniq: false,
1448
+ }
1449
+ }
1450
+
1451
+ if (true === arg?.isScalarKind) {
1452
+ const marker = arg.peg
1453
+ if (Number === marker) {
1454
+ return { domain: 'number', neqs: [], res: [], musts: [], uniq: false }
1455
+ }
1456
+ if (Integer === marker || Float === marker ||
1457
+ BigInteger === marker || BigDecimal === marker) {
1458
+ return { domain: 'number', kind: marker, neqs: [], res: [], musts: [], uniq: false }
1459
+ }
1460
+ return undefined
1461
+ }
1462
+
1463
+ return undefined
1464
+ }
1465
+
1466
+
1467
+ // A container is SETTLED when it and every child have converged. Until
1468
+ // then the member set can still change — an optional key whose value is
1469
+ // a still-resolving reference may yet generate — so a sizing atom must
1470
+ // defer rather than decide (docs/reference-language.md, "`length`
1471
+ // semantics"). Note that an optional holding a settled-but-ungenerable
1472
+ // value, `{x:1,y?:number}`, IS settled: the map converges on the first
1473
+ // pass and `y` is simply never emitted.
1474
+ function containerSettled(bag: any): boolean {
1475
+ // The bag's OWN done-counter is enough: BagVal.unify sets it from the
1476
+ // AND over its children, so an unsettled child already leaves the bag
1477
+ // unsettled. Walking the children again would be a second, drifting
1478
+ // copy of that rule.
1479
+ return true === bag.done
1480
+ }
1481
+
1482
+
1483
+ // The child kinds `BagVal.gen` will attempt to generate. Anything else
1484
+ // is residue: dropped when the key is optional, and a bag-level error
1485
+ // otherwise.
1486
+ function genable(child: any): boolean {
1487
+ return true === child.isScalar || true === child.isMap ||
1488
+ true === child.isList || true === child.isPref ||
1489
+ true === child.isRef || true === child.isDisjunct ||
1490
+ true === child.isNil
1491
+ }
1492
+
1493
+
1494
+ // The children a bag would EMIT, mirroring the selection in
1495
+ // `BagVal.gen` — type/hide marks skipped, non-generable residue and
1496
+ // values that generate nothing dropped, optional keys dropped when
1497
+ // they generate empty.
1498
+ //
1499
+ // It returns the member VALS rather than their generated values,
1500
+ // because uniqueness compares canon and a generated value cannot
1501
+ // express that distinction: `1` and `1.0` generate the same JSON number
1502
+ // and canon differently. Counting uses the same list, so both sizing
1503
+ // atoms see exactly one definition of "member".
1504
+ //
1505
+ // Returns undefined when a REQUIRED child is residue: the container's
1506
+ // own `gen` raises there, and that error is the one worth reporting.
1507
+ function emittedMembers(bag: any, ctx: AontuContext): any[] | undefined {
1508
+ const out: any[] = []
1509
+
1510
+ let entries = items(bag.peg)
1511
+ if (bag.isMap) {
1512
+ // Code-point order, because the two ports disagree on raw key order
1513
+ // — JavaScript hoists integer-like keys, Go keeps insertion order —
1514
+ // and a duplicate report must name the same pair in both.
1515
+ entries = entries
1516
+ .slice()
1517
+ .sort((a: any, b: any) => cmpCodePoint(String(a[0]), String(b[0])))
1518
+ }
1519
+
1520
+ for (const item of entries) {
1521
+ const key = item[0]
1522
+ const child: any = item[1]
1523
+
1524
+ if (child.mark.type || child.mark.hide) {
1525
+ continue
1526
+ }
1527
+
1528
+ const optional = bag.optionalKeys.includes('' + key)
1529
+
1530
+ if (!genable(child)) {
1531
+ if (optional) {
1532
+ continue
1533
+ }
1534
+ return undefined
1535
+ }
1536
+
1537
+ // Generation decides in an isolated collect context, so an
1538
+ // unresolved inner value neither raises here nor pollutes the
1539
+ // caller's errors — the same isolation BagVal.gen uses for an
1540
+ // optional child.
1541
+ const cval = child.gen(ctx.clone({ err: [], collect: true }))
1542
+
1543
+ if (undefined === cval || (optional && empty(cval))) {
1544
+ continue
1545
+ }
1546
+
1547
+ out.push(child)
1548
+ }
1549
+
1550
+ return out
1551
+ }
1552
+
1553
+
1554
+ // Sort accumulated patterns by source in code-point order and drop
1555
+ // exact duplicates. Patterns are NEVER simplified or compared for
1556
+ // containment: deciding `re("a")` subsumes `re("a|b")` is regex
1557
+ // containment, which the algebra deliberately does not do (emptiness
1558
+ // stays approximate — sound, incomplete). Two spellings of one language
1559
+ // therefore both survive, and both are tested.
1560
+ function dedupSortedRes(res: ReAtom[]): ReAtom[] {
1561
+ const sorted = [...res].sort((a, b) => cmpCodePoints(a.src, b.src))
1562
+ const out: ReAtom[] = []
1563
+ for (const r of sorted) {
1564
+ if (0 === out.length || out[out.length - 1].src !== r.src) {
1565
+ out.push(r)
1566
+ }
1567
+ }
1568
+ return out
1569
+ }
1570
+
1571
+
1572
+ // The atom classes registered in the parser's funcMap: each is a
521
1573
  // ConstraintVal that knows its atom name. Constructed by the
522
1574
  // func-paren handler as `new funcval({peg: args})`.
523
1575
  class MinConstraintVal extends ConstraintVal {
@@ -548,14 +1600,46 @@ class NeqConstraintVal extends ConstraintVal {
548
1600
  constructor(spec: ValSpec, ctx?: AontuContext) {
549
1601
  super({ ...spec, atom: 'neq' }, ctx)
550
1602
  }
551
- } /* node:coverage ignore next 11 */
1603
+ }
1604
+
1605
+ class ReConstraintVal extends ConstraintVal {
1606
+ constructor(spec: ValSpec, ctx?: AontuContext) {
1607
+ super({ ...spec, atom: 're' }, ctx)
1608
+ }
1609
+ }
1610
+
1611
+ class MustConstraintVal extends ConstraintVal {
1612
+ constructor(spec: ValSpec, ctx?: AontuContext) {
1613
+ super({ ...spec, atom: 'must' }, ctx)
1614
+ }
1615
+ }
1616
+
1617
+ class LengthConstraintVal extends ConstraintVal {
1618
+ constructor(spec: ValSpec, ctx?: AontuContext) {
1619
+ super({ ...spec, atom: 'length' }, ctx)
1620
+ }
1621
+ }
1622
+
1623
+ class UniqueConstraintVal extends ConstraintVal {
1624
+ constructor(spec: ValSpec, ctx?: AontuContext) {
1625
+ super({ ...spec, atom: 'unique' }, ctx)
1626
+ }
1627
+ } /* node:coverage ignore next 19 */
552
1628
 
553
1629
 
554
1630
  export {
1631
+ // Exported for the differential corpus test (ADR-003): the two ports'
1632
+ // normalisers must produce byte-identical output, and that can only be
1633
+ // checked by calling them.
1634
+ normaliseRe,
555
1635
  ConstraintVal,
556
1636
  MinConstraintVal,
557
1637
  MaxConstraintVal,
558
1638
  AboveConstraintVal,
559
1639
  BelowConstraintVal,
560
1640
  NeqConstraintVal,
1641
+ ReConstraintVal,
1642
+ LengthConstraintVal,
1643
+ UniqueConstraintVal,
1644
+ MustConstraintVal,
561
1645
  }