aontu 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. package/src/walk.ts +99 -0
@@ -49,8 +49,22 @@ 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
+ import { sizingResidue } from './BagVal'
61
+
62
+ import { top } from './top'
63
+
64
+ import { unite, withDepth } from '../unify'
65
+
66
+ import { IntegerVal } from './IntegerVal'
67
+
54
68
  import { makeNilErr } from '../err'
55
69
 
56
70
  import { FeatureVal } from './FeatureVal'
@@ -77,16 +91,447 @@ type Bound = {
77
91
  open: boolean // true for above/below, false for min/max
78
92
  }
79
93
 
94
+ type ReAtom = {
95
+ v: any // the stored pattern StringVal (canon renders the literal)
96
+ src: string // the pattern text AS WRITTEN — canon and dedup use this,
97
+ // never the normalised form, because canon round-trips
98
+ // source and G6's hash will be taken over canon
99
+ norm: string // the engine-neutral rewrite actually compiled (ADR-003)
100
+ re: RegExp // compiled by the host engine, from `norm`
101
+ }
102
+
103
+ type MustAtom = {
104
+ v: any // the value the peer must unify with (any Aontu value)
105
+ msg: any // the author's message StringVal (canon renders the literal)
106
+ }
107
+
80
108
  type ConstraintState = {
81
109
  domain?: 'number' | 'string'
82
110
  kind?: any // numeric leaf marker (Integer | Float | ...) or undefined
83
111
  lo?: Bound
84
112
  hi?: Bound
85
113
  neqs: any[] // excluded scalars, identity per leaf+value
114
+ res: ReAtom[] // accumulated patterns, sorted by source (never simplified)
115
+ count?: ConstraintState // the COUNT residual (length()), itself a residual
116
+ // over the integer domain -- the count atom reuses
117
+ // this same algebra recursively
118
+ uniq: boolean // members must be pairwise distinct (unique())
119
+ uniqBy: string[] // ... and distinct ON EACH OF THESE KEYS
120
+ // (unique(k)), sorted and deduplicated
121
+ musts: MustAtom[] // Band B checks, kept in written order, never simplified
122
+ clash?: boolean // a kind disagreement inside a length() argument, recorded
123
+ // rather than raised: the argument's own meet has no
124
+ // ctx to report through, so emptiness carries the news
86
125
  invalid?: string // why-code when the atom's arguments were unusable
87
126
  }
88
127
 
89
128
 
129
+ // THE PATTERN SUBSET, AND HOW IT IS ENFORCED (G1 phase 2; ADR-003).
130
+ //
131
+ // `re(p)` must mean the same thing in both engines and cost about the
132
+ // same, and the two host engines guarantee neither: TypeScript compiles
133
+ // with JavaScript's backtracking RegExp, Go with RE2 — a different
134
+ // language in a different complexity class, over a different alphabet.
135
+ //
136
+ // The enforcement is NORMALISATION, not refusal (ADR-003). Every
137
+ // construct whose expansion is engine-defined is rewritten here, by
138
+ // this function, into an explicit form that cannot be read two ways;
139
+ // only the rewritten pattern reaches a host engine. The alternative —
140
+ // refusing everything that might differ — was tried first and leaked
141
+ // three times, because its correctness depended on this comment knowing
142
+ // every difference between two large engines.
143
+ //
144
+ // Aontu therefore DEFINES the abbreviations rather than inheriting
145
+ // either host's. The definitions are deliberately the small ASCII ones,
146
+ // because a config value containing U+00A0 is a mistake to catch, not a
147
+ // space to accept silently:
148
+ //
149
+ // \d [0-9] \D [^0-9]
150
+ // \w [0-9A-Za-z_] \W [^0-9A-Za-z_]
151
+ // \s [ \t\n\r\f\v] \S [^ \t\n\r\f\v]
152
+ // . [^\n]
153
+ // \A ^ \z $
154
+ //
155
+ // Neither host agreed with all of these before rewriting: JavaScript's
156
+ // \s also matches U+00A0 and other Unicode spaces, RE2's omits \v, and
157
+ // the two `.` sets differ by \r and the Unicode line separators. After
158
+ // rewriting, both engines see one explicit class and cannot disagree.
159
+ //
160
+ // What is still REFUSED, and why refusal is right for these:
161
+ //
162
+ // 1. Constructs one engine simply lacks — backreferences and
163
+ // lookaround (not in RE2, and not expressible as regular
164
+ // expressions at all), POSIX classes, `\p{...}`, `\x{...}`, `\u`.
165
+ // There is nothing to normalise them TO.
166
+ // 2. `(?` other than `(?:` — named groups are spelled differently
167
+ // (`(?P<n>` in RE2, `(?<n>` in JavaScript) and inline flags change
168
+ // the meaning of everything after them.
169
+ // 3. A quantifier applied to a group containing a quantifier or an
170
+ // alternation. This one is about TIME, not meaning: `(a+)+$`
171
+ // against twenty-nine characters takes 45 SECONDS in JavaScript
172
+ // and 0.065s under RE2, and a regex match is counted by no
173
+ // evaluator budget, so an untrusted schema could otherwise stall
174
+ // the TypeScript evaluator indefinitely (docs/trust.md clause 2).
175
+ // Normalisation cannot fix a complexity difference; only owning the
176
+ // matcher could, which ADR-003 records as the future option.
177
+ //
178
+ // The alphabet is fixed separately, by compiling with the `u` flag:
179
+ // JavaScript otherwise matches UTF-16 code units where RE2 matches code
180
+ // points, so `^.$` accepted U+1D11E in Go and refused it in TypeScript.
181
+ //
182
+ // This function is mirrored statement for statement in go/constraint.go,
183
+ // and `test/spec/files/regex-corpus.txt` pins that the two produce
184
+ // byte-identical output for every pattern in a generated corpus.
185
+
186
+ // The largest repeat count Aontu's pattern language admits. RE2 caps a
187
+ // repeat at 1000 and refuses to compile past it, where JavaScript's
188
+ // backtracking engine accepts any count -- so `re("^a{1001}$")` was
189
+ // VALID in TypeScript and an `error` verdict in Go, a cross-port
190
+ // verdict flip on a schema an agent could be handed
191
+ // (status-2026-08-21.md section 4). Under ADR-003 the host's own
192
+ // compile failure must never be what a user sees, so the bound is
193
+ // Aontu's: at most RE_REPEAT_MAX, refused identically in both ports
194
+ // before either engine compiles. Nested products beyond the cap were
195
+ // already refused, by the quantified-group rule below.
196
+ const RE_REPEAT_MAX = 1000
197
+
198
+ // The normative expansions. These are Aontu's definitions, not either
199
+ // host's; both hosts are rewritten to them.
200
+ const RE_CLASS_DIGIT = '0-9'
201
+ const RE_CLASS_WORD = '0-9A-Za-z_'
202
+ const RE_CLASS_SPACE = ' \\t\\n\\r\\f\\v'
203
+
204
+ // Metacharacters that may be escaped to mean themselves, in both
205
+ // engines. `-` is handled separately: it is legal escaped only INSIDE a
206
+ // character class, because RE2 accepts `a\-b` and JavaScript's unicode
207
+ // mode makes it a syntax error.
208
+ const RE_ESCAPE_PUNCT = '\\.+*?()[]{}|^$/'
209
+
210
+ // Escapes passed through unchanged: the control characters, the ASCII
211
+ // word boundary, and `\xHH`. Each was probed in both engines.
212
+ const RE_ESCAPE_PASS = 'tnrfv'
213
+
214
+ // A counted quantifier `{n}`, `{n,}` or `{n,m}` starting at `src[at]`.
215
+ // Two rules, both about what the hosts do NOT share:
216
+ //
217
+ // - A bound above RE_REPEAT_MAX: RE2 refuses to compile it, JavaScript
218
+ // accepts it.
219
+ // - A brace that does not open a well-formed counted quantifier at
220
+ // all (`x{y}`, `a{`, `{,5}`): JavaScript compiled with the `u` flag
221
+ // makes it a SYNTAX ERROR, while RE2 reads it as a literal brace, so
222
+ // `re("^x{y}$")` was `constraint_pattern` in TypeScript and a plain
223
+ // unresolved value in Go -- the same class of verdict flip as the
224
+ // repeat cap, found by sweeping around it. A literal brace is
225
+ // written escaped (`a\\{1001\\}`), which both engines share.
226
+ function repeatWhy(src: string, at: number): [string, number] {
227
+ const bad = (what: string): [string, number] => [
228
+ 'a ' + what + ', which the two engines do not read the same way', -1]
229
+
230
+ let i = at + 1
231
+ let digits = ''
232
+ const bounds: number[] = []
233
+ let commas = 0
234
+ for (; i < src.length; i++) {
235
+ const c = src[i]
236
+ if ('0' <= c && c <= '9') {
237
+ digits += c
238
+ continue
239
+ }
240
+ if (',' === c) {
241
+ if ('' === digits || 0 < commas) {
242
+ return bad('{ that does not open a counted quantifier')
243
+ }
244
+ bounds.push(parseInt(digits, 10))
245
+ digits = ''
246
+ commas++
247
+ continue
248
+ }
249
+ if ('}' === c) {
250
+ if ('' !== digits) {
251
+ bounds.push(parseInt(digits, 10))
252
+ }
253
+ else if (0 === commas) {
254
+ return bad('{ that does not open a counted quantifier')
255
+ }
256
+ break
257
+ }
258
+ return bad('{ that does not open a counted quantifier')
259
+ }
260
+ if (i >= src.length) {
261
+ return bad('{ that does not open a counted quantifier')
262
+ }
263
+ for (const b of bounds) {
264
+ if (RE_REPEAT_MAX < b) {
265
+ return ['a repeat count above ' + RE_REPEAT_MAX +
266
+ ', which RE2 refuses to compile', -1]
267
+ }
268
+ }
269
+ // A descending range (`{5,2}`) is refused by both engines' own
270
+ // compilers, so it needs no rule here.
271
+ return ['', i]
272
+ }
273
+
274
+
275
+ function isHexDigit(c: string | undefined): boolean {
276
+ return null != c && (
277
+ ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F'))
278
+ }
279
+
280
+
281
+ // normaliseEscape rewrites one `\<n>` into its engine-neutral form.
282
+ // Returns [emitted, why, extra]: `why` non-empty means refused, and
283
+ // `extra` counts source characters consumed beyond the backslash and n.
284
+ function normaliseEscape(
285
+ n: string | undefined, src: string, i: number, inClass: boolean
286
+ ): [string, string, number] {
287
+ if (null == n) {
288
+ return ['', 'a trailing backslash', 0]
289
+ }
290
+ if ('1' <= n && n <= '9') {
291
+ return ['', 'a backreference (\\' + n + '): RE2 has no equivalent, and a' +
292
+ ' pattern with one is not a regular expression', 0]
293
+ }
294
+ if ('k' === n) {
295
+ return ['', 'a named backreference (\\k): RE2 has no equivalent', 0]
296
+ }
297
+ if ('u' === n) {
298
+ return ['', 'a \\u escape, which RE2 spells \\x{...}: write the character' +
299
+ ' itself, or \\xHH for a byte', 0]
300
+ }
301
+ if ('p' === n || 'P' === n) {
302
+ return ['', 'a Unicode class (\\' + n + '), which JavaScript reads as a' +
303
+ ' literal "' + n + '" without a flag Aontu does not set', 0]
304
+ }
305
+ if ('Z' === n) {
306
+ return ['', '\\Z, which RE2 does not accept and JavaScript reads as a' +
307
+ ' literal "Z": write $ for end of text', 0]
308
+ }
309
+ if ('x' === n) {
310
+ if ('{' === src[i + 2]) {
311
+ return ['', 'a \\x{...} escape, which JavaScript spells \\u: write the' +
312
+ ' character itself', 0]
313
+ }
314
+ if (!isHexDigit(src[i + 2]) || !isHexDigit(src[i + 3])) {
315
+ return ['', 'an \\x escape without two hex digits', 0]
316
+ }
317
+ return ['\\x' + src[i + 2] + src[i + 3], '', 2]
318
+ }
319
+
320
+ // The abbreviations, rewritten to Aontu's definitions. Inside a class
321
+ // the expansion splices without its brackets (`[\dx]` -> `[0-9x]`).
322
+ if ('d' === n || 'w' === n || 's' === n) {
323
+ const set = 'd' === n ? RE_CLASS_DIGIT :
324
+ 'w' === n ? RE_CLASS_WORD : RE_CLASS_SPACE
325
+ return [inClass ? set : '[' + set + ']', '', 0]
326
+ }
327
+ if ('D' === n || 'W' === n || 'S' === n) {
328
+ if (inClass) {
329
+ // `[^...]` cannot be spliced into an enclosing class: the negation
330
+ // would apply to the whole class rather than this member.
331
+ return ['', 'a negated abbreviation (\\' + n + ') inside a character' +
332
+ ' class, which cannot be expanded in place: write the characters out', 0]
333
+ }
334
+ const set = 'D' === n ? RE_CLASS_DIGIT :
335
+ 'W' === n ? RE_CLASS_WORD : RE_CLASS_SPACE
336
+ return ['[^' + set + ']', '', 0]
337
+ }
338
+
339
+ // Anchors. `\A`/`\z` are RE2 spellings that JavaScript reads as
340
+ // literals, so they are rewritten rather than refused. Inside a class
341
+ // an anchor is meaningless, and `[\b]` is a BACKSPACE in JavaScript.
342
+ if ('A' === n || 'z' === n || 'b' === n || 'B' === n) {
343
+ if (inClass) {
344
+ return ['', '\\' + n + ' inside a character class, where the two' +
345
+ ' engines do not agree what it means', 0]
346
+ }
347
+ return ['A' === n ? '^' : 'z' === n ? '$' : '\\' + n, '', 0]
348
+ }
349
+
350
+ if ('-' === n) {
351
+ return inClass ? ['\\-', '', 0] :
352
+ ['', '\\- outside a character class: it is a range separator inside' +
353
+ ' one and a syntax error outside one (write a bare -)', 0]
354
+ }
355
+ if (RE_ESCAPE_PASS.includes(n) || RE_ESCAPE_PUNCT.includes(n)) {
356
+ return ['\\' + n, '', 0]
357
+ }
358
+ return ['', '\\' + n + ', an escape whose meaning the two engines do not' +
359
+ ' share', 0]
360
+ }
361
+
362
+
363
+ // normaliseRe rewrites a pattern into the engine-neutral subset.
364
+ // Returns [normalised, why]: a non-empty `why` means the pattern is
365
+ // outside the subset and names the construct.
366
+ function normaliseRe(src: string): [string, string] {
367
+ let inClass = false
368
+ const out: string[] = []
369
+
370
+ // One frame per open group, recording whether it contains a quantifier
371
+ // or an alternation. A group carrying either may not itself be
372
+ // quantified; containment is transitive, so a frame hands its flags up
373
+ // to its parent when it closes.
374
+ const groups: { q: boolean, alt: boolean }[] = []
375
+ const mark = (k: 'q' | 'alt') => {
376
+ if (0 < groups.length) {
377
+ groups[groups.length - 1][k] = true
378
+ }
379
+ }
380
+
381
+ // Where the counted quantifier validated below closes, so its own
382
+ // `}` is told apart from a stray one; and whether the atom just
383
+ // emitted was `^` or `$`, which cannot be quantified.
384
+ let repeatEnd = -1
385
+ let anchorPrev = false
386
+
387
+ for (let i = 0; i < src.length; i++) {
388
+ const c = src[i]
389
+ const afterAnchor = anchorPrev
390
+ anchorPrev = false
391
+
392
+ if ('\\' === c) {
393
+ const [emit, why, extra] = normaliseEscape(src[i + 1], src, i, inClass)
394
+ if ('' !== why) {
395
+ return ['', why]
396
+ }
397
+ out.push(emit)
398
+ // `\b` AND `\B` ARE ASSERTIONS TOO, and quantifying one is the
399
+ // same disagreement `^{1}` is: JavaScript under `u` calls it a
400
+ // syntax error, RE2 quantifies the assertion happily. The rule
401
+ // said "`\b` and `\B` quantify identically in both and are left
402
+ // alone" and that was measured wrong -- `re("\\b{1}x")` is
403
+ // `constraint_pattern` in TypeScript and an accepted schema in
404
+ // Go. Only OUTSIDE a class: inside one, `\b` is a backspace in
405
+ // both engines and repeats like any other character.
406
+ if (!inClass && ('b' === src[i + 1] || 'B' === src[i + 1])) {
407
+ anchorPrev = true
408
+ }
409
+ i += 1 + extra
410
+ continue
411
+ }
412
+
413
+ // A POSIX class opener, anywhere: the form lives inside an ordinary
414
+ // class (`[[:alpha:]]`), and refusing it everywhere is one rule
415
+ // rather than two.
416
+ if ('[' === c && ':' === src[i + 1]) {
417
+ return ['', 'a POSIX class ([:...:]), which JavaScript does not have']
418
+ }
419
+
420
+ if (inClass) {
421
+ if (']' === c) {
422
+ inClass = false
423
+ }
424
+ out.push(c)
425
+ continue
426
+ }
427
+
428
+ if ('[' === c) {
429
+ // `[]` is a never-matching class in JavaScript and a parse error in
430
+ // RE2; `[^]` is the same disagreement one character along.
431
+ const first = '^' === src[i + 1] ? src[i + 2] : src[i + 1]
432
+ if (']' === first) {
433
+ return ['', 'an empty character class, which RE2 refuses']
434
+ }
435
+ inClass = true
436
+ out.push(c)
437
+ continue
438
+ }
439
+
440
+ if ('.' === c) {
441
+ out.push('[^\\n]')
442
+ continue
443
+ }
444
+
445
+ if ('(' === c) {
446
+ if ('?' === src[i + 1]) {
447
+ if (':' !== src[i + 2]) {
448
+ return ['', 'a (?...) group other than the non-capturing (?:']
449
+ }
450
+ out.push('(?:')
451
+ i += 2
452
+ }
453
+ else {
454
+ out.push(c)
455
+ }
456
+ groups.push({ q: false, alt: false })
457
+ continue
458
+ }
459
+
460
+ if (')' === c) {
461
+ const g = groups.pop()
462
+ if (null == g) {
463
+ return ['', 'an unbalanced group']
464
+ }
465
+ const nx = src[i + 1]
466
+ const quantified = '*' === nx || '+' === nx || '?' === nx || '{' === nx
467
+ if (quantified && (g.q || g.alt)) {
468
+ return ['', 'a quantifier applied to a group containing ' +
469
+ (g.q ? 'another quantifier' : 'an alternation') +
470
+ ', which backtracks exponentially in JavaScript']
471
+ }
472
+ if (g.q) mark('q')
473
+ if (g.alt) mark('alt')
474
+ out.push(c)
475
+ continue
476
+ }
477
+
478
+ if ('|' === c) {
479
+ mark('alt')
480
+ out.push(c)
481
+ continue
482
+ }
483
+
484
+ if ('*' === c || '+' === c || '?' === c || '{' === c) {
485
+ // Nothing to repeat. JavaScript under the `u` flag makes this a
486
+ // SYNTAX ERROR, where RE2 quantifies the assertion happily and
487
+ // matches, so `re("^{1}")` was `constraint_pattern` in
488
+ // TypeScript and an accepted schema in Go. The four assertions
489
+ // behave alike here -- `^`, `$`, `\b` and `\B` -- so the rule
490
+ // names all four.
491
+ if (afterAnchor) {
492
+ return ['', 'a quantifier applied to `^`, `$`, `\\b` or ' +
493
+ '`\\B`, which has nothing to repeat']
494
+ }
495
+ if ('{' === c) {
496
+ const [why, end] = repeatWhy(src, i)
497
+ if ('' !== why) {
498
+ return ['', why]
499
+ }
500
+ repeatEnd = end
501
+ }
502
+ mark('q')
503
+ out.push(c)
504
+ continue
505
+ }
506
+
507
+ // A `}` that closes no counted quantifier: JavaScript under `u`
508
+ // refuses it as a lone quantifier bracket, RE2 reads it as a
509
+ // literal. Written escaped inside a class (`[}]`) it is a literal
510
+ // in both, which is the spelling that survives.
511
+ if ('}' === c) {
512
+ if (i !== repeatEnd) {
513
+ return ['', 'a `}` that closes no counted quantifier, which ' +
514
+ 'the two engines do not read the same way']
515
+ }
516
+ out.push(c)
517
+ continue
518
+ }
519
+
520
+ out.push(c)
521
+ anchorPrev = ('^' === c || '$' === c)
522
+ }
523
+
524
+ if (inClass) {
525
+ return ['', 'an unterminated character class']
526
+ }
527
+ if (0 < groups.length) {
528
+ return ['', 'an unclosed group']
529
+ }
530
+
531
+ return [out.join(''), '']
532
+ }
533
+
534
+
90
535
  // True for a scalar Val the algebra can order: a numeric leaf or a
91
536
  // string. (Booleans and null have no order and no bounds.)
92
537
  function numericLeaf(v: any): boolean {
@@ -128,6 +573,27 @@ function leafMarker(v: any): any {
128
573
  }
129
574
 
130
575
 
576
+ // Conjunct sort order for an atom that must see the WHOLE value.
577
+ // Every other value sorts below the container default (99999), so such
578
+ // an atom is the LAST term to fold: `a:length(2) a:{x:1} a:{y:2}` must
579
+ // count the MERGED map, and a constraint that folded at 50000 would
580
+ // count `{x:1}` alone and refuse the layering that is the whole point
581
+ // of the language.
582
+ //
583
+ // The order atoms keep the low slot: `min(2) & 1 & 2` may decide as
584
+ // soon as it sees a scalar, because meeting more scalars can only
585
+ // narrow. Meeting more containers GROWS the member set, which is why
586
+ // the two orders differ.
587
+ //
588
+ // Three atoms are late: `length` and `unique` (they count members), and
589
+ // `must` (an evaluate-only check against the finished value).
590
+ const LATE_CJO = 150000
591
+
592
+ function lateAtom(atom: string): boolean {
593
+ return 'length' === atom || 'unique' === atom || 'must' === atom
594
+ }
595
+
596
+
131
597
  class ConstraintVal extends FeatureVal {
132
598
  isConstraint = true
133
599
  cjo = 50000
@@ -137,7 +603,18 @@ class ConstraintVal extends FeatureVal {
137
603
  lo?: Bound
138
604
  hi?: Bound
139
605
  neqs: any[] = []
606
+ res: ReAtom[] = []
607
+ count?: ConstraintState
608
+ uniq = false
609
+ uniqBy: string[] = []
610
+ musts: MustAtom[] = []
611
+ // An atom whose arguments have not settled yet (G1 phase 4). Held
612
+ // until unify has a ctx to resolve them through; never present on a
613
+ // residual.
614
+ pending?: { atom: string, args: any[] }
615
+ clash?: boolean
140
616
  invalid?: string
617
+ invalidWhy?: string
141
618
 
142
619
  constructor(
143
620
  spec: ValSpec & { atom?: string, state?: ConstraintState },
@@ -151,39 +628,118 @@ class ConstraintVal extends FeatureVal {
151
628
  this.lo = spec.state.lo
152
629
  this.hi = spec.state.hi
153
630
  this.neqs = spec.state.neqs
631
+ // A state built by an embedder (or by a per-port test) may predate
632
+ // the pattern field; an absent one means "no patterns", not undefined.
633
+ this.res = spec.state.res ?? []
634
+ this.count = spec.state.count
635
+ this.uniq = spec.state.uniq ?? false
636
+ this.uniqBy = spec.state.uniqBy ?? []
637
+ this.musts = spec.state.musts ?? []
154
638
  this.invalid = spec.state.invalid
155
639
  }
156
640
  else if (spec.atom) {
157
- this.fromAtom(spec.atom, (spec.peg as any[]) ?? [])
641
+ const args = atomArgs(spec.atom, (spec.peg as any[]) ?? [])
642
+ // An argument that is not yet concrete — a reference, an
643
+ // arithmetic expression, a conjunct of atoms — makes the whole
644
+ // atom PENDING rather than invalid (G1 phase 4). It is resolved
645
+ // in unify, where there is a ctx to resolve through, and the
646
+ // residual is built from the settled arguments. Only a settled
647
+ // argument of the wrong shape is an `invalid-arg`.
648
+ // The effectful-argument refusal happens HERE, on the written
649
+ // form, and not in fromAtom: settling is what runs the effect, so
650
+ // by the time fromAtom sees a settled `move($.b)` the move has
651
+ // already happened and the argument is just its result.
652
+ if ('must' === spec.atom && args.some((a: any) => holdsMove(a))) {
653
+ this.invalid = 'invalid-arg'
654
+ }
655
+ else if (args.some((a: any) => true !== a?.done)) {
656
+ this.pending = { atom: spec.atom, args }
657
+ }
658
+ else {
659
+ this.fromAtom(spec.atom, args)
660
+ }
158
661
  }
159
662
 
160
- // A residual constraint is stable, like a ScalarKindVal.
161
- this.dc = DONE
663
+ if (null != this.count || this.uniq || 0 < this.uniqBy.length ||
664
+ 0 < this.musts.length ||
665
+ (null != this.pending && lateAtom(this.pending.atom))) {
666
+ this.cjo = LATE_CJO
667
+ }
668
+
669
+ // A residual constraint is stable, like a ScalarKindVal — but a
670
+ // pending atom is not a residual yet, and must be re-entered on
671
+ // later passes until its arguments settle.
672
+ if (null == this.pending) {
673
+ this.dc = DONE
674
+ }
675
+ else {
676
+ this.notdone()
677
+ }
162
678
  }
163
679
 
164
680
 
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).
168
- private fromAtom(atom: string, args: any[]) {
169
- // Mark the residual invalid and report so (a plain boolean, so no
170
- // void value is consumed by the callers' `return` statements).
171
- const bad = (why: string): boolean => {
681
+ // Normalise one atom call into state. Every argument here is already
682
+ // SETTLED — the constructor routes an unsettled one to `pending` —
683
+ // so a shape this cannot use is a genuine `invalid-arg`, not a
684
+ // not-yet.
685
+ private fromAtom(atom: string, args: any[]): void {
686
+ // Mark the residual invalid. It answers NOTHING: every caller
687
+ // spells its exit `return bad(...)` and none reads a value, so a
688
+ // return type would be a value nobody consumes -- and a routine
689
+ // that answers on some paths and not others is the shape a reader
690
+ // (and a static analyser) has to stop and check.
691
+ const bad = (why: string): void => {
172
692
  this.invalid = why
173
- return true
174
693
  }
175
694
 
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]
695
+ // `unique()` is a property of the container -- not a comparison
696
+ // against a value -- so with no argument it says the members are
697
+ // pairwise DISTINCT. THE ONE ARGUMENT IS A PROJECTOR (the review's
698
+ // finding I: "unique()-by-field is reserved but absent", and the
699
+ // arity was reserved for exactly this): `unique(port)` says no two
700
+ // members share a `port`, which is how "no two services share a
701
+ // port" and "event ids are unique" are said.
702
+ if ('unique' === atom) {
703
+ if (0 === args.length) {
704
+ this.uniq = true
705
+ return
183
706
  }
184
- else if (1 === args.length && true === (args[0] as any)?.isList) {
185
- args = (args[0] as any).peg
707
+ if (1 !== args.length || !stringLeaf(args[0])) {
708
+ return bad('invalid-arg')
186
709
  }
710
+ this.uniqBy = [args[0].peg]
711
+ return
712
+ }
713
+
714
+ // `must(c, msg)` is Band B: an evaluate-only check against the
715
+ // finished value, carrying the author's own message. It is KEPT,
716
+ // never simplified and never consulted for emptiness or
717
+ // subsumption — Band B is opaque by construction, which is exactly
718
+ // what makes it the honest channel for a rule the algebra cannot
719
+ // reason about (docs/reference-language.md, "Band B: `must`").
720
+ if ('must' === atom) {
721
+ if (2 !== args.length) {
722
+ return bad('arg')
723
+ }
724
+ if (!stringLeaf(args[1])) {
725
+ return bad('invalid-arg')
726
+ }
727
+ // A check carrying a nil can never be satisfied, so it is refused
728
+ // as an ARGUMENT rather than left to fail against every value with
729
+ // the author's message attached -- which would blame the data for
730
+ // a mistake in the check. `must([1-x],m)` is the reachable case: a
731
+ // degenerate expression leaves a nil inside the written list.
732
+ if (holdsNil(args[0])) {
733
+ return bad('invalid-arg')
734
+ }
735
+ // (An effectful argument is refused at construction, in the
736
+ // constructor: by the time this arm sees a settled `move($.b)`
737
+ // the move has already run.)
738
+ this.musts = [{ v: args[0], msg: args[1] }]
739
+ return
740
+ }
741
+
742
+ if ('neq' === atom) {
187
743
  if (0 === args.length) {
188
744
  return bad('arg')
189
745
  }
@@ -204,6 +760,74 @@ class ConstraintVal extends FeatureVal {
204
760
  return bad('arg')
205
761
  }
206
762
  const a = args[0]
763
+
764
+ // `re` is the one atom whose argument is not an ORDER point: a
765
+ // pattern is a membership test, so it takes the string domain
766
+ // outright rather than inferring a domain from the argument's leaf.
767
+ if ('re' === atom) {
768
+ if (!stringLeaf(a)) {
769
+ return bad('invalid-arg')
770
+ }
771
+ const src = a.peg as string
772
+ // Normalise BEFORE compiling: the host engines only ever see a
773
+ // pattern that cannot be read two ways (ADR-003).
774
+ const [norm, why] = normaliseRe(src)
775
+ if ('' !== why) {
776
+ this.invalidWhy = why
777
+ return bad('constraint_pattern')
778
+ }
779
+ let re: RegExp
780
+ try {
781
+ // The `u` flag is REQUIRED for parity, not an optimisation.
782
+ // Without it JavaScript matches UTF-16 code units while RE2
783
+ // matches code points, so `re("^.$")` accepted U+1D11E in Go
784
+ // and refused it in TypeScript (and `^..$` did the reverse).
785
+ // With it, `.` and every quantifier count code points in both.
786
+ // It also makes JavaScript refuse the identity escapes this
787
+ // scanner rejects by hand, which is defence in depth rather
788
+ // than a substitute: RE2 accepts some of them, so the scanner
789
+ // is what keeps the two ports agreeing.
790
+ re = new RegExp(norm, 'u')
791
+ }
792
+ catch (e: any) {
793
+ // The host engine refuses what the subset scanner passed — a
794
+ // malformed quantifier, an unbalanced group. Same refusal under
795
+ // the same code: the author gets one rule, not two. The message
796
+ // is the host's, so it is NOT pinned by a shared row; the code
797
+ // and the located frame are.
798
+ this.invalidWhy = 'not a valid pattern'
799
+ return bad('constraint_pattern')
800
+ }
801
+ this.domain = 'string'
802
+ this.res = [{ v: a, src, norm, re }]
803
+ return
804
+ }
805
+
806
+ // `length` is the other non-ORDER atom: its argument constrains the
807
+ // COUNT, not the value, so it is itself a residual over the integer
808
+ // domain (docs/reference-language.md, "`length` semantics"). It is
809
+ // resolved HERE, at construction, by walking the written argument
810
+ // rather than by unifying it: the func-paren handler builds atoms
811
+ // without an AontuContext, so there is nothing to fold a nested
812
+ // conjunct through. Walking is enough because a length argument is by
813
+ // definition a meet of concrete Band A atoms; anything else (a
814
+ // reference, an expression) is refused rather than deferred, the
815
+ // same discipline min/max apply to their own arguments.
816
+ if ('length' === atom) {
817
+ const arg = countArgState(a)
818
+ if (null == arg) {
819
+ return bad('invalid-arg')
820
+ }
821
+ const inner = meetCount(countBase(), arg)
822
+ this.count = inner
823
+ // `length(min(5)&max(3))` is unsatisfiable with no peer in sight, so
824
+ // it is refused at composition time like any other empty meet.
825
+ if (stateEmpty(inner)) {
826
+ return bad('constraint')
827
+ }
828
+ return
829
+ }
830
+
207
831
  const domain = numericLeaf(a) ? 'number' : stringLeaf(a) ? 'string' : undefined
208
832
  if (null == domain) {
209
833
  return bad('invalid-arg')
@@ -228,8 +852,12 @@ class ConstraintVal extends FeatureVal {
228
852
  // residual is stable and the ladder is total.
229
853
  let out: Val
230
854
 
231
- if (null != this.invalid) {
232
- out = makeNilErr(ctx, this.invalid, this, undefined, 'constrain')
855
+ if (null != this.pending) {
856
+ out = this.settle(peer, ctx)
857
+ }
858
+ else if (null != this.invalid) {
859
+ out = makeNilErr(ctx, this.invalid, this, undefined, 'constrain',
860
+ null == this.invalidWhy ? undefined : { reason: this.invalidWhy })
233
861
  }
234
862
  else if (null == peer || (peer as any).isTop) {
235
863
  out = this
@@ -246,9 +874,20 @@ class ConstraintVal extends FeatureVal {
246
874
  else if ((peer as any).isScalar) {
247
875
  out = this.admit(peer, ctx)
248
876
  }
877
+ else if ((peer as any).isMap || (peer as any).isList) {
878
+ out = this.admitContainer(peer, ctx)
879
+ }
880
+ /* node:coverage ignore next 12 */
249
881
  else {
250
- // Maps, lists, and every other non-scalar shape: no order, no
251
- // membership — a conflict of the constraint family.
882
+ // Every other shape: no order, no membership, nothing to count —
883
+ // a conflict of the constraint family. The ladder above is total
884
+ // in practice: every remaining Val kind either sorts BELOW a
885
+ // constraint in a conjunct (conjunct, disjunct, pref, ref) and so
886
+ // drives the meet from its own side, or resolves to a
887
+ // scalar/container before a constraint sees it (func, op, var,
888
+ // expect). The arm is kept because "in practice" depends on the
889
+ // cjo table, and a future value class would land here rather than
890
+ // falling out of unify with no result.
252
891
  out = this.fail(ctx, peer)
253
892
  }
254
893
 
@@ -258,61 +897,374 @@ class ConstraintVal extends FeatureVal {
258
897
  }
259
898
 
260
899
 
900
+ // Resolve a pending atom's arguments and, once they have all settled,
901
+ // become the residual they describe (G1 phase 4).
902
+ //
903
+ // This is the residuation discipline FuncBaseVal already follows for
904
+ // its own operands: push each unsettled argument one step by unifying
905
+ // it with `top`, and if any is still moving, mark not-done and defer
906
+ // — either as this same pending atom (against a `top` peer) or
907
+ // wrapped with the peer in a conjunct the next pass will re-enter.
908
+ // Nothing is decided from a half-resolved argument, which is what
909
+ // keeps `min($.lo)` from reporting a conflict against a bound that
910
+ // has not arrived yet.
911
+ private settle(peer: Val, ctx: AontuContext): Val {
912
+ const TOP = top()
913
+ const pend = this.pending as { atom: string, args: any[] }
914
+
915
+ let settled = true
916
+ const args: any[] = []
917
+ for (const arg of pend.args) {
918
+ let next = arg
919
+ if (true !== arg?.done) {
920
+ // Charged to the depth budget: this recurses without going
921
+ // through `unite`, so the counter would otherwise stay flat
922
+ // while the stack grows (the rule FuncBaseVal follows).
923
+ next = withDepth(ctx, arg, TOP, () => arg.unify(TOP, ctx))
924
+ }
925
+ settled = settled && true === next?.done
926
+ args.push(next)
927
+ }
928
+
929
+ if (settled) {
930
+ // Build the residual the atom always meant, at this atom's site,
931
+ // then let the ordinary ladder meet it with the peer.
932
+ const built = new ConstraintVal({ peg: args, atom: pend.atom }, ctx)
933
+ built.path = this.path
934
+ built.site.row = this.site.row
935
+ built.site.col = this.site.col
936
+ built.site.url = this.site.url
937
+ propagateMarks(this, built)
938
+ return built.unify(peer, ctx)
939
+ }
940
+
941
+ this.notdone()
942
+
943
+ // A fresh pending atom carrying the partially-resolved arguments, so
944
+ // the next pass starts from the progress this one made rather than
945
+ // re-resolving from source.
946
+ const again = new ConstraintVal({ peg: args, atom: pend.atom }, ctx)
947
+ again.path = this.path
948
+ again.site.row = this.site.row
949
+ again.site.col = this.site.col
950
+ again.site.url = this.site.url
951
+ propagateMarks(this, again)
952
+
953
+ if (null == peer || (peer as any).isTop) {
954
+ return again
955
+ }
956
+ // No nil-peer arm: `unite` returns a nil operand before dispatching
957
+ // to any Val's unify (ts/src/unify.ts), so a nil never reaches here
958
+ // — and were one to, the conjunct below folds to it unchanged.
959
+ return new ConjunctVal({ peg: [again, peer] }, ctx)
960
+ }
961
+
962
+
261
963
  // Membership: the peer scalar passes every part of the residual, or
262
964
  // the whole meet is a located conflict.
263
965
  private admit(peer: any, ctx: AontuContext): Val {
264
- const domainOf = numericLeaf(peer) ? 'number' :
265
- stringLeaf(peer) ? 'string' : undefined
266
-
267
- if (domainOf !== this.domain) {
966
+ // No scalar has members, so a `unique()` residual admits none --
967
+ // and neither does a `unique(k)` one, for the same reason.
968
+ if (this.uniq || 0 < this.uniqBy.length) {
268
969
  return this.fail(ctx, peer)
269
970
  }
270
- if (null != this.kind && leafMarker(peer) !== this.kind) {
971
+ if (!stateAdmits(this, peer)) {
271
972
  return this.fail(ctx, peer)
272
973
  }
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)) {
974
+ if (null != this.count) {
975
+ // Only a string among the scalars has a length, and it is counted
976
+ // in CODE POINTS -- not UTF-16 units (this host's native count)
977
+ // and not bytes (Go's). Iterating a string yields code points.
978
+ if (!stringLeaf(peer)) {
277
979
  return this.fail(ctx, peer)
278
980
  }
279
- }
280
- if (null != this.hi) {
281
- const c = cmpVal(d, peer, this.hi.v)
282
- if (c > 0 || (0 === c && this.hi.open)) {
981
+ if (!stateAdmits(this.count, countVal([...peer.peg].length))) {
283
982
  return this.fail(ctx, peer)
284
983
  }
285
984
  }
286
- for (const n of this.neqs) {
287
- if (sameScalar(peer, n)) {
985
+ // A SCALAR HAS NO MEMBERS to accumulate, so its musts are decided
986
+ // here and never residuate: the final reading is the only reading
987
+ // a scalar has.
988
+ const bad = this.checkMusts(peer, ctx, true)
989
+ if (null != bad) {
990
+ return bad
991
+ }
992
+ return peer
993
+ }
994
+
995
+
996
+ // Band B, applied to a finished value. Each check is a plain
997
+ // unification against a CLONE of the peer: `must` reports, it never
998
+ // contributes: whatever the check would have added to the value is
999
+ // discarded, and `peer` is returned untouched by the callers.
1000
+ //
1001
+ // The trial runs in an isolated collect context so a failing check
1002
+ // leaves nothing on the caller's error list — only the located nil
1003
+ // this returns, carrying the author's own message.
1004
+ // `final` is the generation-time reading (see settleContainer): a
1005
+ // must whose own check is a SIZING atom inherits that atom's
1006
+ // provisionality, because `must(length(2), …)` against a map that may
1007
+ // still gain members has not failed yet -- it has not been answered.
1008
+ // Without this the must would decide against whatever the container
1009
+ // held when it first settled, which is the very defect the sizing
1010
+ // atoms were fixed for (use-cases/BUGS.md §16, §17).
1011
+ private checkMusts(
1012
+ peer: any, ctx: AontuContext, final?: boolean): Val | undefined {
1013
+ for (const m of this.musts) {
1014
+ const trial = ctx.clone({ err: [], collect: true })
1015
+ let got: any = unite(trial, m.v.clone(trial), peer.clone(trial), 'must')
1016
+ // A must whose check is a SIZING atom answers a residue rather
1017
+ // than a verdict, because the atom is waiting for members that
1018
+ // may still arrive. Before generation that residue IS the answer
1019
+ // -- the must has not failed, it has not been asked yet -- and at
1020
+ // generation it is settled, because nothing more can arrive.
1021
+ const residue = sizingResidue(got)
1022
+ if (undefined !== residue) {
1023
+ if (true !== final) {
1024
+ continue
1025
+ }
1026
+ got = residue.con.settleContainer(residue.bag, trial)
1027
+ }
1028
+ if (true === (got as any)?.isNil || 0 < trial.err.length) {
1029
+ return makeNilErr(ctx, 'must', this, peer, undefined, {
1030
+ message: m.msg.peg,
1031
+ expected: m.v.canon,
1032
+ actual: peer.canon,
1033
+ })
1034
+ }
1035
+ }
1036
+ return undefined
1037
+ }
1038
+
1039
+
1040
+ // THE FINAL READING of a sizing atom over a container: the same
1041
+ // checks admitContainer makes, with the provisional ones taken as
1042
+ // decided. Called from ConjunctVal.gen, which is where no more
1043
+ // members can arrive. Returns the container (to generate) or the
1044
+ // constraint's own nil (to report).
1045
+ settleContainer(peer: any, ctx: AontuContext): Val {
1046
+ return this.admitContainer(peer, ctx, true)
1047
+ }
1048
+
1049
+
1050
+ // Membership for a container peer. Only the SIZING atoms have anything
1051
+ // to say about a map or a list; every other atom is scalar-domain and
1052
+ // refuses one.
1053
+ //
1054
+ // The members that count are the members that GENERATE
1055
+ // (docs/reference-language.md, "`length` semantics"), and rather than
1056
+ // mirror generation's filter — type/hide marks, optional keys that
1057
+ // drop, empty optional values — this asks generation itself, in an
1058
+ // isolated collect context so nothing leaks into the caller's errors.
1059
+ // A mirror would be a second copy of a filter that has already grown
1060
+ // subtle, free to drift from it; asking is correct by construction.
1061
+ private admitContainer(
1062
+ peer: any, ctx: AontuContext, final?: boolean): Val {
1063
+ // A scalar-domain residual has no reading over a container.
1064
+ if (null != this.domain) {
1065
+ return this.fail(ctx, peer)
1066
+ }
1067
+ // Not yet settled: the container, or an optional child, may still
1068
+ // resolve, so the member set is not final. Defer rather than decide
1069
+ // — the same discipline OpBaseVal follows for a non-concrete operand.
1070
+ if (!containerSettled(peer)) {
1071
+ this.dc = 0
1072
+ return new ConjunctVal({ peg: [this, peer] }, ctx)
1073
+ }
1074
+
1075
+ const bad = this.checkMusts(peer, ctx, final)
1076
+ if (null != bad) {
1077
+ return bad
1078
+ }
1079
+
1080
+ // A MUST OVER A CONTAINER RESIDUATES with the atoms, for the same
1081
+ // reason (use-cases/BUGS.md §17): `must({t: max(60)}, …)` beside a
1082
+ // `{t: integer}` schema was answered against the schema layer
1083
+ // ALONE, and discharged before the data it was written to judge
1084
+ // ever arrived. It is decided at generation, where the value is
1085
+ // whole -- unless there is nothing else on the constraint AND the
1086
+ // reading is already final.
1087
+ if (!this.uniq && 0 === this.uniqBy.length && null == this.count) {
1088
+ if (true === final || 0 === this.musts.length) {
1089
+ return peer
1090
+ }
1091
+ return this.hold(peer, ctx)
1092
+ }
1093
+
1094
+ const members = emittedMembers(peer, ctx)
1095
+ if (null == members) {
1096
+ // NOT COUNTABLE YET IS NOT A PASS. A container that cannot
1097
+ // generate cannot be counted, and a SCHEMA is exactly that: the
1098
+ // members of `{a: integer}` are types, so nothing is emitted and
1099
+ // nothing can be counted. Discharging the atom here was the §16
1100
+ // defect wearing its other face -- `length(min(2)) & {a: integer}`
1101
+ // dropped its bound while still alone, so the data half of a
1102
+ // `vet` meet was never measured at all and short data vetted
1103
+ // clean against a bound the evaluator enforces.
1104
+ //
1105
+ // At generation the reading IS final: a container that still
1106
+ // cannot generate has its own error, and that error is the one
1107
+ // worth reporting, so it passes through untouched.
1108
+ if (true === final) {
1109
+ return peer
1110
+ }
1111
+ return this.hold(peer, ctx)
1112
+ }
1113
+
1114
+ // MONOTONE READINGS ONLY (the review's finding C, use-cases/BUGS.md
1115
+ // §16). Members ACCUMULATE under unification -- a meet adds keys and
1116
+ // never removes them -- so a settled container is settled ON ITS
1117
+ // OWN and not against every value that may still contribute. The
1118
+ // schema half of a `vet` meet, a document that is about to receive
1119
+ // an `@` include, a bag a later `pack` will fill: each settles, and
1120
+ // an atom that decided there decided too early.
1121
+ //
1122
+ // So a verdict is taken only when MORE MEMBERS CANNOT CHANGE IT:
1123
+ // - an upper bound VIOLATED is permanent -> refuse now;
1124
+ // satisfied is provisional -> keep the atom, re-check later.
1125
+ // - a lower bound SATISFIED is permanent -> that part may go;
1126
+ // violated is provisional -> keep the atom (which is why
1127
+ // `length(min(1))` beside a template no longer kills the schema
1128
+ // it was written for).
1129
+ // - a DUPLICATE is permanent -> refuse now; distinctness is
1130
+ // provisional -> keep the atom.
1131
+ // Anything provisional residuates, exactly as an unsettled
1132
+ // container does above, and the two spellings then agree.
1133
+ const count = null == this.count ? undefined : this.count
1134
+ const n = members.length
1135
+ if (null != count) {
1136
+ if (null != count.hi && !stateAdmits({ ...count, lo: undefined },
1137
+ countVal(n))) {
1138
+ return this.fail(ctx, peer)
1139
+ }
1140
+ if (0 < count.neqs.length && !stateAdmits(
1141
+ { ...count, lo: undefined, hi: undefined }, countVal(n))) {
1142
+ return this.fail(ctx, peer)
1143
+ }
1144
+ // The provisional half, decided only when nothing more can
1145
+ // arrive: a lower bound still short is a refusal at generation
1146
+ // and a residue before it.
1147
+ if (true === final && !stateAdmits(count, countVal(n))) {
288
1148
  return this.fail(ctx, peer)
289
1149
  }
290
1150
  }
291
- return peer
1151
+
1152
+ if (this.uniq) {
1153
+ // Members compare by CANONICAL FORM, which reduces to scalar
1154
+ // identity for scalars (so [1, 1.0] is distinct) and gives
1155
+ // structural equality for container members without a second
1156
+ // rule. A generated value could not express the first: `1` and
1157
+ // `1.0` generate the same JSON number.
1158
+ const seen = new Set<string>()
1159
+ for (const m of members) {
1160
+ const key = m.canon
1161
+ if (seen.has(key)) {
1162
+ return this.fail(ctx, peer)
1163
+ }
1164
+ seen.add(key)
1165
+ }
1166
+ }
1167
+
1168
+ // ... and the same test per PROJECTED key. A member that has no such
1169
+ // key FAILS the constraint rather than being skipped: distinctness
1170
+ // it cannot be shown to have is distinctness it does not have, and
1171
+ // skipping would let one keyless record hide a duplicate.
1172
+ for (const field of this.uniqBy) {
1173
+ const seen = new Set<string>()
1174
+ for (const m of members) {
1175
+ const at: any = true === (m as any).isMap ?
1176
+ (m as any).peg[field] : undefined
1177
+ if (null == at) {
1178
+ return this.fail(ctx, peer)
1179
+ }
1180
+ const key = at.canon
1181
+ if (seen.has(key)) {
1182
+ return this.fail(ctx, peer)
1183
+ }
1184
+ seen.add(key)
1185
+ }
1186
+ }
1187
+
1188
+ // WHAT IS LEFT IS PROVISIONAL, so the atom stays on the value. A
1189
+ // lower bound already met is the one reading that cannot be undone,
1190
+ // and an atom holding nothing else is spent: that is when it goes.
1191
+ const spent = true === final || (0 === this.musts.length &&
1192
+ !this.uniq && 0 === this.uniqBy.length &&
1193
+ (null == count ||
1194
+ (null == count.hi && 0 === count.neqs.length &&
1195
+ stateAdmits(count, countVal(n)))))
1196
+ if (spent) {
1197
+ return peer
1198
+ }
1199
+ // DONE, unlike the unsettled case above. The container HAS settled;
1200
+ // the atom is kept only because a LATER value could still add
1201
+ // members, and a residue that reported itself unresolved would
1202
+ // leave every enclosing value unresolved with it -- a `type()`
1203
+ // waiting on its argument for ever, and use case 09's whole
1204
+ // registry with it.
1205
+ // The ATOM is done too, not just the conjunct. An earlier pass may
1206
+ // have set `dc = 0` on the unsettled branch above, and a conjunct
1207
+ // recomputes its own doneness from its terms on every later meet --
1208
+ // so a stale 0 here would drag the residue, and every value holding
1209
+ // it, back to unresolved for ever.
1210
+ return this.hold(peer, ctx)
1211
+ }
1212
+
1213
+
1214
+ // The atom kept on a value whose reading is still provisional.
1215
+ // DONE, unlike the unsettled container above: the value HAS settled;
1216
+ // the atom is kept only because a LATER value could still add
1217
+ // members, and a residue that reported itself unresolved would leave
1218
+ // every enclosing value unresolved with it -- a `type()` waiting on
1219
+ // its argument for ever, and use case 09's whole registry with it.
1220
+ // The ATOM is done too, not just the conjunct. An earlier pass may
1221
+ // have set `dc = 0` on the unsettled branch, and a conjunct
1222
+ // recomputes its own doneness from its terms on every later meet --
1223
+ // so a stale 0 here would drag the residue, and every value holding
1224
+ // it, back to unresolved for ever.
1225
+ private hold(peer: any, ctx: AontuContext): Val {
1226
+ this.dc = DONE
1227
+ const held: any = new ConjunctVal({ peg: [this, peer] }, ctx)
1228
+ held.dc = DONE
1229
+ return held
292
1230
  }
293
1231
 
294
1232
 
295
1233
  // 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.
1234
+ // already implied by an ORDER atom's argument; a numeric LEAF narrows
1235
+ // the residual; anything else has an empty intersection with the
1236
+ // constraint's domain.
1237
+ //
1238
+ // A sizing residual (`length`, `unique`) has no domain of its own -- a
1239
+ // count says nothing about what is counted -- so a kind here SETS one
1240
+ // rather than merely agreeing with it: `string & length(3)` is a
1241
+ // three-character string, and `number & length(3)` is empty because a
1242
+ // number has no length (stateEmpty decides that, not this).
298
1243
  private meetKind(peer: any, ctx: AontuContext): Val {
299
1244
  const marker = peer.peg
1245
+ const merged = this.cloneState()
300
1246
 
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)
1247
+ if (Number === marker || String === marker) {
1248
+ const d = Number === marker ? 'number' : 'string'
1249
+ if (d === this.domain) {
1250
+ return this
1251
+ }
1252
+ if (null != this.domain) {
1253
+ return this.fail(ctx, peer)
1254
+ }
1255
+ merged.domain = d
1256
+ return this.finish(merged, ctx, peer)
306
1257
  }
1258
+
307
1259
  const isLeaf = Integer === marker || Float === marker ||
308
1260
  BigInteger === marker || BigDecimal === marker
309
- if (!isLeaf || 'number' !== this.domain) {
1261
+ if (!isLeaf || 'string' === this.domain) {
310
1262
  return this.fail(ctx, peer)
311
1263
  }
312
1264
  if (null != this.kind && this.kind !== marker) {
313
1265
  return this.fail(ctx, peer)
314
1266
  }
315
- const merged = this.cloneState()
1267
+ merged.domain = 'number'
316
1268
  merged.kind = marker
317
1269
  return this.finish(merged, ctx, peer)
318
1270
  }
@@ -322,7 +1274,8 @@ class ConstraintVal extends FeatureVal {
322
1274
  // union, kind union — then the eager emptiness rules.
323
1275
  private meetConstraint(peer: ConstraintVal, ctx: AontuContext): Val {
324
1276
  if (null != peer.invalid) {
325
- return makeNilErr(ctx, peer.invalid, peer, undefined, 'constrain')
1277
+ return makeNilErr(ctx, peer.invalid, peer, undefined, 'constrain',
1278
+ null == peer.invalidWhy ? undefined : { reason: peer.invalidWhy })
326
1279
  }
327
1280
  if (null != this.domain && null != peer.domain && this.domain !== peer.domain) {
328
1281
  return this.fail(ctx, peer)
@@ -338,6 +1291,23 @@ class ConstraintVal extends FeatureVal {
338
1291
  merged.lo = tighter(d, this.lo, peer.lo, true)
339
1292
  merged.hi = tighter(d, this.hi, peer.hi, false)
340
1293
  merged.neqs = dedupSorted(d, [...this.neqs, ...peer.neqs])
1294
+ merged.res = dedupSortedRes([...this.res, ...peer.res])
1295
+ // `length(c1) & length(c2)` is `length(c1 & c2)`: the count atom reuses
1296
+ // numeric algebra recursively, over the counts rather than the
1297
+ // values.
1298
+ merged.count = null == this.count ? peer.count :
1299
+ null == peer.count ? this.count : meetCount(this.count, peer.count)
1300
+ // `unique()` is idempotent: two of them are one.
1301
+ merged.uniq = this.uniq || peer.uniq
1302
+ // `unique(a) & unique(b)` is BOTH, not the later one: each names a
1303
+ // key on which the members must differ, and dropping either would
1304
+ // silently weaken the constraint. Sorted and deduplicated, so the
1305
+ // meet is commutative and `unique(a) & unique(a)` is one atom.
1306
+ merged.uniqBy = [...new Set([...this.uniqBy, ...peer.uniqBy])].sort()
1307
+ // Band B checks accumulate in written order and are never merged,
1308
+ // deduplicated or reordered: each carries its own author message,
1309
+ // and two checks with the same shape may still say different things.
1310
+ merged.musts = [...this.musts, ...peer.musts]
341
1311
 
342
1312
  return this.finish(merged, ctx, peer)
343
1313
  }
@@ -345,51 +1315,8 @@ class ConstraintVal extends FeatureVal {
345
1315
 
346
1316
  // Build the merged residual, applying the eager emptiness rules.
347
1317
  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
- }
1318
+ if (stateEmpty(state)) {
1319
+ return this.fail(ctx, peer)
393
1320
  }
394
1321
 
395
1322
  const out = new ConstraintVal({ peg: [], state }, ctx)
@@ -422,6 +1349,11 @@ class ConstraintVal extends FeatureVal {
422
1349
  lo: this.lo,
423
1350
  hi: this.hi,
424
1351
  neqs: [...this.neqs],
1352
+ res: [...this.res],
1353
+ count: this.count,
1354
+ uniq: this.uniq,
1355
+ uniqBy: [...this.uniqBy],
1356
+ musts: [...this.musts],
425
1357
  invalid: this.invalid,
426
1358
  }
427
1359
  }
@@ -437,33 +1369,30 @@ class ConstraintVal extends FeatureVal {
437
1369
  out.lo = this.lo
438
1370
  out.hi = this.hi
439
1371
  out.neqs = [...this.neqs]
1372
+ out.res = [...this.res]
1373
+ out.count = this.count
1374
+ out.uniq = this.uniq
1375
+ out.uniqBy = [...this.uniqBy]
1376
+ out.musts = [...this.musts]
1377
+ out.pending = this.pending
1378
+ out.cjo = this.cjo
440
1379
  out.invalid = this.invalid
1380
+ out.invalidWhy = this.invalidWhy
441
1381
  return out
442
1382
  }
443
1383
 
444
1384
 
445
1385
  // 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.
1386
+ // sorted), re, length, unique. No spaces; reparses to a conjunct that
1387
+ // normalises back to this exact residual.
448
1388
  get canon() {
449
- const parts: string[] = []
450
- if (null != this.kind) {
451
- parts.push((this.kind as any).name.toLowerCase())
1389
+ if (null != this.pending) {
1390
+ // A pending atom has no residual yet, so canon renders the call as
1391
+ // written — the same shape FuncBaseVal renders while deferring.
1392
+ return this.pending.atom +
1393
+ '(' + this.pending.args.map((a: any) => a.canon).join(',') + ')'
452
1394
  }
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(',') + ')')
461
- }
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('&')
1395
+ return canonState(this)
467
1396
  }
468
1397
 
469
1398
 
@@ -517,7 +1446,624 @@ function dedupSorted(domain: 'number' | 'string', neqs: any[]): any[] {
517
1446
  }
518
1447
 
519
1448
 
520
- // The five atom classes registered in the parser's funcMap: each is a
1449
+ // Is this value, or anything inside it, a nil? A written argument that
1450
+ // holds one can never be satisfied, so the atom refuses it as an
1451
+ // argument rather than reporting a mystery failure against every peer.
1452
+ // Every caller passes a Val: `args[0]` comes from atomArgs, and the
1453
+ // walk below descends only into container pegs, which hold Vals. The
1454
+ // null/non-Val guard this function used to open with was therefore
1455
+ // unreachable, and the coverage gate said so.
1456
+ function holdsNil(v: any): boolean {
1457
+ if (true === v.isNil) {
1458
+ return true
1459
+ }
1460
+ const peg = v.peg
1461
+ if (Array.isArray(peg)) {
1462
+ return peg.some((c: any) => holdsNil(c))
1463
+ }
1464
+ if (null != peg && 'object' === typeof peg) {
1465
+ for (const k in peg) {
1466
+ if (holdsNil(peg[k])) {
1467
+ return true
1468
+ }
1469
+ }
1470
+ }
1471
+ return false
1472
+ }
1473
+
1474
+
1475
+ // Whether a value, or anything inside it, is an effectful call — one
1476
+ // whose evaluation changes a node OTHER than the one being computed.
1477
+ // `move()` is the only builtin that does: it hides its resolution
1478
+ // target in place. Band B refuses one as an argument, because settling
1479
+ // it runs the effect against the live root before `must`'s trial clone
1480
+ // is taken, and a check that mutates cannot be report-only.
1481
+ function holdsMove(v: any): boolean {
1482
+ if (true === v.isFunc && 'move' === v.funcname?.()) {
1483
+ return true
1484
+ }
1485
+ const peg = v.peg
1486
+ if (Array.isArray(peg)) {
1487
+ return peg.some((c: any) => holdsMove(c))
1488
+ }
1489
+ if (null != peg && 'object' === typeof peg) {
1490
+ for (const k in peg) {
1491
+ if (holdsMove(peg[k])) {
1492
+ return true
1493
+ }
1494
+ }
1495
+ }
1496
+ return false
1497
+ }
1498
+
1499
+
1500
+ // The written arguments of an atom, flattened.
1501
+ //
1502
+ // A multi-argument call arrives from the func-paren grammar as ONE
1503
+ // entry holding the comma group, and `neq([3,1,2])` means the same as
1504
+ // `neq(3,1,2)`. The group is always a ListVal by the time it reaches
1505
+ // here -- the func-paren handler rawToVals every argument (issue #49) --
1506
+ // so there is no raw-array case to unwrap. Flattening happens before
1507
+ // the settled check, because an unsettled member hiding inside the
1508
+ // group would otherwise make the atom look ready.
1509
+ function atomArgs(atom: string, args: any[]): any[] {
1510
+ if (('neq' === atom || 'must' === atom) && 1 === args.length &&
1511
+ true === (args[0] as any)?.isList) {
1512
+ return (args[0] as any).peg
1513
+ }
1514
+ return args
1515
+ }
1516
+
1517
+
1518
+ // The canonical rendering of a residual, in the fixed atom order:
1519
+ // kind, lower bound, upper bound, neq, re, length, unique. Taken over the
1520
+ // STATE rather than the Val because `length`'s argument is a residual too,
1521
+ // and renders by exactly the same rules.
1522
+ function canonState(s: ConstraintState): string {
1523
+ const parts: string[] = []
1524
+ if (null != s.kind) {
1525
+ parts.push((s.kind as any).name.toLowerCase())
1526
+ }
1527
+ // An ORDER atom's argument implies the domain, so it is not spelled
1528
+ // out. A SIZING residual carries no order, and there `string` is the
1529
+ // only thing saying what is being sized -- drop it and the reparse
1530
+ // would admit lists and maps too.
1531
+ else if ('string' === s.domain &&
1532
+ null == s.lo && null == s.hi && 0 === s.neqs.length && 0 === s.res.length) {
1533
+ parts.push('string')
1534
+ }
1535
+ if (null != s.lo) {
1536
+ parts.push((s.lo.open ? 'above(' : 'min(') + s.lo.v.canon + ')')
1537
+ }
1538
+ if (null != s.hi) {
1539
+ parts.push((s.hi.open ? 'below(' : 'max(') + s.hi.v.canon + ')')
1540
+ }
1541
+ if (0 < s.neqs.length) {
1542
+ parts.push('neq(' + s.neqs.map((n: any) => n.canon).join(',') + ')')
1543
+ }
1544
+ for (const r of s.res) {
1545
+ parts.push('re(' + r.v.canon + ')')
1546
+ }
1547
+ if (null != s.count) {
1548
+ // Rendered UNABRIDGED, implied parts and all: `length(3)` canonicalises
1549
+ // to `length(integer&min(3)&max(3))` because that IS the residual the
1550
+ // count must satisfy, and canon is a normal form (G6 hashes it),
1551
+ // not a pretty-printer. Abbreviating would mean a second set of
1552
+ // rules for when the implied `integer & min(0)` may be dropped.
1553
+ parts.push('length(' + canonState(s.count) + ')')
1554
+ }
1555
+ if (s.uniq) {
1556
+ parts.push('unique()')
1557
+ }
1558
+ // After the bare atom, and sorted: canon is a normal form, so two
1559
+ // documents writing the same keys in different orders must render
1560
+ // the same string.
1561
+ for (const key of s.uniqBy) {
1562
+ parts.push('unique(' + JSON.stringify(key) + ')')
1563
+ }
1564
+ for (const m of s.musts) {
1565
+ parts.push('must(' + m.v.canon + ',' + m.msg.canon + ')')
1566
+ }
1567
+ if (0 === parts.length) {
1568
+ // Raw invalid atom: render the call so the error frame shows it.
1569
+ return 'constraint()'
1570
+ }
1571
+ return parts.join('&')
1572
+ }
1573
+
1574
+
1575
+ // SUBSUMPTION over two residuals (G3, the query built on the table in
1576
+ // docs/reference-language.md "Subsumption"): does every value `s`
1577
+ // admits pass `g` too? Implemented here because the compare machinery
1578
+ // (cmpVal, sameScalar, the recursive count state) is this module's.
1579
+ //
1580
+ // Three-valued: true / false / 'undecided'. `must` on the GENERAL side
1581
+ // is undecided by construction (a Band B predicate is opaque — G3 maps
1582
+ // the table's "never" to the query's honest sub_evaluate_only); every
1583
+ // other approximation folds toward false, the safe direction.
1584
+ function constraintStateSubsumes(
1585
+ g: {
1586
+ domain?: 'number' | 'string', kind?: any, lo?: Bound, hi?: Bound,
1587
+ neqs: any[], res: ReAtom[], count?: ConstraintState, uniq: boolean,
1588
+ uniqBy: string[],
1589
+ musts: MustAtom[],
1590
+ },
1591
+ s: {
1592
+ domain?: 'number' | 'string', kind?: any, lo?: Bound, hi?: Bound,
1593
+ neqs: any[], res: ReAtom[], count?: ConstraintState, uniq: boolean,
1594
+ uniqBy: string[],
1595
+ musts: MustAtom[],
1596
+ },
1597
+ ): boolean | 'undecided' {
1598
+ // A Band B predicate on the general side makes its admitted set
1599
+ // unknowable; an extra `must` on the SPECIFIC side only narrows it
1600
+ // and is ignored.
1601
+ if (0 < g.musts.length) {
1602
+ return 'undecided'
1603
+ }
1604
+
1605
+ // Domains must agree where both constrain one; a sizing-only residual
1606
+ // has no domain and passes this gate.
1607
+ if (null != g.domain && g.domain !== s.domain) {
1608
+ return false
1609
+ }
1610
+
1611
+ // A general leaf restriction requires the same leaf on the specific
1612
+ // side (leaves are disjoint; an unrestricted specific admits other
1613
+ // leaves the general refuses).
1614
+ if (null != g.kind && g.kind !== s.kind) {
1615
+ return false
1616
+ }
1617
+
1618
+ // Interval containment: the general's endpoints at or beyond the
1619
+ // specific's, and where they coincide the general's may not be the
1620
+ // open one.
1621
+ const d = g.domain ?? s.domain
1622
+ if (null != g.lo) {
1623
+ if (null == s.lo || null == d) {
1624
+ return false
1625
+ }
1626
+ const c = cmpVal(d, g.lo.v, s.lo.v)
1627
+ if (0 < c || (0 === c && g.lo.open && !s.lo.open)) {
1628
+ return false
1629
+ }
1630
+ }
1631
+ if (null != g.hi) {
1632
+ if (null == s.hi || null == d) {
1633
+ return false
1634
+ }
1635
+ const c = cmpVal(d, g.hi.v, s.hi.v)
1636
+ if (c < 0 || (0 === c && g.hi.open && !s.hi.open)) {
1637
+ return false
1638
+ }
1639
+ }
1640
+
1641
+ // Excluding FEWER values is more general: every general exclusion
1642
+ // must be excluded by the specific too.
1643
+ for (const n of g.neqs) {
1644
+ if (!s.neqs.some((m: any) => sameScalar(n, m))) {
1645
+ return false
1646
+ }
1647
+ }
1648
+
1649
+ // Patterns compare as TEXT sets (the sanctioned approximation:
1650
+ // deciding regex containment is what this algebra refuses to do).
1651
+ for (const r of g.res) {
1652
+ if (!s.res.some((q: ReAtom) => q.src === r.src)) {
1653
+ return false
1654
+ }
1655
+ }
1656
+
1657
+ // `unique()` subsumes only itself on that axis: a general uniqueness
1658
+ // demand admits no container the unconstrained specific also admits.
1659
+ // ... and a general `unique(k)` needs the same key on the specific
1660
+ // side: distinctness on `port` says nothing about distinctness on
1661
+ // `name`.
1662
+ if (g.uniqBy.some((k: string) => !s.uniqBy.includes(k))) {
1663
+ return false
1664
+ }
1665
+ if (g.uniq && !s.uniq) {
1666
+ return false
1667
+ }
1668
+
1669
+ // The count atom reuses this same table over the integer domain.
1670
+ if (null != g.count) {
1671
+ if (null == s.count) {
1672
+ return false
1673
+ }
1674
+ return constraintStateSubsumes(g.count, s.count)
1675
+ }
1676
+
1677
+ return true
1678
+ }
1679
+
1680
+
1681
+ // The query surface for ts/src/subsume.ts: residual-vs-residual, and
1682
+ // residual-vs-scalar membership (with `must` and `unique` making the
1683
+ // scalar case undecided/false exactly as the meet would).
1684
+ function constraintSubsumesConstraint(
1685
+ g: ConstraintVal, s: ConstraintVal): boolean | 'undecided' {
1686
+ return constraintStateSubsumes(g as any, s as any)
1687
+ }
1688
+
1689
+ function constraintAdmitsScalar(
1690
+ g: ConstraintVal, scalar: any): boolean | 'undecided' {
1691
+ if (0 < g.musts.length) {
1692
+ return 'undecided'
1693
+ }
1694
+ if (g.uniq || 0 < g.uniqBy.length) {
1695
+ return false
1696
+ }
1697
+ // A count residual admits by LENGTH, which is the meet's business;
1698
+ // the query answers false (the safe direction) rather than
1699
+ // reimplementing the sizing walk here.
1700
+ if (null != (g as any).count) {
1701
+ return false
1702
+ }
1703
+ return stateAdmits(g as any, scalar)
1704
+ }
1705
+
1706
+
1707
+ // Does this residual's ORDER and MEMBERSHIP part admit the scalar? The
1708
+ // sizing atoms are deliberately not consulted: `admit` applies them to
1709
+ // the peer's length, and the count check applies this same function to
1710
+ // the count.
1711
+ function stateAdmits(s: ConstraintState, peer: any): boolean {
1712
+ const domainOf = numericLeaf(peer) ? 'number' :
1713
+ stringLeaf(peer) ? 'string' : undefined
1714
+
1715
+ if (null == s.domain) {
1716
+ // A sizing residual has no domain, and admits any scalar the sizing
1717
+ // atoms can then rule on. Booleans and null are not among them:
1718
+ // they have no order, no length and no members.
1719
+ if (null == domainOf) {
1720
+ return false
1721
+ }
1722
+ return true
1723
+ }
1724
+ if (domainOf !== s.domain) {
1725
+ return false
1726
+ }
1727
+ if (null != s.kind && leafMarker(peer) !== s.kind) {
1728
+ return false
1729
+ }
1730
+ const d = s.domain
1731
+ if (null != s.lo) {
1732
+ const c = cmpVal(d, peer, s.lo.v)
1733
+ if (c < 0 || (0 === c && s.lo.open)) {
1734
+ return false
1735
+ }
1736
+ }
1737
+ if (null != s.hi) {
1738
+ const c = cmpVal(d, peer, s.hi.v)
1739
+ if (c > 0 || (0 === c && s.hi.open)) {
1740
+ return false
1741
+ }
1742
+ }
1743
+ for (const n of s.neqs) {
1744
+ if (sameScalar(peer, n)) {
1745
+ return false
1746
+ }
1747
+ }
1748
+ // Every accumulated pattern must match: the meet of two `re` atoms
1749
+ // is conjunction, and matching is UNANCHORED in both engines
1750
+ // (JS RegExp.test, Go regexp.MatchString), so `re("el")` admits
1751
+ // "hello". Anchor with ^ and $ to mean the whole string.
1752
+ for (const r of s.res) {
1753
+ if (!r.re.test(peer.peg)) {
1754
+ return false
1755
+ }
1756
+ }
1757
+ return true
1758
+ }
1759
+
1760
+
1761
+ // The eager emptiness rules. Each is EXACT -- it reports empty only
1762
+ // where no value could satisfy the residual -- so the algebra stays
1763
+ // sound; the incompleteness it accepts is documented in
1764
+ // docs/reference-language.md, "Emptiness".
1765
+ function stateEmpty(s: ConstraintState): boolean {
1766
+ // Two disagreeing kind narrowings inside a length argument, recorded by
1767
+ // meetCount because that meet has no ctx to fail through.
1768
+ if (s.clash) {
1769
+ return true
1770
+ }
1771
+
1772
+ const d = s.domain as 'number' | 'string'
1773
+
1774
+ // Empty interval.
1775
+ if (null != s.lo && null != s.hi) {
1776
+ const c = cmpVal(d, s.hi.v, s.lo.v)
1777
+ if (c < 0 || (0 === c && (s.lo.open || s.hi.open))) {
1778
+ return true
1779
+ }
1780
+ }
1781
+
1782
+ const integral = Integer === s.kind || BigInteger === s.kind
1783
+
1784
+ // Integral gap: an integer-narrowed interval containing no whole
1785
+ // number is empty (integer & above(1) & below(2)).
1786
+ if (integral && null != s.lo && null != s.hi) {
1787
+ const lo = scaledOfNumeric(s.lo.v)
1788
+ const hi = scaledOfNumeric(s.hi.v)
1789
+ if (!lo.inf && !hi.inf) {
1790
+ // Smallest admissible integer above/at the lower bound.
1791
+ let n = scaledFloor(lo)
1792
+ if (!scaledIsIntegral(lo) || s.lo.open) {
1793
+ n += 1n
1794
+ }
1795
+ // Largest admissible integer below/at the upper bound.
1796
+ let m = scaledFloor(hi)
1797
+ if (s.hi.open && scaledIsIntegral(hi)) {
1798
+ m -= 1n
1799
+ }
1800
+ if (m < n) {
1801
+ return true
1802
+ }
1803
+ }
1804
+ }
1805
+
1806
+ // Point deletion under a narrowed leaf: a closed point interval
1807
+ // whose single value of the narrowed leaf is excluded is empty
1808
+ // (integer & min(3) & max(3) & neq(3)). Without a narrowing the
1809
+ // point survives in the other leaves.
1810
+ if (null != s.kind && null != s.lo && null != s.hi &&
1811
+ !s.lo.open && !s.hi.open &&
1812
+ 0 === cmpVal(d, s.lo.v, s.hi.v)) {
1813
+ for (const n of s.neqs) {
1814
+ if (leafMarker(n) === s.kind && 0 === cmpNumeric(n, s.lo.v)) {
1815
+ return true
1816
+ }
1817
+ }
1818
+ }
1819
+
1820
+ // Sizing over the number domain: a number has neither a length nor
1821
+ // members, so `integer & length(3)` and `min(2) & unique()` admit
1822
+ // nothing. Uniqueness over the string domain is empty for the same
1823
+ // reason -- a string's members are not values the algebra compares.
1824
+ if ('number' === d && (null != s.count || s.uniq ||
1825
+ 0 < s.uniqBy.length)) {
1826
+ return true
1827
+ }
1828
+ if ('string' === d && s.uniq) {
1829
+ return true
1830
+ }
1831
+
1832
+ // An empty count residual makes the whole thing empty: no container
1833
+ // and no string has a length no integer can take.
1834
+ if (null != s.count && stateEmpty(s.count)) {
1835
+ return true
1836
+ }
1837
+
1838
+ return false
1839
+ }
1840
+
1841
+
1842
+ // The base every `length` argument meets: a count is a non-negative
1843
+ // integer, whatever else the argument says (docs/reference-language.md,
1844
+ // "`length` semantics" -- `length(c)` is empty iff `c & integer & min(0)`
1845
+ // is).
1846
+ function countBase(): ConstraintState {
1847
+ return {
1848
+ domain: 'number',
1849
+ kind: Integer,
1850
+ lo: { v: countVal(0), open: false },
1851
+ neqs: [],
1852
+ res: [],
1853
+ musts: [],
1854
+ // A COUNT is a number, and a number has no members to be distinct.
1855
+ uniq: false,
1856
+ uniqBy: [],
1857
+ }
1858
+ }
1859
+
1860
+
1861
+ // A count as a Val, so the count residual can be applied by exactly the
1862
+ // same membership function as any other numeric residual.
1863
+ function countVal(n: number): any {
1864
+ return new IntegerVal({ peg: n })
1865
+ }
1866
+
1867
+
1868
+ // The pure meet of two count residuals. Both are number-domain and
1869
+ // carry no pattern or sizing atom of their own, so the merge is the
1870
+ // interval/exclusion part alone. A kind disagreement becomes a `clash`
1871
+ // rather than an error: this meet runs at construction, where there is
1872
+ // no AontuContext to raise through, and an empty residual carries the
1873
+ // same news to `unify`.
1874
+ function meetCount(a: ConstraintState, b: ConstraintState): ConstraintState {
1875
+ return {
1876
+ domain: 'number',
1877
+ // `a` is always a countBase()-seeded residual, so its kind is
1878
+ // always Integer — there is no kindless side to fall back from.
1879
+ kind: a.kind,
1880
+ lo: tighter('number', a.lo, b.lo, true),
1881
+ hi: tighter('number', a.hi, b.hi, false),
1882
+ neqs: dedupSorted('number', [...a.neqs, ...b.neqs]),
1883
+ res: [],
1884
+ musts: [],
1885
+ uniq: false,
1886
+ uniqBy: [],
1887
+ clash: true === a.clash || true === b.clash ||
1888
+ (null != a.kind && null != b.kind && a.kind !== b.kind),
1889
+ }
1890
+ }
1891
+
1892
+
1893
+ // Read a WRITTEN `length` argument as a count residual, or undefined when
1894
+ // it is not one. Accepted: an integer literal (an exact count), a
1895
+ // numeric kind, a Band A residual over the number domain, and any
1896
+ // conjunct of those. A conjunct never reaches here any more: an
1897
+ // unsettled argument is held as `pending` and folded before the count
1898
+ // reads it (G1 phase 4), so `length(min(2)&max(5))` arrives as the
1899
+ // single residual it folds to.
1900
+ //
1901
+ // Anything else is refused rather than deferred. A reference or an
1902
+ // expression would have to residuate, and the sizing atoms do not
1903
+ // residuate on their ARGUMENT -- only on the peer whose members are
1904
+ // still settling.
1905
+ function countArgState(arg: any): ConstraintState | undefined {
1906
+ if (numericLeaf(arg)) {
1907
+ return {
1908
+ domain: 'number',
1909
+ lo: { v: arg, open: false },
1910
+ hi: { v: arg, open: false },
1911
+ neqs: [], res: [], musts: [], uniq: false, uniqBy: [],
1912
+ }
1913
+ }
1914
+
1915
+ if (true === arg?.isConstraint) {
1916
+ const c = arg as ConstraintVal
1917
+ // A pattern, a sizing atom or a string bound inside a count is not
1918
+ // a count constraint at all, and neither is a broken one.
1919
+ if (null != c.invalid || 0 < c.res.length || c.uniq ||
1920
+ 0 < c.uniqBy.length || null != c.count ||
1921
+ 'number' !== c.domain) {
1922
+ return undefined
1923
+ }
1924
+ return {
1925
+ domain: 'number',
1926
+ kind: c.kind,
1927
+ lo: c.lo,
1928
+ hi: c.hi,
1929
+ neqs: [...c.neqs],
1930
+ res: [], musts: [], uniq: false, uniqBy: [],
1931
+ }
1932
+ }
1933
+
1934
+ if (true === arg?.isScalarKind) {
1935
+ const marker = arg.peg
1936
+ if (Number === marker) {
1937
+ return {
1938
+ domain: 'number', neqs: [], res: [], musts: [],
1939
+ uniq: false, uniqBy: [],
1940
+ }
1941
+ }
1942
+ if (Integer === marker || Float === marker ||
1943
+ BigInteger === marker || BigDecimal === marker) {
1944
+ return {
1945
+ domain: 'number', kind: marker, neqs: [], res: [], musts: [],
1946
+ uniq: false, uniqBy: [],
1947
+ }
1948
+ }
1949
+ return undefined
1950
+ }
1951
+
1952
+ return undefined
1953
+ }
1954
+
1955
+
1956
+ // A container is SETTLED when it and every child have converged. Until
1957
+ // then the member set can still change — an optional key whose value is
1958
+ // a still-resolving reference may yet generate — so a sizing atom must
1959
+ // defer rather than decide (docs/reference-language.md, "`length`
1960
+ // semantics"). Note that an optional holding a settled-but-ungenerable
1961
+ // value, `{x:1,y?:number}`, IS settled: the map converges on the first
1962
+ // pass and `y` is simply never emitted.
1963
+ function containerSettled(bag: any): boolean {
1964
+ // The bag's OWN done-counter is enough: BagVal.unify sets it from the
1965
+ // AND over its children, so an unsettled child already leaves the bag
1966
+ // unsettled. Walking the children again would be a second, drifting
1967
+ // copy of that rule.
1968
+ return true === bag.done
1969
+ }
1970
+
1971
+
1972
+ // The child kinds `BagVal.gen` will attempt to generate. Anything else
1973
+ // is residue: dropped when the key is optional, and a bag-level error
1974
+ // otherwise.
1975
+ function genable(child: any): boolean {
1976
+ return true === child.isScalar || true === child.isMap ||
1977
+ true === child.isList || true === child.isPref ||
1978
+ true === child.isRef || true === child.isDisjunct ||
1979
+ true === child.isNil ||
1980
+ // A settled sizing residue generates through ConjunctVal.gen, so it
1981
+ // is a member like any other -- and a `length` that did not count
1982
+ // its sibling's `unique` would be the early-fold defect again, one
1983
+ // level up.
1984
+ undefined !== sizingResidue(child)
1985
+ }
1986
+
1987
+
1988
+ // The children a bag would EMIT, mirroring the selection in
1989
+ // `BagVal.gen` — type/hide marks skipped, non-generable residue and
1990
+ // values that generate nothing dropped, optional keys dropped when
1991
+ // they generate empty.
1992
+ //
1993
+ // It returns the member VALS rather than their generated values,
1994
+ // because uniqueness compares canon and a generated value cannot
1995
+ // express that distinction: `1` and `1.0` generate the same JSON number
1996
+ // and canon differently. Counting uses the same list, so both sizing
1997
+ // atoms see exactly one definition of "member".
1998
+ //
1999
+ // Returns undefined when a REQUIRED child is residue: the container's
2000
+ // own `gen` raises there, and that error is the one worth reporting.
2001
+ function emittedMembers(bag: any, ctx: AontuContext): any[] | undefined {
2002
+ const out: any[] = []
2003
+
2004
+ let entries = items(bag.peg)
2005
+ if (bag.isMap) {
2006
+ // Code-point order, because the two ports disagree on raw key order
2007
+ // — JavaScript hoists integer-like keys, Go keeps insertion order —
2008
+ // and a duplicate report must name the same pair in both.
2009
+ entries = entries
2010
+ .slice()
2011
+ .sort((a: any, b: any) => cmpCodePoint(String(a[0]), String(b[0])))
2012
+ }
2013
+
2014
+ for (const item of entries) {
2015
+ const key = item[0]
2016
+ const child: any = item[1]
2017
+
2018
+ if (child.mark.type || child.mark.hide) {
2019
+ continue
2020
+ }
2021
+
2022
+ const optional = bag.optionalKeys.includes('' + key)
2023
+
2024
+ if (!genable(child)) {
2025
+ if (optional) {
2026
+ continue
2027
+ }
2028
+ return undefined
2029
+ }
2030
+
2031
+ // Generation decides in an isolated collect context, so an
2032
+ // unresolved inner value neither raises here nor pollutes the
2033
+ // caller's errors — the same isolation BagVal.gen uses for an
2034
+ // optional child.
2035
+ const cval = child.gen(ctx.clone({ err: [], collect: true }))
2036
+
2037
+ if (undefined === cval || (optional && empty(cval))) {
2038
+ continue
2039
+ }
2040
+
2041
+ out.push(child)
2042
+ }
2043
+
2044
+ return out
2045
+ }
2046
+
2047
+
2048
+ // Sort accumulated patterns by source in code-point order and drop
2049
+ // exact duplicates. Patterns are NEVER simplified or compared for
2050
+ // containment: deciding `re("a")` subsumes `re("a|b")` is regex
2051
+ // containment, which the algebra deliberately does not do (emptiness
2052
+ // stays approximate — sound, incomplete). Two spellings of one language
2053
+ // therefore both survive, and both are tested.
2054
+ function dedupSortedRes(res: ReAtom[]): ReAtom[] {
2055
+ const sorted = [...res].sort((a, b) => cmpCodePoints(a.src, b.src))
2056
+ const out: ReAtom[] = []
2057
+ for (const r of sorted) {
2058
+ if (0 === out.length || out[out.length - 1].src !== r.src) {
2059
+ out.push(r)
2060
+ }
2061
+ }
2062
+ return out
2063
+ }
2064
+
2065
+
2066
+ // The atom classes registered in the parser's funcMap: each is a
521
2067
  // ConstraintVal that knows its atom name. Constructed by the
522
2068
  // func-paren handler as `new funcval({peg: args})`.
523
2069
  class MinConstraintVal extends ConstraintVal {
@@ -548,14 +2094,51 @@ class NeqConstraintVal extends ConstraintVal {
548
2094
  constructor(spec: ValSpec, ctx?: AontuContext) {
549
2095
  super({ ...spec, atom: 'neq' }, ctx)
550
2096
  }
551
- } /* node:coverage ignore next 11 */
2097
+ }
2098
+
2099
+ class ReConstraintVal extends ConstraintVal {
2100
+ constructor(spec: ValSpec, ctx?: AontuContext) {
2101
+ super({ ...spec, atom: 're' }, ctx)
2102
+ }
2103
+ }
2104
+
2105
+ class MustConstraintVal extends ConstraintVal {
2106
+ constructor(spec: ValSpec, ctx?: AontuContext) {
2107
+ super({ ...spec, atom: 'must' }, ctx)
2108
+ }
2109
+ }
2110
+
2111
+ class LengthConstraintVal extends ConstraintVal {
2112
+ constructor(spec: ValSpec, ctx?: AontuContext) {
2113
+ super({ ...spec, atom: 'length' }, ctx)
2114
+ }
2115
+ }
2116
+
2117
+ class UniqueConstraintVal extends ConstraintVal {
2118
+ constructor(spec: ValSpec, ctx?: AontuContext) {
2119
+ super({ ...spec, atom: 'unique' }, ctx)
2120
+ }
2121
+ } /* node:coverage ignore next 23 */
552
2122
 
553
2123
 
554
2124
  export {
2125
+ // Exported for the differential corpus test (ADR-003): the two ports'
2126
+ // normalisers must produce byte-identical output, and that can only be
2127
+ // checked by calling them.
2128
+ normaliseRe,
2129
+ // Exported for the subsumption query (G3, ts/src/subsume.ts): the
2130
+ // residual-vs-residual and residual-vs-scalar rules live here beside
2131
+ // the compare machinery they reuse.
2132
+ constraintSubsumesConstraint,
2133
+ constraintAdmitsScalar,
555
2134
  ConstraintVal,
556
2135
  MinConstraintVal,
557
2136
  MaxConstraintVal,
558
2137
  AboveConstraintVal,
559
2138
  BelowConstraintVal,
560
2139
  NeqConstraintVal,
2140
+ ReConstraintVal,
2141
+ LengthConstraintVal,
2142
+ UniqueConstraintVal,
2143
+ MustConstraintVal,
561
2144
  }