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
@@ -15,7 +15,8 @@ import { makeNilErr } from '../err'
15
15
 
16
16
  import { NilVal } from '../val/NilVal'
17
17
  import { ScalarKindVal } from '../val/ScalarKindVal'
18
- import { makeScalar } from '../val/valutil'
18
+ import { makeScalarLike } from '../val/valutil'
19
+ import { Decimal } from '../val/Decimal'
19
20
 
20
21
 
21
22
 
@@ -43,14 +44,30 @@ class UpperFuncVal extends FuncBaseVal {
43
44
 
44
45
 
45
46
  resolve(ctx: AontuContext | undefined, args: Val[]) {
46
- const oldpeg = args?.[0].peg
47
+ // A missing argument (`upper()`) must produce an invalid-arg error
48
+ // value, as the Go port does — reading .peg off nothing threw a
49
+ // TypeError that the unifier could only report as an opaque
50
+ // internal error.
51
+ const arg = args?.[0]
52
+ const oldpeg = arg?.peg
47
53
  const peg = 'string' === typeof oldpeg ? oldpeg.toUpperCase() :
48
54
  'number' === typeof oldpeg ? Math.ceil(oldpeg) :
49
- undefined
55
+ // The exact leaves take an EXACT ceiling and keep their kind: a
56
+ // biginteger is already integral so it is its own ceiling, and a
57
+ // bigdecimal ceils by coefficient arithmetic. Math.ceil is not
58
+ // an option for either — it would round the value into binary64
59
+ // first, which is the loss the `0d` leaves exist to refuse.
60
+ 'bigint' === typeof oldpeg ? oldpeg :
61
+ oldpeg instanceof Decimal ? oldpeg.ceil() :
62
+ undefined
50
63
  const out = this.place(
51
64
  null == peg ?
52
65
  makeNilErr(ctx, 'invalid-arg', this) :
53
- makeScalar(peg)
66
+ // The ceiling keeps the ARGUMENT's kind (upper(2) is an integer
67
+ // 2, upper(1.1) is a number 2) — the function must not narrow
68
+ // number to integer, and this also makes the actual result kind
69
+ // agree with the superior() advertised below.
70
+ makeScalarLike(peg, arg)
54
71
  )
55
72
  return out
56
73
  }
@@ -64,7 +81,7 @@ class UpperFuncVal extends FuncBaseVal {
64
81
  })) :
65
82
  super.superior()
66
83
  }
67
- }
84
+ } /* node:coverage ignore next 6 */
68
85
 
69
86
 
70
87
  export {
package/src/val/Val.ts CHANGED
@@ -148,11 +148,13 @@ abstract class Val {
148
148
 
149
149
  deps?: any
150
150
 
151
- #ctx: any
151
+ // TS-private (as _site above): the `#` form emits downlevel helper
152
+ // functions whose branches no supported Node can execute.
153
+ private _ctx: any
152
154
 
153
155
  // TODO: Site needed in ctor
154
156
  constructor(spec: ValSpec, ctx?: AontuContext) {
155
- this.#ctx = ctx
157
+ this._ctx = ctx
156
158
 
157
159
  this.peg = spec?.peg
158
160
 
@@ -179,7 +181,7 @@ abstract class Val {
179
181
 
180
182
 
181
183
  ctx() {
182
- return this.#ctx
184
+ return this._ctx
183
185
  }
184
186
 
185
187
 
@@ -217,9 +219,12 @@ abstract class Val {
217
219
 
218
220
  out.dc = this.done ? DONE : out.dc
219
221
 
220
- out.site.row = spec?.row ?? this.site.row ?? -1
221
- out.site.col = spec?.col ?? this.site.col ?? -1
222
- out.site.url = spec?.url ?? this.site.url ?? ''
222
+ // this.site is a lazy getter that always yields a Site, and Site's
223
+ // constructor coerces row/col to numbers and url to a string, so the
224
+ // spec value is the only one that can be absent.
225
+ out.site.row = spec?.row ?? this.site.row
226
+ out.site.col = spec?.col ?? this.site.col
227
+ out.site.url = spec?.url ?? this.site.url
223
228
 
224
229
  out.mark = Object.assign({}, this.mark, fullspec.mark ?? {})
225
230
  out.mark.type = this.mark.type && (fullspec.mark?.type ?? true)
@@ -427,7 +432,7 @@ function inspectpeg(peg: any, d: number) {
427
432
  ('{' +
428
433
  Object.entries(peg).map((n: any) =>
429
434
  '\n ' + indent + n[0] + ': ' + // n[1].inspect(d)
430
- (n[1].inspect(d) ?? '' + n[1])
435
+ n[1].inspect(d)
431
436
  ).join(',') +
432
437
  '\n' + indent + '}')
433
438
  )
@@ -448,7 +453,7 @@ function empty(o: any) {
448
453
  || (null != o && 'object' === typeof o && 0 === Object.keys(o).length)
449
454
  || false
450
455
  )
451
- }
456
+ } /* node:coverage ignore next 16 */
452
457
 
453
458
 
454
459
 
package/src/val/VarVal.ts CHANGED
@@ -30,6 +30,7 @@ import { NullVal } from './NullVal'
30
30
  import { BooleanVal } from './BooleanVal'
31
31
  import { NumberVal } from './NumberVal'
32
32
  import { IntegerVal } from './IntegerVal'
33
+ import { isIntegerKind } from './numkind'
33
34
 
34
35
 
35
36
  import {
@@ -106,7 +107,12 @@ class VarVal extends FeatureVal {
106
107
  out = new BooleanVal({ peg: found })
107
108
  }
108
109
  else if ('number' === ft) {
109
- out = Number.isInteger(found) ?
110
+ // A raw var binding carries no source text, so the integral +
111
+ // int64-range conditions decide the kind (same helper as the
112
+ // val rule and rawToVal).
113
+ // (ctx.vars is declared as Record<string, Val> but also
114
+ // carries raw scalars, which is what `ft` has just proven.)
115
+ out = isIntegerKind(found as unknown as number) ?
110
116
  new IntegerVal({ peg: found }) :
111
117
  new NumberVal({ peg: found })
112
118
  }
@@ -183,7 +189,7 @@ class VarVal extends FeatureVal {
183
189
 
184
190
  return undefined
185
191
  }
186
- }
192
+ } /* node:coverage ignore next 6 */
187
193
 
188
194
 
189
195
  export {
@@ -0,0 +1,189 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ /*
4
+ * Exact cross-leaf numeric comparison for the constraint algebra
5
+ * (docs/reference-language.md, "The constraint algebra"): order is a
6
+ * property of the number line, not the leaf, so a bound must compare an
7
+ * integer, a float, a biginteger and a bigdecimal EXACTLY, with no
8
+ * rounding anywhere. Every finite binary64 is exactly a rational —
9
+ * mant * 2^exp — and 2^-k = 5^k * 10^-k, so every leaf converts
10
+ * losslessly to a signed scaled-decimal (unscaled / 10^scale) and
11
+ * comparison is bigint arithmetic. The Go mirror is numcmp.go.
12
+ *
13
+ * The scaled forms built here are comparison-internal: they are never
14
+ * stored, never rendered, and deliberately NOT subject to the exact
15
+ * leaves' 4096-digit budget (a float's exact expansion can need ~770
16
+ * digits and that is fine for a compare).
17
+ */
18
+
19
+ import { Decimal } from './Decimal'
20
+
21
+
22
+ // A comparison-internal exact rational with a power-of-ten denominator.
23
+ // `inf` marks the two binary64 infinities, which some float leaf value
24
+ // could in principle carry; NaN cannot reach a scalar (the parser has
25
+ // no spelling for it) and is rejected by the atom constructors.
26
+ type Scaled = {
27
+ inf?: 1 | -1
28
+ unscaled: bigint // signed
29
+ scale: number // value = unscaled / 10^scale, scale >= 0
30
+ }
31
+
32
+
33
+ const F64 = new DataView(new ArrayBuffer(8))
34
+
35
+
36
+ // The exact scaled-decimal value of a finite or infinite binary64.
37
+ function scaledOfFloat(f: number): Scaled {
38
+ if (Number.isInteger(f) && Math.abs(f) <= Number.MAX_SAFE_INTEGER) {
39
+ return { unscaled: BigInt(f), scale: 0 }
40
+ }
41
+ if (Infinity === f) {
42
+ return { inf: 1, unscaled: 0n, scale: 0 }
43
+ }
44
+ if (-Infinity === f) {
45
+ return { inf: -1, unscaled: 0n, scale: 0 }
46
+ }
47
+
48
+ F64.setFloat64(0, f)
49
+ const hi = BigInt(F64.getUint32(0))
50
+ const lo = BigInt(F64.getUint32(4))
51
+ const bits = (hi << 32n) | lo
52
+
53
+ const sign = (bits >> 63n) & 1n ? -1n : 1n
54
+ const expBits = Number((bits >> 52n) & 0x7ffn)
55
+ const frac = bits & 0xfffffffffffffn
56
+
57
+ // Normal: implicit leading bit, exponent bias 1023 plus the 52
58
+ // fraction bits. Subnormal: no implicit bit, fixed exponent -1074.
59
+ let mant: bigint
60
+ let exp: number
61
+ if (0 === expBits) {
62
+ mant = frac
63
+ exp = -1074
64
+ }
65
+ else {
66
+ mant = frac | (1n << 52n)
67
+ exp = expBits - 1075
68
+ }
69
+
70
+ if (0 <= exp) {
71
+ return { unscaled: sign * (mant << BigInt(exp)), scale: 0 }
72
+ }
73
+ // mant * 2^-k == mant * 5^k / 10^k, exactly.
74
+ return { unscaled: sign * mant * 5n ** BigInt(-exp), scale: -exp }
75
+ }
76
+
77
+
78
+ // The exact scaled-decimal value of any numeric leaf Val.
79
+ function scaledOfNumeric(v: any): Scaled {
80
+ if (v.isBigInteger) {
81
+ return { unscaled: v.peg as bigint, scale: 0 }
82
+ }
83
+ if (v.isBigDecimal) {
84
+ const d = v.peg as Decimal
85
+ return { unscaled: d.unscaled, scale: d.scale }
86
+ }
87
+ // integer and float leaves both hold a binary64.
88
+ return scaledOfFloat(v.peg as number)
89
+ }
90
+
91
+
92
+ function pow10(n: number): bigint {
93
+ return 10n ** BigInt(n)
94
+ }
95
+
96
+
97
+ // Exact three-way comparison of two scaled decimals.
98
+ function cmpScaled(a: Scaled, b: Scaled): number {
99
+ const ai = a.inf ?? 0
100
+ const bi = b.inf ?? 0
101
+ if (0 !== ai || 0 !== bi) {
102
+ return ai < bi ? -1 : ai > bi ? 1 : 0
103
+ }
104
+ let au = a.unscaled
105
+ let bu = b.unscaled
106
+ if (a.scale < b.scale) {
107
+ au *= pow10(b.scale - a.scale)
108
+ }
109
+ else if (b.scale < a.scale) {
110
+ bu *= pow10(a.scale - b.scale)
111
+ }
112
+ return au < bu ? -1 : au > bu ? 1 : 0
113
+ }
114
+
115
+
116
+ // Exact three-way comparison of two numeric leaf Vals, any leaves.
117
+ function cmpNumeric(a: any, b: any): number {
118
+ return cmpScaled(scaledOfNumeric(a), scaledOfNumeric(b))
119
+ }
120
+
121
+
122
+ // Lexical comparison by Unicode CODE POINTS — not UTF-16 code units,
123
+ // which order astral-plane text differently. Go compares strings
124
+ // byte-wise in UTF-8, which IS code-point order, so this is the side
125
+ // that must adapt (a Phase 0 decision, docs/reference-language.md).
126
+ function cmpCodePoints(a: string, b: string): number {
127
+ let ai = 0
128
+ let bi = 0
129
+ while (ai < a.length && bi < b.length) {
130
+ const ac = a.codePointAt(ai) as number
131
+ const bc = b.codePointAt(bi) as number
132
+ if (ac !== bc) {
133
+ return ac < bc ? -1 : 1
134
+ }
135
+ ai += ac > 0xffff ? 2 : 1
136
+ bi += bc > 0xffff ? 2 : 1
137
+ }
138
+ const ar = a.length - ai
139
+ const br = b.length - bi
140
+ return ar < br ? -1 : ar > br ? 1 : 0
141
+ }
142
+
143
+
144
+ // The tower order integer < float < biginteger < bigdecimal, used when
145
+ // two endpoints at the SAME point meet: the survivor is the
146
+ // tower-lowest spelling (docs/reference-language.md, bounds ruling 2).
147
+ function towerRank(v: any): number {
148
+ return v.isBigDecimal ? 3 : v.isBigInteger ? 2 :
149
+ v.isInteger ? 0 : 1
150
+ }
151
+
152
+
153
+ // True when the scaled value is integral (an exact whole number).
154
+ function scaledIsIntegral(s: Scaled): boolean {
155
+ if (s.inf) {
156
+ return false
157
+ }
158
+ if (0 === s.scale) {
159
+ return true
160
+ }
161
+ const p = pow10(s.scale)
162
+ return 0n === s.unscaled % p
163
+ }
164
+
165
+
166
+ // The exact floor of a scaled value, as a bigint. Infinite inputs are
167
+ // the caller's job to exclude.
168
+ function scaledFloor(s: Scaled): bigint {
169
+ if (0 === s.scale) {
170
+ return s.unscaled
171
+ }
172
+ const p = pow10(s.scale)
173
+ const q = s.unscaled / p
174
+ // BigInt division truncates toward zero; floor rounds down.
175
+ return (s.unscaled < 0n && 0n !== s.unscaled % p) ? q - 1n : q
176
+ } /* node:coverage ignore next 14 */
177
+
178
+
179
+ export {
180
+ Scaled,
181
+ scaledOfFloat,
182
+ scaledOfNumeric,
183
+ cmpScaled,
184
+ cmpNumeric,
185
+ cmpCodePoints,
186
+ towerRank,
187
+ scaledIsIntegral,
188
+ scaledFloor,
189
+ }
@@ -0,0 +1,256 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ /*
4
+ * Numeric kind classification, shared by every site that turns a raw
5
+ * JavaScript number into an IntegerVal or a NumberVal (the aontu val
6
+ * rule, rawToVal, VarVal, the `+` operator, upper()/lower()). Keeping
7
+ * the test in one place stops those sites from drifting apart, and
8
+ * stops them drifting from the Go port (see isIntegerKind in
9
+ * go/lang.go).
10
+ *
11
+ * This module deliberately imports nothing: IntegerVal itself needs the
12
+ * test for its constructor guard, so any import here risks a cycle.
13
+ */
14
+
15
+
16
+ // The int64 range, as exact float64 bounds. -2^63 and 2^63 are both
17
+ // exactly representable; 2^63-1 is NOT (it rounds up to 2^63), which is
18
+ // why the upper bound is exclusive and 0x7fffffffffffffff correctly
19
+ // falls outside.
20
+ const INT64_MIN = -9223372036854775808.0
21
+ const INT64_LIMIT = 9223372036854775808.0
22
+
23
+
24
+ // True when a numeric literal has *integer* kind. All three conditions
25
+ // must hold:
26
+ // (a) the source text, when there is any, contains no '.'
27
+ // (b) the value is integral
28
+ // (c) the value lies within the int64 range
29
+ //
30
+ // The range test is written against the float64 bounds rather than as
31
+ // `n === Number(BigInt(n))` or Go's `n == float64(int64(n))`: converting
32
+ // an out-of-range float64 to int64 is implementation-defined in Go, and
33
+ // that accident is exactly what the two ports used to disagree about.
34
+ //
35
+ // `src` is undefined at construction sites with no source text (raw
36
+ // values from an implicit top-level list, or a $var binding); there
37
+ // condition (a) is vacuous and (b)+(c) decide.
38
+ function isIntegerKind(n: number, src?: string): boolean {
39
+ if (null != src && src.includes('.')) {
40
+ return false
41
+ }
42
+ // Number.isInteger is false for NaN and both infinities, so a
43
+ // non-finite value never reaches the range test.
44
+ return Number.isInteger(n) && INT64_MIN <= n && n < INT64_LIMIT
45
+ }
46
+
47
+
48
+ // The same int64 window, exactly, for values that were computed as
49
+ // bigints and have not been through a double yet.
50
+ const INT64_MIN_EXACT = -9223372036854775808n
51
+ const INT64_LIMIT_EXACT = 9223372036854775808n
52
+
53
+
54
+ // D6 -- THE STORAGE CONTRACT FOR AN EXACTLY COMPUTED INTEGER.
55
+ //
56
+ // `integer + integer` is computed exactly (in bigint here, in checked
57
+ // int64 in Go) and the exact answer is only then offered to the integer
58
+ // leaf, which accepts it when it is integral (a bigint always is),
59
+ // within the int64 window, AND exactly representable in binary64.
60
+ //
61
+ // The binary64 half is the parity-critical one. Go's int64 can hold sums
62
+ // this port's double cannot -- 2^53+1 among them -- so a test written
63
+ // only against the int64 window would let Go store `9007199254740993`
64
+ // while TypeScript silently stored `...992`. The round-trip through
65
+ // Number() is that test: it is true exactly when nothing was lost. Both
66
+ // ports must refuse the same sums, which is why this is the shared
67
+ // contract and not a local convenience.
68
+ //
69
+ // Refusal, not rounding: a sum that fails here is a located error whose
70
+ // hint points at `0d` for an exact integer, per R1's own storage rule
71
+ // applied to a result rather than a literal.
72
+ function isIntegerStorable(n: bigint): boolean {
73
+ return INT64_MIN_EXACT <= n && n < INT64_LIMIT_EXACT &&
74
+ isExactInBinary64(n)
75
+ }
76
+
77
+
78
+ // The binary64 half of the storage contract, on its own: true when an
79
+ // exact integer survives a round trip through a double unchanged.
80
+ //
81
+ // D7's literal rule needs exactly this half and NOT the int64 window --
82
+ // 10^20 is outside int64 and is still a perfectly good float-kind value,
83
+ // because it lands exactly on a binary64. THE RULE IS EXACTNESS, NOT
84
+ // MAGNITUDE: `0x10000000000000000000000000000000` is 2^124, a power of
85
+ // two, so it is exact and stays a value, while the very much smaller
86
+ // `0x7fffffffffffffff` (2^63-1) is not and is refused.
87
+ //
88
+ // It is factored out rather than reimplemented so a LITERAL and a
89
+ // COMPUTED SUM cannot disagree about what "exact" means: both ask this
90
+ // one function, in both ports.
91
+ //
92
+ // The Number.isFinite guard is what makes it total. A bigint far beyond
93
+ // binary64's range converts to Infinity, and BigInt(Infinity) THROWS --
94
+ // so a literal like 10^400 (which the lexer already turns into a
95
+ // non-finite `not_number`) would crash the test rather than fail it.
96
+ function isExactInBinary64(n: bigint): boolean {
97
+ const d = Number(n)
98
+ return Number.isFinite(d) && BigInt(d) === n
99
+ }
100
+
101
+
102
+ // D7 -- LOSSY INTEGER LITERALS ARE REFUSED.
103
+ //
104
+ // True when `src` is an integer-source literal whose exact value cannot
105
+ // be held in binary64, so storing it would silently substitute a
106
+ // DIFFERENT number. Aontu refuses instead: the caller turns this into a
107
+ // located parse error whose hint names the escape (`0d<digits>`).
108
+ //
109
+ // WHICH LITERALS ARE IN SCOPE. An integer-source literal is one that
110
+ // denotes an exact integer:
111
+ //
112
+ // - plain decimal digits (`9007199254740993`), with the landed `_`
113
+ // separator rule;
114
+ // - a base-prefixed run (`0x…`, `0o…`, `0b…`);
115
+ // - either of those with a NON-NEGATIVE exponent (`1e21`), which still
116
+ // denotes an integer.
117
+ //
118
+ // Everything else is out of scope and unchanged. A `.` in the source
119
+ // makes it a float literal by R1, so it is not an integer source at all;
120
+ // a NEGATIVE exponent (`2e-1`, `1e-400`) denotes a fraction, and testing
121
+ // those for binary64 exactness would refuse `2e-1` -- 0.2 is not exactly
122
+ // representable either -- which is emphatically not what D7 is about.
123
+ // The `0d` family never reaches here: it has its own matcher and its own
124
+ // exact leaves.
125
+ //
126
+ // The value is re-derived from the SOURCE TEXT rather than read off the
127
+ // lexed double `n`, because that double is the rounded value this rule
128
+ // exists to detect; comparing it with itself would always agree. `n` is
129
+ // passed only for the fast path, which is why the signature mirrors
130
+ // isIntegerKind's.
131
+ function isLossyIntegerLiteral(n: number, src?: string): boolean {
132
+ // No source text (a raw value from an implicit top-level list, a $var
133
+ // binding) means there is no literal to judge -- the same way R1's
134
+ // "no `.` in the source" condition is vacuous at those sites.
135
+ if (null == src) {
136
+ return false
137
+ }
138
+
139
+ // FAST PATH, and the one every ordinary document takes: below 2^53
140
+ // every integer is representable, so an integer literal whose ROUNDED
141
+ // value is strictly inside that window cannot have had a different
142
+ // exact value -- a different integer would be at least 1 away, and
143
+ // rounding here is accurate to half a unit. This runs at every numeric
144
+ // literal in every parse, so it must not allocate a bigint (2^53+1 has
145
+ // rounded value 2^53 and is deliberately NOT inside the window).
146
+ if (Number.isInteger(n) && -POW53 < n && n < POW53) {
147
+ return false
148
+ }
149
+ // A `.` makes it a float source (R1), out of scope: checked here, and
150
+ // not left to the patterns below, so the other common literal shape
151
+ // also leaves without touching a regexp.
152
+ if (-1 !== src.indexOf('.')) {
153
+ return false
154
+ }
155
+
156
+ let exact: bigint
157
+
158
+ const dec = DEC_INT_LITERAL_RE.exec(src)
159
+ if (null != dec) {
160
+ // A negative exponent does not denote an integer -- out of scope.
161
+ const exp = null == dec[3] ? 0 : Number(stripSep(dec[3]))
162
+ if (!(0 <= exp) || !Number.isFinite(exp)) {
163
+ return false
164
+ }
165
+ const coeff = BigInt(stripSep(dec[2]))
166
+ // Zero at any exponent is zero, and zero is exact -- test it before
167
+ // the exponent bound, which would otherwise refuse `0e500`.
168
+ if (0n === coeff) {
169
+ return false
170
+ }
171
+ // Beyond this the value cannot be finite in binary64, so it cannot
172
+ // be exact either -- and 10n ** BigInt(exp) must not be built.
173
+ if (MAX_EXPONENT < exp) {
174
+ return true
175
+ }
176
+ exact = coeff * 10n ** BigInt(exp)
177
+ }
178
+ else {
179
+ const base = BASE_INT_LITERAL_RE.exec(src)
180
+ if (null == base) {
181
+ return false
182
+ }
183
+ exact = BigInt('0' + base[2].toLowerCase() + stripSep(base[3]))
184
+ }
185
+
186
+ // The sign is irrelevant to exactness (binary64 is sign-symmetric) and
187
+ // is anyway lexed as the unary prefix, but accept it so the test does
188
+ // not depend on that.
189
+ return !isExactInBinary64(exact)
190
+ }
191
+
192
+
193
+ // `[+-]? digits [ (e|E) [+-] digits ]`, with the landed single-`_`-
194
+ // between-digits separator rule. No `.`: that is a float source (R1).
195
+ const DEC_INT_LITERAL_RE =
196
+ /^([-+]?)([0-9](?:_?[0-9])*)(?:[eE]([-+]?[0-9](?:_?[0-9])*))?$/
197
+
198
+ // `[+-]? 0(x|o|b) digits`. Hex digits are the widest set, and a bad
199
+ // digit for the base simply makes BigInt throw -- which cannot happen,
200
+ // since the lexer only produced a token for a well-formed run.
201
+ const BASE_INT_LITERAL_RE =
202
+ /^([-+]?)0([xXoObB])([0-9a-fA-F](?:_?[0-9a-fA-F])*)$/
203
+
204
+ // Above this the value cannot be finite in binary64, so it cannot be
205
+ // exact either, and 10n ** BigInt(exp) must not be built.
206
+ const MAX_EXPONENT = 400
207
+
208
+ // 2^53, the point at which the integers stop being contiguous in
209
+ // binary64 and so the first magnitude at which a literal can be lossy.
210
+ const POW53 = 9007199254740992
211
+
212
+
213
+ function stripSep(s: string): string {
214
+ return -1 === s.indexOf('_') ? s : s.replace(/_/g, '')
215
+ }
216
+
217
+
218
+ // THE SINGLE RENDERING OF AN INTEGER-KIND PEG. Every site that turns one
219
+ // into text must call this -- so that "how many rendering sites are there?"
220
+ // is answerable by grepping this function's callers.
221
+ //
222
+ // That question has been answered wrongly twice. Issue #21 was the
223
+ // divergence where TypeScript printed 2^60 as 1152921504606847000, a
224
+ // DIFFERENT integer that merely rounds to the same double, because a JS
225
+ // number's toString emits the shortest decimal that round-trips (at most 17
226
+ // significant digits) while Go printed the exact value from its int64. It
227
+ // was closed having fixed the two sites known at the time, canon and
228
+ // generate. A differential audit then found a THIRD (`+`'s string
229
+ // coercion) and, while fixing that, a FOURTH (a variable used as a path
230
+ // segment). Both were the same one-character mistake -- `String(peg)` or
231
+ // `'' + peg` on a JS number -- at a site nobody had enumerated.
232
+ //
233
+ // BigInt is exact here by construction: an integer-kind peg is integral and
234
+ // inside the int64 window (isIntegerKind, enforced by IntegerVal's
235
+ // constructor), so the conversion cannot throw or lose anything. Below 2^53
236
+ // nothing moves -- BigInt(n).toString() === String(n) for every safe
237
+ // integer, and both give "0" for negative zero.
238
+ //
239
+ // FLOAT KIND MUST NOT COME HERE. A float's shortest form IS the right
240
+ // answer and is what Go prints too (1e21 -> "1e+21"), so a caller must
241
+ // select on kind, not merely on the peg being a JS number.
242
+ function integerDigits(peg: number): string {
243
+ return BigInt(peg).toString()
244
+ } /* node:coverage ignore next 13 */
245
+
246
+
247
+ // The window bounds stay module-local: they are the rules' working
248
+ // parts, not API, and every caller wants isIntegerKind or
249
+ // isIntegerStorable.
250
+ export {
251
+ integerDigits,
252
+ isExactInBinary64,
253
+ isIntegerKind,
254
+ isIntegerStorable,
255
+ isLossyIntegerLiteral,
256
+ }
@@ -3,9 +3,14 @@
3
3
  import { AontuError } from '../err'
4
4
  import { ScalarVal } from './ScalarVal'
5
5
  import { NumberVal } from './NumberVal'
6
+ import { IntegerVal } from './IntegerVal'
6
7
  import { StringVal } from './StringVal'
7
8
  import { BooleanVal } from './BooleanVal'
8
9
  import { NullVal } from './NullVal'
10
+ import { BigIntegerVal } from './BigIntegerVal'
11
+ import { BigDecimalVal } from './BigDecimalVal'
12
+ import { Decimal } from './Decimal'
13
+ import { isIntegerKind } from './numkind'
9
14
 
10
15
 
11
16
  // TODO: move to FuncBaseVal
@@ -29,3 +34,34 @@ export function makeScalar(scalar: any): ScalarVal {
29
34
  throw new AontuError('Not a scalar: ' + scalar)
30
35
  }
31
36
  }
37
+
38
+
39
+ // Like makeScalar, but for a numeric result that must not narrow the
40
+ // kind of the value it was derived from (kind contagion: upper(2) is an
41
+ // integer 2, upper(1.1) is a number 2). `like` is the source Val whose
42
+ // kind is being carried over; anything that is not integer kind — and
43
+ // any result that has left the int64 range — yields a NumberVal.
44
+ //
45
+ // The two EXACT leaves need no `like`: unlike integer and float, which
46
+ // share the JavaScript `number` type and so can only be told apart by
47
+ // the value they came from, a bigint is a biginteger and a Decimal is a
48
+ // bigdecimal. Carrying the kind is automatic because the exact result
49
+ // types ARE the kinds — an exact ceiling of a bigdecimal is a Decimal,
50
+ // so upper(0d1.1) is bigdecimal 0d2.0 and upper(0d5) is biginteger 0d5.
51
+ //
52
+ // makeScalar keeps its own contract (every number becomes a NumberVal)
53
+ // for callers that have no kind to preserve.
54
+ export function makeScalarLike(scalar: any, like: any): ScalarVal {
55
+ if ('bigint' === typeof scalar) {
56
+ return new BigIntegerVal({ peg: scalar })
57
+ }
58
+ if (scalar instanceof Decimal) {
59
+ return new BigDecimalVal({ peg: scalar })
60
+ }
61
+ if ('number' === typeof scalar &&
62
+ true === like?.isInteger &&
63
+ isIntegerKind(scalar)) {
64
+ return new IntegerVal({ peg: scalar })
65
+ }
66
+ return makeScalar(scalar)
67
+ }