aontu 0.50.1 → 0.52.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 (197) hide show
  1. package/bin/aontu-lsp.js +6 -0
  2. package/bin/aontu.js +7 -0
  3. package/dist/aontu.d.ts +4 -2
  4. package/dist/aontu.js +21 -8
  5. package/dist/aontu.js.map +1 -1
  6. package/dist/cli.d.ts +0 -1
  7. package/dist/cli.js +43 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/ctx.js +1 -1
  10. package/dist/ctx.js.map +1 -1
  11. package/dist/err.js +3 -2
  12. package/dist/err.js.map +1 -1
  13. package/dist/exactjson.d.ts +37 -0
  14. package/dist/exactjson.js +228 -0
  15. package/dist/exactjson.js.map +1 -0
  16. package/dist/hints.d.ts +3 -1
  17. package/dist/hints.js +187 -5
  18. package/dist/hints.js.map +1 -1
  19. package/dist/keyorder.d.ts +9 -0
  20. package/dist/keyorder.js +65 -0
  21. package/dist/keyorder.js.map +1 -0
  22. package/dist/lang.js +263 -30
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lsp-server.d.ts +2 -2
  25. package/dist/lsp-server.js +10 -10
  26. package/dist/lsp-server.js.map +1 -1
  27. package/dist/lsp.js +39 -21
  28. package/dist/lsp.js.map +1 -1
  29. package/dist/site.js +1 -1
  30. package/dist/site.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/unify.js +48 -1
  33. package/dist/unify.js.map +1 -1
  34. package/dist/utility.js +3 -8
  35. package/dist/utility.js.map +1 -1
  36. package/dist/val/BagVal.js +9 -6
  37. package/dist/val/BagVal.js.map +1 -1
  38. package/dist/val/BigDecimalVal.d.ts +13 -0
  39. package/dist/val/BigDecimalVal.js +80 -0
  40. package/dist/val/BigDecimalVal.js.map +1 -0
  41. package/dist/val/BigIntegerVal.d.ts +11 -0
  42. package/dist/val/BigIntegerVal.js +75 -0
  43. package/dist/val/BigIntegerVal.js.map +1 -0
  44. package/dist/val/BooleanVal.js +1 -1
  45. package/dist/val/BooleanVal.js.map +1 -1
  46. package/dist/val/CloseFuncVal.js +1 -1
  47. package/dist/val/CloseFuncVal.js.map +1 -1
  48. package/dist/val/ConjunctVal.js +1 -1
  49. package/dist/val/ConjunctVal.js.map +1 -1
  50. package/dist/val/ConstraintVal.d.ts +56 -0
  51. package/dist/val/ConstraintVal.js +407 -0
  52. package/dist/val/ConstraintVal.js.map +1 -0
  53. package/dist/val/CopyFuncVal.js +1 -1
  54. package/dist/val/CopyFuncVal.js.map +1 -1
  55. package/dist/val/Decimal.d.ts +32 -0
  56. package/dist/val/Decimal.js +318 -0
  57. package/dist/val/Decimal.js.map +1 -0
  58. package/dist/val/DisjunctVal.js +1 -29
  59. package/dist/val/DisjunctVal.js.map +1 -1
  60. package/dist/val/ExpectVal.js +1 -1
  61. package/dist/val/ExpectVal.js.map +1 -1
  62. package/dist/val/FeatureVal.js +1 -1
  63. package/dist/val/FeatureVal.js.map +1 -1
  64. package/dist/val/FuncBaseVal.js +1 -1
  65. package/dist/val/FuncBaseVal.js.map +1 -1
  66. package/dist/val/HideFuncVal.js +1 -1
  67. package/dist/val/HideFuncVal.js.map +1 -1
  68. package/dist/val/IntegerVal.d.ts +2 -0
  69. package/dist/val/IntegerVal.js +77 -5
  70. package/dist/val/IntegerVal.js.map +1 -1
  71. package/dist/val/JunctionVal.js +1 -1
  72. package/dist/val/JunctionVal.js.map +1 -1
  73. package/dist/val/KeyFuncVal.d.ts +1 -1
  74. package/dist/val/KeyFuncVal.js +34 -4
  75. package/dist/val/KeyFuncVal.js.map +1 -1
  76. package/dist/val/ListVal.js +7 -7
  77. package/dist/val/ListVal.js.map +1 -1
  78. package/dist/val/LowerFuncVal.js +21 -4
  79. package/dist/val/LowerFuncVal.js.map +1 -1
  80. package/dist/val/MapVal.js +17 -13
  81. package/dist/val/MapVal.js.map +1 -1
  82. package/dist/val/MoveFuncVal.js +26 -9
  83. package/dist/val/MoveFuncVal.js.map +1 -1
  84. package/dist/val/NilVal.d.ts +1 -0
  85. package/dist/val/NilVal.js +10 -3
  86. package/dist/val/NilVal.js.map +1 -1
  87. package/dist/val/NullVal.js +1 -1
  88. package/dist/val/NullVal.js.map +1 -1
  89. package/dist/val/NumberVal.d.ts +1 -0
  90. package/dist/val/NumberVal.js +25 -4
  91. package/dist/val/NumberVal.js.map +1 -1
  92. package/dist/val/OpBaseVal.js +10 -8
  93. package/dist/val/OpBaseVal.js.map +1 -1
  94. package/dist/val/OpenFuncVal.js +1 -1
  95. package/dist/val/OpenFuncVal.js.map +1 -1
  96. package/dist/val/PathFuncVal.js +10 -2
  97. package/dist/val/PathFuncVal.js.map +1 -1
  98. package/dist/val/PlusOpVal.d.ts +3 -1
  99. package/dist/val/PlusOpVal.js +140 -29
  100. package/dist/val/PlusOpVal.js.map +1 -1
  101. package/dist/val/PrefFuncVal.js +1 -1
  102. package/dist/val/PrefFuncVal.js.map +1 -1
  103. package/dist/val/PrefVal.d.ts +2 -0
  104. package/dist/val/PrefVal.js +44 -7
  105. package/dist/val/PrefVal.js.map +1 -1
  106. package/dist/val/RefVal.d.ts +2 -0
  107. package/dist/val/RefVal.js +135 -11
  108. package/dist/val/RefVal.js.map +1 -1
  109. package/dist/val/ScalarKindVal.d.ts +12 -2
  110. package/dist/val/ScalarKindVal.js +108 -13
  111. package/dist/val/ScalarKindVal.js.map +1 -1
  112. package/dist/val/ScalarVal.d.ts +1 -0
  113. package/dist/val/ScalarVal.js +18 -3
  114. package/dist/val/ScalarVal.js.map +1 -1
  115. package/dist/val/StringVal.js +1 -1
  116. package/dist/val/StringVal.js.map +1 -1
  117. package/dist/val/SuperFuncVal.d.ts +1 -1
  118. package/dist/val/SuperFuncVal.js +15 -2
  119. package/dist/val/SuperFuncVal.js.map +1 -1
  120. package/dist/val/TopVal.js +1 -1
  121. package/dist/val/TopVal.js.map +1 -1
  122. package/dist/val/TypeFuncVal.js +1 -1
  123. package/dist/val/TypeFuncVal.js.map +1 -1
  124. package/dist/val/UpperFuncVal.js +21 -4
  125. package/dist/val/UpperFuncVal.js.map +1 -1
  126. package/dist/val/Val.d.ts +1 -1
  127. package/dist/val/Val.js +11 -21
  128. package/dist/val/Val.js.map +1 -1
  129. package/dist/val/VarVal.js +8 -2
  130. package/dist/val/VarVal.js.map +1 -1
  131. package/dist/val/numcmp.d.ts +14 -0
  132. package/dist/val/numcmp.js +134 -0
  133. package/dist/val/numcmp.js.map +1 -0
  134. package/dist/val/numkind.d.ts +6 -0
  135. package/dist/val/numkind.js +224 -0
  136. package/dist/val/numkind.js.map +1 -0
  137. package/dist/val/valutil.d.ts +1 -0
  138. package/dist/val/valutil.js +35 -0
  139. package/dist/val/valutil.js.map +1 -1
  140. package/package.json +7 -5
  141. package/src/aontu.ts +26 -9
  142. package/src/cli.ts +48 -21
  143. package/src/ctx.ts +1 -1
  144. package/src/err.ts +3 -2
  145. package/src/exactjson.ts +254 -0
  146. package/src/hints.ts +213 -4
  147. package/src/keyorder.ts +73 -0
  148. package/src/lang.ts +303 -31
  149. package/src/lsp-server.ts +17 -15
  150. package/src/lsp.ts +37 -14
  151. package/src/site.ts +1 -1
  152. package/src/type.ts +1 -1
  153. package/src/unify.ts +52 -1
  154. package/src/utility.ts +3 -9
  155. package/src/val/BagVal.ts +9 -6
  156. package/src/val/BigDecimalVal.ts +117 -0
  157. package/src/val/BigIntegerVal.ts +110 -0
  158. package/src/val/BooleanVal.ts +1 -1
  159. package/src/val/CloseFuncVal.ts +1 -1
  160. package/src/val/ConjunctVal.ts +1 -1
  161. package/src/val/ConstraintVal.ts +561 -0
  162. package/src/val/CopyFuncVal.ts +1 -1
  163. package/src/val/Decimal.ts +385 -0
  164. package/src/val/DisjunctVal.ts +1 -37
  165. package/src/val/ExpectVal.ts +1 -1
  166. package/src/val/FeatureVal.ts +1 -1
  167. package/src/val/FuncBaseVal.ts +1 -1
  168. package/src/val/HideFuncVal.ts +1 -1
  169. package/src/val/IntegerVal.ts +81 -5
  170. package/src/val/JunctionVal.ts +1 -1
  171. package/src/val/KeyFuncVal.ts +36 -4
  172. package/src/val/ListVal.ts +3 -5
  173. package/src/val/LowerFuncVal.ts +22 -5
  174. package/src/val/MapVal.ts +13 -9
  175. package/src/val/MoveFuncVal.ts +27 -10
  176. package/src/val/NilVal.ts +13 -3
  177. package/src/val/NullVal.ts +1 -1
  178. package/src/val/NumberVal.ts +27 -4
  179. package/src/val/OpBaseVal.ts +10 -10
  180. package/src/val/OpenFuncVal.ts +1 -1
  181. package/src/val/PathFuncVal.ts +10 -2
  182. package/src/val/PlusOpVal.ts +168 -32
  183. package/src/val/PrefFuncVal.ts +1 -1
  184. package/src/val/PrefVal.ts +70 -8
  185. package/src/val/RefVal.ts +143 -11
  186. package/src/val/ScalarKindVal.ts +123 -12
  187. package/src/val/ScalarVal.ts +20 -3
  188. package/src/val/StringVal.ts +1 -1
  189. package/src/val/SuperFuncVal.ts +15 -2
  190. package/src/val/TopVal.ts +1 -1
  191. package/src/val/TypeFuncVal.ts +1 -1
  192. package/src/val/UpperFuncVal.ts +22 -5
  193. package/src/val/Val.ts +13 -8
  194. package/src/val/VarVal.ts +8 -2
  195. package/src/val/numcmp.ts +189 -0
  196. package/src/val/numkind.ts +256 -0
  197. package/src/val/valutil.ts +36 -0
@@ -0,0 +1,385 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ /*
4
+ * The exact base-10 decimal value behind the `bigdecimal` leaf, and the
5
+ * parser for the `0d` literal family that reaches both exact leaves.
6
+ *
7
+ * A Decimal is a coefficient and a scale:
8
+ *
9
+ * value = unscaled / 10^scale
10
+ *
11
+ * with `unscaled` a native bigint and `scale` a plain integer. Every
12
+ * operation here is EXACT: aontu's whole numeric operator surface
13
+ * (`+`, `upper()`, `lower()`, unary minus) is exact, so no rounding
14
+ * context, precision or rounding mode exists anywhere in this file --
15
+ * addition aligns scales and adds coefficients, and ceiling/floor is
16
+ * coefficient arithmetic that never lets a float64 near the value.
17
+ * Where an exact answer will not fit the budget the answer is refused,
18
+ * never rounded (see decimalOverBudget).
19
+ *
20
+ * D4 -- ONE VALUE, ONE RENDERING. In a unifier canon must be a function
21
+ * of the value, so scale is presentation and not identity: `0d0.10`,
22
+ * `0d0.1` and `0d1e-1` are the same lattice point and must render
23
+ * identically. A Decimal therefore NORMALISES IN ITS CONSTRUCTOR --
24
+ * every Decimal in existence is in minimal form, which is what makes
25
+ * `equals` a two-field comparison and `same()` correct without any
26
+ * pointer identity (D2).
27
+ *
28
+ * The minimal form keeps ONE decimal place at the bottom. `0d` names the
29
+ * numeric FAMILY, not the leaf, so an integral bigdecimal that rendered
30
+ * as `0d1000` would reparse as a biginteger -- a kind-flipping canon of
31
+ * exactly the sort R4's `.0` suffix exists to prevent. So `0d1e3` canons
32
+ * as `0d1000.0`, and scale is never reduced below 1.
33
+ *
34
+ * Go mirror (D8): a struct of (coeff *big.Int, scale int32) normalised
35
+ * by the same rule -- strip trailing zeros while scale > 1, then raise
36
+ * the scale to 1 when it is lower, zero always at scale 1.
37
+ */
38
+
39
+
40
+ // D6 -- THE EXACTNESS BUDGET, enforced at parse.
41
+ //
42
+ // Both halves are needed and the scale half is the load-bearing one: a
43
+ // coefficient-only check never fires for `0d1e1000000000`, whose
44
+ // coefficient is one digit, yet plain-form rendering of that value would
45
+ // have to materialise a gigabyte of zeros (and overflow the Go port's
46
+ // int32 scale field) without any arithmetic having occurred.
47
+ //
48
+ // A literal over either bound is a located error, never a rounded or
49
+ // expanded value: this language does not round, so the only honest
50
+ // answer at the limit is refusal.
51
+ const DECIMAL_COEFFICIENT_BUDGET = 4096
52
+ const DECIMAL_SCALE_BUDGET = 4096
53
+
54
+
55
+ // Powers of ten as bigints, memoised: normalisation of an
56
+ // exponent-bearing literal needs one per parse.
57
+ function pow10(n: number): bigint {
58
+ return 10n ** BigInt(n)
59
+ }
60
+
61
+
62
+ // True when a decimal's SOURCE form is over either budget bound.
63
+ //
64
+ // Every route that builds a Decimal from text must ask this BEFORE
65
+ // constructing one, because normalising an over-budget value is itself
66
+ // the resource event the bound exists to prevent: the constructor folds
67
+ // a negative scale by multiplying the coefficient by 10^-scale, so
68
+ // `1e1000000000` would try to build a billion-digit bigint.
69
+ //
70
+ // Written once and used by both routes -- the `0d` literal and the
71
+ // exact-input API -- because the Go port bounds both (NewBigDecimal
72
+ // shares the literal path's checker) and a TypeScript route that did not
73
+ // would be a silent cross-port divergence as well as a hazard.
74
+ //
75
+ // `scale` may be Infinity or NaN when the exponent digits overflow a JS
76
+ // number; the comparison is written so that either answers "over".
77
+ function overBudget(coeffDigits: number, scale: number): boolean {
78
+ return DECIMAL_COEFFICIENT_BUDGET < coeffDigits ||
79
+ !(Math.abs(scale) <= DECIMAL_SCALE_BUDGET)
80
+ }
81
+
82
+
83
+ // The decimal digit count of a coefficient, sign ignored (zero counts as
84
+ // one digit). Only the budget test needs it.
85
+ function coeffDigits(unscaled: bigint): number {
86
+ return (unscaled < 0n ? -unscaled : unscaled).toString().length
87
+ }
88
+
89
+
90
+ class Decimal {
91
+ // value = unscaled / 10^scale, always in minimal form (see above).
92
+ readonly unscaled: bigint
93
+
94
+ // Always >= 1: an integral bigdecimal keeps one decimal place so its
95
+ // canon cannot reparse as a biginteger.
96
+ readonly scale: number
97
+
98
+ constructor(unscaled: bigint, scale: number) {
99
+ let u = unscaled
100
+ let s = Math.trunc(scale)
101
+
102
+ if (0n === u) {
103
+ // R2/D5: there is no negative zero here to begin with (a bigint
104
+ // has one zero), and every zero is the same lattice point, so it
105
+ // takes the one canonical form `0d0.0`.
106
+ s = 1
107
+ }
108
+ else {
109
+ // Strip the trailing zeros that scale-as-presentation would
110
+ // otherwise smuggle into identity (`0d0.10` -> `0d0.1`)...
111
+ while (1 < s && 0n === u % 10n) {
112
+ u /= 10n
113
+ s--
114
+ }
115
+ // ...and fold a negative scale (an exponent) back up to the one
116
+ // decimal place the leaf marker requires (`0d1e3` -> 10000/10^1).
117
+ if (s < 1) {
118
+ u *= pow10(1 - s)
119
+ s = 1
120
+ }
121
+ }
122
+
123
+ this.unscaled = u
124
+ this.scale = s
125
+ }
126
+
127
+
128
+ // Exact numeric equality. Both operands are normalised by
129
+ // construction, so equal values have equal fields -- this is value
130
+ // comparison, never object identity (D2).
131
+ equals(peer: Decimal): boolean {
132
+ return this.unscaled === peer.unscaled && this.scale === peer.scale
133
+ }
134
+
135
+
136
+ // Exact ordering: -1, 0 or 1. Compares on a common scale, so no
137
+ // float64 ever touches the operands.
138
+ compare(peer: Decimal): number {
139
+ let a = this.unscaled
140
+ let b = peer.unscaled
141
+ if (this.scale < peer.scale) {
142
+ a *= pow10(peer.scale - this.scale)
143
+ }
144
+ else if (peer.scale < this.scale) {
145
+ b *= pow10(this.scale - peer.scale)
146
+ }
147
+ return a < b ? -1 : a > b ? 1 : 0
148
+ }
149
+
150
+
151
+ // Exact negation, keeping the leaf. -0 folds to 0 (D5) because the
152
+ // normalising constructor sends every zero to the same form.
153
+ negate(): Decimal {
154
+ return new Decimal(-this.unscaled, this.scale)
155
+ }
156
+
157
+
158
+ // D6 -- EXACT ADDITION. Align the scales, add the coefficients. That
159
+ // is the entire algorithm: there is no rounding mode to consult and no
160
+ // precision to run out of, which is why `0d0.1 + 0d0.2` is exactly
161
+ // `0d0.3` here and 0.30000000000000004 in binary64.
162
+ //
163
+ // The work is bounded because both operands are within the parse-time
164
+ // budget, so alignment can inflate a coefficient only by the scale
165
+ // span. The RESULT, however, can be over budget even when both
166
+ // operands are inside it (`0d1e4000 + 0d1e-4000` needs 8001
167
+ // coefficient digits), so every caller must test the result with
168
+ // decimalOverBudget() and report `decimal_budget` rather than store
169
+ // it. Refusal is the only honest answer at the limit: rounding to fit
170
+ // is exactly what this leaf exists to prevent.
171
+ add(peer: Decimal): Decimal {
172
+ const scale = this.scale < peer.scale ? peer.scale : this.scale
173
+ const a = this.unscaled * pow10(scale - this.scale)
174
+ const b = peer.unscaled * pow10(scale - peer.scale)
175
+ return new Decimal(a + b, scale)
176
+ }
177
+
178
+
179
+ // Exact ceiling and floor, for `upper()`/`lower()`. Coefficient
180
+ // arithmetic only -- Math.ceil of this value would mean rounding it to
181
+ // binary64 first, which for a leaf that exists to escape binary64 is
182
+ // no answer at all.
183
+ //
184
+ // The leaf is kept (R5 contagion): the result is a bigdecimal whose
185
+ // value happens to be integral, so it keeps the one decimal place the
186
+ // family marker requires -- `upper(0d1.1)` is `0d2.0`, not `0d2`.
187
+ //
188
+ // Neither can leave the exactness budget: with scale >= 1 the integer
189
+ // part drops `scale` digits and normalisation adds one back, so the
190
+ // coefficient never grows. That is why these return a Decimal
191
+ // directly while add() makes its caller check.
192
+ ceil(): Decimal {
193
+ return new Decimal(this.intPart(1n), 0)
194
+ }
195
+
196
+
197
+ floor(): Decimal {
198
+ return new Decimal(this.intPart(-1n), 0)
199
+ }
200
+
201
+
202
+ // The integer part, stepped one in the direction of `dir` when a
203
+ // remainder of that sign was discarded. bigint division truncates
204
+ // toward zero and the remainder carries the dividend's sign, so
205
+ // truncation already IS the floor of a positive and the ceiling of a
206
+ // negative; only the other half of each case needs the step.
207
+ private intPart(dir: bigint): bigint {
208
+ const p = pow10(this.scale)
209
+ const q = this.unscaled / p
210
+ const r = this.unscaled % p
211
+ return 0n !== r && (0n < r) === (0n < dir) ? q + dir : q
212
+ }
213
+
214
+
215
+ isZero(): boolean {
216
+ return 0n === this.unscaled
217
+ }
218
+
219
+
220
+ // Plain digits, no `0d` marker: `1000.0`, `-0.1`. Plain form at every
221
+ // magnitude, never scientific -- the budget is what makes that safe.
222
+ // This is also the rendering string concatenation will want in Phase 4
223
+ // (`"q" + 0d0.1` is `"q0.1"`), which is why the marker lives in
224
+ // `canon()` and not here.
225
+ toString(): string {
226
+ const neg = this.unscaled < 0n
227
+ let d = (neg ? -this.unscaled : this.unscaled).toString()
228
+ if (d.length <= this.scale) {
229
+ d = '0'.repeat(this.scale - d.length + 1) + d
230
+ }
231
+ const cut = d.length - this.scale
232
+ return (neg ? '-' : '') + d.slice(0, cut) + '.' + d.slice(cut)
233
+ }
234
+
235
+
236
+ // Canon rendering: the sign goes BEFORE the marker (`-0d1.5`), because
237
+ // `0d-1.5` is not a literal this language accepts.
238
+ canon(): string {
239
+ const neg = this.unscaled < 0n
240
+ return (neg ? '-0d' : '0d') + (neg ? this.negate() : this).toString()
241
+ }
242
+
243
+
244
+ // Exact-input construction for library consumers (D8). A JS `number`
245
+ // argument is already rounded before this library could inspect it, so
246
+ // the exact leaves are reachable only from a bigint or from text.
247
+ // Accepts `[+-]?digits[.digits][(e|E)[+-]digits]`, with or without the
248
+ // `0d` marker, and no `_` separators (those are literal syntax).
249
+ static fromString(src: string): Decimal {
250
+ const m = /^([-+]?)(?:0[dD])?([0-9]+)(?:\.([0-9]+))?(?:[eE]([-+]?[0-9]+))?$/
251
+ .exec(src)
252
+ if (null == m) {
253
+ throw new Error('not-decimal: ' + src)
254
+ }
255
+ const frac = m[3] ?? ''
256
+ const exp = null == m[4] ? 0 : Number(m[4])
257
+ const scale = frac.length - exp
258
+
259
+ // The same bound the `0d` literal obeys (D6). Without it this API
260
+ // route reached the resource exhaustion the budget exists to
261
+ // prevent -- `fromString('1e1000000000')` would normalise by
262
+ // building a billion-digit coefficient -- and disagreed with the Go
263
+ // port, whose NewBigDecimal shares the literal path's checker.
264
+ if (overBudget(m[2].length + frac.length, scale)) {
265
+ throw new Error('decimal-budget: ' + src)
266
+ }
267
+
268
+ const unscaled = BigInt(m[2] + frac)
269
+ return new Decimal('-' === m[1] ? -unscaled : unscaled, scale)
270
+ }
271
+ }
272
+
273
+
274
+ // D6 -- THE BUDGET APPLIES TO RESULTS, NOT JUST LITERALS. True when a
275
+ // Decimal produced by arithmetic is over either bound and so must be
276
+ // refused with `decimal_budget`.
277
+ //
278
+ // The test is applied to the NORMAL form -- the representation the value
279
+ // is actually stored and rendered in -- rather than to some minimal
280
+ // re-derived source form. That keeps the rule cheap and mechanical
281
+ // enough for the Go port to mirror exactly (count the coefficient's
282
+ // digits, read the scale), and it bounds precisely the thing the bound
283
+ // exists to bound: the digits plain-form rendering will have to
284
+ // materialise.
285
+ //
286
+ // One consequence is worth naming: a literal is budget-checked on its
287
+ // SOURCE form, so an extreme literal whose normal form is wider than its
288
+ // source (`0d1e4096` -- one written digit, 4098 normalised ones) parses
289
+ // but cannot take part in arithmetic. Both bounds are deliberately far
290
+ // beyond any real document, and the alternative -- re-deriving a minimal
291
+ // form per operation -- only moves the same edge somewhere else
292
+ // (`0d100e4096` has minimal scale -4098, over the bound its source form
293
+ // was under), at the cost of a second normal form that both ports would
294
+ // then have to agree on digit for digit.
295
+ function decimalOverBudget(d: Decimal): boolean {
296
+ return overBudget(coeffDigits(d.unscaled), d.scale)
297
+ }
298
+
299
+
300
+ // D3 -- THE `0d` LITERAL.
301
+ //
302
+ // 0[dD] digits [ . digits ] [ (e|E) [+-] digits ]
303
+ //
304
+ // with single `_` separators between digits, exactly as the landed
305
+ // separator rule allows for ordinary numbers (`[0-9](?:_?[0-9])*` admits
306
+ // `1_000` and refuses `1__0`, `_1`, `1_`).
307
+ //
308
+ // LEAF BY SOURCE, mirroring R1's precedent that the source text and not
309
+ // the value decides the kind: digits only is a biginteger, a `.` or an
310
+ // exponent anywhere makes it a bigdecimal. So `0d5` is a biginteger and
311
+ // `0d1e3` is a bigdecimal whose value happens to be integral.
312
+ //
313
+ // THE SIGN IS NOT PART OF THIS PATTERN, though it is part of the D3
314
+ // grammar: `-0d5` is read as the existing unary-minus prefix applied to
315
+ // `0d5` (see negative-prefix in lang.ts), exactly as `-1.5` already is.
316
+ // Aontu's lexer runs regexp value matchers BEFORE the fixed-token
317
+ // matcher, so a `[-+]?` here would claim the `+` of `0d1 +0d2` as a
318
+ // literal sign and silently turn an addition into an implicit list --
319
+ // while plain `1 +2` stayed an addition. The sign belongs to the same
320
+ // operator for both, which is also what keeps `0d-5` rejected.
321
+ //
322
+ // Kept RE2-compatible (no lookaround, no backreferences) so the Go port
323
+ // can use the byte-identical pattern.
324
+ const BIG_LITERAL_RE =
325
+ /^0[dD]([0-9](?:_?[0-9])*)(?:\.([0-9](?:_?[0-9])*))?(?:[eE]([-+]?[0-9](?:_?[0-9])*))?/
326
+
327
+
328
+ // The outcome of reading a `0d` literal: one of the two exact leaves, or
329
+ // a budget refusal. `undefined` is impossible for text the matcher above
330
+ // claimed, and is reported as an error rather than guessed at.
331
+ type BigLiteral =
332
+ { leaf: 'biginteger', int: bigint } |
333
+ { leaf: 'bigdecimal', dec: Decimal } |
334
+ { leaf: 'error', code: string }
335
+
336
+
337
+ function stripSep(s: string): string {
338
+ return -1 === s.indexOf('_') ? s : s.replace(/_/g, '')
339
+ }
340
+
341
+
342
+ // Read a `0d` literal from the groups of BIG_LITERAL_RE:
343
+ // 1 integer digits, 2 fraction digits, 3 exponent.
344
+ function readBigLiteral(m: RegExpExecArray | (string | undefined)[]): BigLiteral {
345
+ const intd = stripSep(m[1] as string)
346
+ const fracd = null == m[2] ? undefined : stripSep(m[2])
347
+ const expd = null == m[3] ? undefined : stripSep(m[3])
348
+
349
+ // Leaf by SOURCE, not by value.
350
+ if (undefined === fracd && undefined === expd) {
351
+ return { leaf: 'biginteger', int: BigInt(intd) }
352
+ }
353
+
354
+ // The exponent is read as a plain number on purpose: a 4000-digit
355
+ // exponent lands on Infinity, whose absolute value is over budget, so
356
+ // the refusal below fires without any bigint ever being built.
357
+ const exp = undefined === expd ? 0 : Number(expd)
358
+ const fracLen = undefined === fracd ? 0 : fracd.length
359
+ const scale = fracLen - exp
360
+
361
+ // The budget is checked on the SOURCE form, before normalisation:
362
+ // normalising `0d1e1000000000` is itself the resource event the bound
363
+ // exists to prevent. Coefficient digits are counted as written
364
+ // (leading zeros included) so the two ports need not agree on any
365
+ // cleverer rule.
366
+ if (overBudget(intd.length + fracLen, scale)) {
367
+ return { leaf: 'error', code: 'decimal_budget' }
368
+ }
369
+
370
+ return {
371
+ leaf: 'bigdecimal',
372
+ dec: new Decimal(BigInt(intd + (fracd ?? '')), scale),
373
+ }
374
+ } /* node:coverage ignore next 12 */
375
+
376
+
377
+ export {
378
+ BIG_LITERAL_RE,
379
+ BigLiteral,
380
+ DECIMAL_COEFFICIENT_BUDGET,
381
+ DECIMAL_SCALE_BUDGET,
382
+ Decimal,
383
+ decimalOverBudget,
384
+ readBigLiteral,
385
+ }
@@ -278,44 +278,8 @@ class DisjunctVal extends JunctionVal {
278
278
  }
279
279
 
280
280
  return super.gen(ctx)
281
-
282
- // // // console.log('DJ-GEN', this.peg)
283
-
284
- // if (1 === this.peg.length) {
285
- // return this.peg[0].gen(ctx)
286
- // }
287
- // else if (1 < this.peg.length) {
288
- // let peg = this.peg.filter((v: Val) => v instanceof PrefVal)
289
-
290
- // if (1 === peg.length) {
291
- // return peg[0].gen(ctx)
292
- // }
293
- // else {
294
-
295
- // let nil = Nil.make(
296
- // ctx,
297
- // 'disjunct',
298
- // this,
299
- // undefined
300
- // )
301
-
302
- // // TODO: refactor to use Site
303
- // nil.path = this.path
304
- // nil.site.url = this.url
305
- // nil.site.row = this.row
306
- // nil.site.col = this.col
307
-
308
- // // descErr(nil, ctx)
309
-
310
- // if (null == ctx) {
311
- // throw new Error(nil.msg)
312
- // }
313
- // }
314
-
315
- // return undefined
316
- // }
317
281
  }
318
- }
282
+ } /* node:coverage ignore next 8 */
319
283
 
320
284
 
321
285
 
@@ -87,7 +87,7 @@ class ExpectVal extends FeatureVal {
87
87
  ',parent=' + this.parent?.inspect(d)
88
88
  }
89
89
 
90
- }
90
+ } /* node:coverage ignore next 6 */
91
91
 
92
92
 
93
93
  export {
@@ -48,7 +48,7 @@ abstract class FeatureVal extends Val {
48
48
  return undefined
49
49
  }
50
50
 
51
- }
51
+ } /* node:coverage ignore next 6 */
52
52
 
53
53
 
54
54
  export {
@@ -197,7 +197,7 @@ class FuncBaseVal extends FeatureVal {
197
197
  }
198
198
 
199
199
 
200
- }
200
+ } /* node:coverage ignore next 6 */
201
201
 
202
202
 
203
203
  export {
@@ -77,7 +77,7 @@ class HideFuncVal extends FuncBaseVal {
77
77
 
78
78
  return out
79
79
  }
80
- }
80
+ } /* node:coverage ignore next 6 */
81
81
 
82
82
 
83
83
  export {
@@ -13,6 +13,7 @@ import { makeNilErr, AontuError } from '../err'
13
13
 
14
14
  import { ScalarVal } from './ScalarVal'
15
15
  import { Integer } from './ScalarKindVal'
16
+ import { isIntegerKind, integerDigits } from './numkind'
16
17
 
17
18
  import {
18
19
  explainOpen,
@@ -24,13 +25,88 @@ import {
24
25
  class IntegerVal extends ScalarVal {
25
26
  isInteger = true
26
27
 
28
+
29
+ // Canon renders the EXACT digits of the value, not JavaScript's
30
+ // shortest round-tripping form.
31
+ //
32
+ // A JS number's toString emits the shortest decimal that parses back
33
+ // to the same double — at most 17 significant digits — so 2^60 printed
34
+ // as `1152921504606847000`, a DIFFERENT integer that merely rounds to
35
+ // the same double, while the Go port printed the true
36
+ // `1152921504606846976` from its int64. That was the one entry in the
37
+ // parity ledger (issue #21).
38
+ //
39
+ // Neither rendering was "the wrong number" while lossy literals were
40
+ // still accepted, which is why the ledger deferred to the Phase 6
41
+ // refusal — but Phase 6 refuses INEXACT literals, and every value in
42
+ // this window (2^60, 2^63-1024, …) is exactly representable and so is
43
+ // accepted. The divergence was never about exactness; it was about
44
+ // rendering. Once the value is exactly what the source asked for,
45
+ // there IS a right answer, and it is the exact digits.
46
+ //
47
+ // BigInt is exact here by construction: an IntegerVal's peg is
48
+ // integral and inside the int64 window (isIntegerKind, enforced in the
49
+ // constructor), so the bigint conversion cannot lose anything.
50
+ get canon() {
51
+ return integerDigits(this.peg as number)
52
+ }
53
+
54
+
55
+ // The same exactness in GENERATED output -- the other half of #21, and
56
+ // the half canon cannot reach.
57
+ //
58
+ // `generate()` returns NATIVE values, and the emitter sees only those:
59
+ // by the time exactJSON has a JS number it can no longer tell an
60
+ // integer-kind 2^60 (whose exact digits are the answer) from a
61
+ // float-kind 1e21 (whose shortest form `1e+21` is the answer, and is
62
+ // what Go prints too). The kind has to travel WITH the value, so above
63
+ // the safe-integer range an integer-kind value generates as a `bigint`,
64
+ // which the emitter already writes as exact digits.
65
+ //
66
+ // WHY Number.isSafeInteger IS THE LINE. It is precisely "this double is
67
+ // an integer that renders its own exact digits": inside the range the
68
+ // integers are contiguous, so toString round-trips exactly and a
69
+ // `number` loses nothing. Outside it they are not, and JS itself
70
+ // declares the value untrustworthy -- so handing back a `number` there
71
+ // would give a consumer something that prints wrong AND does
72
+ // arithmetic wrong, silently. A bigint makes both explicit: mixing it
73
+ // with a number throws rather than quietly rounding.
74
+ //
75
+ // The line is deliberately the WELL-KNOWN one rather than the tightest
76
+ // possible one. Some doubles above the range do still render exactly
77
+ // (2^54, say), so a predicate of `BigInt(peg).toString() === String(peg)`
78
+ // would keep marginally more values as `number` -- at the cost of a
79
+ // contract no consumer could evaluate in their head, and of handing
80
+ // back unsafe numbers for arithmetic. "Integers up to
81
+ // Number.MAX_SAFE_INTEGER are numbers; beyond that, bigints" is a rule
82
+ // that can be stated once and relied on.
83
+ //
84
+ // This is a BREAKING change for a consumer reading integers above 2^53
85
+ // out of generate() -- but those were the values that were already
86
+ // silently wrong, and a TypeError on `out.x + 1` is the better failure.
87
+ // Go needs none of this: its integer leaf is an int64, exact across the
88
+ // whole window, and encoding/json writes its digits.
89
+ //
90
+ // The BYTES are unchanged below the line, and the values that cross it
91
+ // with 17 or fewer significant digits (2^53 itself) serialise the same
92
+ // either way -- only their runtime type moves, which is why the `gens`
93
+ // rows cannot see it and ts/test/exactjson.test.ts pins it instead.
94
+ gen(_ctx?: AontuContext) {
95
+ const peg = this.peg as number
96
+ return Number.isSafeInteger(peg) ? peg : BigInt(peg)
97
+ }
98
+
27
99
  constructor(
28
100
  spec: ValSpec,
29
101
  ctx?: AontuContext
30
102
  ) {
31
- if (!Number.isInteger(spec.peg)) {
32
- // TODO: use Nil?
33
- throw new AontuError('not-integer: ' + spec.peg)
103
+ // An IntegerVal must hold a value of *integer kind*: integral AND
104
+ // within the int64 range, so it can never hold something the Go
105
+ // port's int64 storage could not (see isIntegerKind). Every
106
+ // construction site pre-checks with the same helper, so reaching
107
+ // here is a programming error, not user input — hence a throw.
108
+ if (!isIntegerKind(spec.peg)) {
109
+ throw new AontuError('not-integer-kind: ' + spec.peg)
34
110
  }
35
111
  // super({ peg: spec.peg, kind: Integer }, ctx)
36
112
  super({ ...spec, kind: Integer }, ctx)
@@ -42,7 +118,7 @@ class IntegerVal extends ScalarVal {
42
118
  let out: Val = this
43
119
 
44
120
  if (null != peer) {
45
- if (peer.isScalarKind) {
121
+ if (peer.isScalarKind || (peer as any).isConstraint) {
46
122
  out = peer.unify(this, te ? ctx.clone({ explain: ec(te, 'KND') }) : ctx)
47
123
  }
48
124
  else if (
@@ -68,7 +144,7 @@ class IntegerVal extends ScalarVal {
68
144
 
69
145
  return out
70
146
  }
71
- }
147
+ } /* node:coverage ignore next 6 */
72
148
 
73
149
 
74
150
  export {
@@ -46,7 +46,7 @@ abstract class JunctionVal extends FeatureVal {
46
46
 
47
47
  // Abstract method to be implemented by subclasses to define their junction symbol
48
48
  abstract getJunctionSymbol(): string
49
- }
49
+ } /* node:coverage ignore next 6 */
50
50
 
51
51
 
52
52
  export {
@@ -17,6 +17,7 @@ import { ConjunctVal } from '../val/ConjunctVal'
17
17
 
18
18
 
19
19
  import { FuncBaseVal } from './FuncBaseVal'
20
+ import { makeNilErr } from '../err'
20
21
 
21
22
 
22
23
  class KeyFuncVal extends FuncBaseVal {
@@ -75,12 +76,43 @@ class KeyFuncVal extends FuncBaseVal {
75
76
  }
76
77
 
77
78
 
78
- resolve(_ctx: AontuContext, _args: Val[]) {
79
+ resolve(ctx: AontuContext, _args: Val[]) {
79
80
  let out: Val = this
80
81
 
81
82
  // if (!this.mark.type && !this.mark.hide) {
82
- let move = this.peg?.[0]?.peg
83
- move = isNaN(move) ? 1 : +move
83
+ //
84
+ // THE LEVEL MUST BE AN INTEGER, OR ABSENT.
85
+ //
86
+ // A level is an index into the path (0 the own key, the default 1 the
87
+ // parent), so the argument is an integer or it is a mistake. Both
88
+ // exact integer leaves qualify -- `integer` and `biginteger` -- and
89
+ // everything else is refused rather than silently meaning "parent",
90
+ // which is what made a mistyped level undetectable.
91
+ //
92
+ // This also removes a crash. The test used to be
93
+ // `isNaN(move) ? 1 : +move` with the COERCING global isNaN, which is
94
+ // ToNumber, and ToNumber THROWS for a bigint (a biginteger's peg) and
95
+ // for the null-prototype object a map's peg is -- neither has a
96
+ // toString or valueOf to call. The exception escaped into unify's
97
+ // catch-all and surfaced as an opaque [aontu/internal].
98
+ const argval: any = this.peg?.[0]
99
+ let move = 1
100
+
101
+ if (null != argval) {
102
+ if (argval.isInteger) {
103
+ move = argval.peg as number
104
+ }
105
+ else if (argval.isBigInteger) {
106
+ // A level far outside the path simply misses, exactly as an
107
+ // out-of-range plain integer already does, so Number() here needs
108
+ // no bound of its own.
109
+ move = Number(argval.peg as bigint)
110
+ }
111
+ else {
112
+ return makeNilErr(ctx, 'key_level', this)
113
+ }
114
+ }
115
+
84
116
  const key = this.path[this.path.length - (1 + move)] ?? ''
85
117
  // console.log('KEY', this.path, move, key)
86
118
 
@@ -95,7 +127,7 @@ class KeyFuncVal extends FuncBaseVal {
95
127
  return undefined
96
128
  }
97
129
 
98
- }
130
+ } /* node:coverage ignore next 6 */
99
131
 
100
132
 
101
133
  export {