aontu 0.50.0 → 0.51.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 (102) hide show
  1. package/dist/aontu.d.ts +4 -2
  2. package/dist/aontu.js +18 -2
  3. package/dist/aontu.js.map +1 -1
  4. package/dist/cli.js +33 -7
  5. package/dist/cli.js.map +1 -1
  6. package/dist/exactjson.d.ts +37 -0
  7. package/dist/exactjson.js +228 -0
  8. package/dist/exactjson.js.map +1 -0
  9. package/dist/hints.js +61 -4
  10. package/dist/hints.js.map +1 -1
  11. package/dist/keyorder.d.ts +9 -0
  12. package/dist/keyorder.js +65 -0
  13. package/dist/keyorder.js.map +1 -0
  14. package/dist/lang.js +200 -4
  15. package/dist/lang.js.map +1 -1
  16. package/dist/lsp.js +12 -2
  17. package/dist/lsp.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/dist/val/BagVal.js +8 -5
  20. package/dist/val/BagVal.js.map +1 -1
  21. package/dist/val/BigDecimalVal.d.ts +13 -0
  22. package/dist/val/BigDecimalVal.js +80 -0
  23. package/dist/val/BigDecimalVal.js.map +1 -0
  24. package/dist/val/BigIntegerVal.d.ts +11 -0
  25. package/dist/val/BigIntegerVal.js +75 -0
  26. package/dist/val/BigIntegerVal.js.map +1 -0
  27. package/dist/val/Decimal.d.ts +32 -0
  28. package/dist/val/Decimal.js +318 -0
  29. package/dist/val/Decimal.js.map +1 -0
  30. package/dist/val/IntegerVal.d.ts +2 -0
  31. package/dist/val/IntegerVal.js +75 -3
  32. package/dist/val/IntegerVal.js.map +1 -1
  33. package/dist/val/KeyFuncVal.d.ts +1 -1
  34. package/dist/val/KeyFuncVal.js +33 -3
  35. package/dist/val/KeyFuncVal.js.map +1 -1
  36. package/dist/val/LowerFuncVal.js +20 -3
  37. package/dist/val/LowerFuncVal.js.map +1 -1
  38. package/dist/val/MapVal.js +6 -4
  39. package/dist/val/MapVal.js.map +1 -1
  40. package/dist/val/MoveFuncVal.js +25 -8
  41. package/dist/val/MoveFuncVal.js.map +1 -1
  42. package/dist/val/NumberVal.d.ts +1 -0
  43. package/dist/val/NumberVal.js +23 -2
  44. package/dist/val/NumberVal.js.map +1 -1
  45. package/dist/val/PathFuncVal.js +9 -1
  46. package/dist/val/PathFuncVal.js.map +1 -1
  47. package/dist/val/PlusOpVal.d.ts +3 -1
  48. package/dist/val/PlusOpVal.js +140 -29
  49. package/dist/val/PlusOpVal.js.map +1 -1
  50. package/dist/val/PrefVal.d.ts +2 -0
  51. package/dist/val/PrefVal.js +40 -4
  52. package/dist/val/PrefVal.js.map +1 -1
  53. package/dist/val/RefVal.js +57 -2
  54. package/dist/val/RefVal.js.map +1 -1
  55. package/dist/val/ScalarKindVal.d.ts +12 -2
  56. package/dist/val/ScalarKindVal.js +100 -11
  57. package/dist/val/ScalarKindVal.js.map +1 -1
  58. package/dist/val/ScalarVal.d.ts +1 -0
  59. package/dist/val/ScalarVal.js +16 -1
  60. package/dist/val/ScalarVal.js.map +1 -1
  61. package/dist/val/SuperFuncVal.d.ts +1 -1
  62. package/dist/val/SuperFuncVal.js +14 -1
  63. package/dist/val/SuperFuncVal.js.map +1 -1
  64. package/dist/val/UpperFuncVal.js +20 -3
  65. package/dist/val/UpperFuncVal.js.map +1 -1
  66. package/dist/val/VarVal.js +7 -1
  67. package/dist/val/VarVal.js.map +1 -1
  68. package/dist/val/numkind.d.ts +6 -0
  69. package/dist/val/numkind.js +224 -0
  70. package/dist/val/numkind.js.map +1 -0
  71. package/dist/val/valutil.d.ts +1 -0
  72. package/dist/val/valutil.js +35 -0
  73. package/dist/val/valutil.js.map +1 -1
  74. package/package.json +7 -7
  75. package/src/aontu.ts +23 -2
  76. package/src/cli.ts +36 -8
  77. package/src/exactjson.ts +254 -0
  78. package/src/hints.ts +73 -4
  79. package/src/keyorder.ts +73 -0
  80. package/src/lang.ts +229 -5
  81. package/src/lsp.ts +10 -2
  82. package/src/val/BagVal.ts +8 -5
  83. package/src/val/BigDecimalVal.ts +117 -0
  84. package/src/val/BigIntegerVal.ts +110 -0
  85. package/src/val/Decimal.ts +385 -0
  86. package/src/val/IntegerVal.ts +79 -3
  87. package/src/val/KeyFuncVal.ts +35 -3
  88. package/src/val/LowerFuncVal.ts +21 -4
  89. package/src/val/MapVal.ts +6 -4
  90. package/src/val/MoveFuncVal.ts +26 -9
  91. package/src/val/NumberVal.ts +25 -2
  92. package/src/val/PathFuncVal.ts +9 -1
  93. package/src/val/PlusOpVal.ts +168 -32
  94. package/src/val/PrefVal.ts +66 -5
  95. package/src/val/RefVal.ts +61 -2
  96. package/src/val/ScalarKindVal.ts +115 -10
  97. package/src/val/ScalarVal.ts +18 -1
  98. package/src/val/SuperFuncVal.ts +14 -1
  99. package/src/val/UpperFuncVal.ts +21 -4
  100. package/src/val/VarVal.ts +7 -1
  101. package/src/val/numkind.ts +256 -0
  102. package/src/val/valutil.ts +36 -0
@@ -0,0 +1,254 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ /*
4
+ * D9 -- THE EXACT JSON EMITTER.
5
+ *
6
+ * `generate()` returns NATIVE values, and once a document opts into the
7
+ * exact leaves two of them are not things `JSON.stringify` can write: a
8
+ * biginteger generates as a native `bigint`, and a bigdecimal generates
9
+ * as a `Decimal`. `JSON.stringify` THROWS on a bigint ("Do not know how
10
+ * to serialize a BigInt") and a `replacer` cannot rescue it, because a
11
+ * replacer may only return another *value* -- everything it returns that
12
+ * is not already a JSON primitive gets quoted, so the exact digits could
13
+ * only come back as a JSON *string*, which is a different document.
14
+ *
15
+ * JSON itself was never the obstacle. A JSON number is arbitrary-
16
+ * precision decimal TEXT: `{"x":9007199254740993}` is a perfectly legal
17
+ * document that every conforming parser reads, and Go's `encoding/json`
18
+ * already emits exactly that for a `*big.Int`. Only JavaScript's
19
+ * serialiser stands in the way, so aontu ships its own.
20
+ *
21
+ * WHY THIS IS PUBLIC API and not a CLI internal: D9 promises that an
22
+ * exact value reaches JSON as exact digits. A library consumer whose
23
+ * document contains `0d9007199254740993` has no other supported way to
24
+ * keep that promise -- hiding the only implementation behind the command
25
+ * line would leave the contract unfulfillable from the library. The
26
+ * `aontu` CLI consumes this same export (with `indent`), and so does the
27
+ * shared suite's byte-exact `gens` mode, so there is exactly one set of
28
+ * bytes to keep in step with the Go port.
29
+ *
30
+ * PARITY WITH GO. The output must match Go's
31
+ * `json.Encoder` + `SetEscapeHTML(false)` (see specGens in
32
+ * go/spec_test.go) byte for byte:
33
+ *
34
+ * - Key order is the order `generate()` produced. Both ports already
35
+ * sort map keys (see the entries sort in BagVal.gen; Go's encoder
36
+ * sorts map keys), so neither has to sort again here.
37
+ * - Strings keep JavaScript's escaping, which is what the `gens` mode
38
+ * settled on when it turned Go's HTML escaping OFF so that `<`, `>`
39
+ * and `&` stay literal in both ports. U+2028/U+2029 are the one
40
+ * place JS and Go disagree by default -- Go escapes them, JS does
41
+ * not -- so they are escaped here too, which is both legal JSON and
42
+ * the safer output (it can be embedded in a JavaScript source).
43
+ * - Numbers keep JavaScript's `Number.prototype.toString` form, which
44
+ * is already what Go's encoder produces: fixed notation inside
45
+ * [1e-6, 1e21) and an unpadded, always-signed exponent outside it
46
+ * (`1e+21`, `1e-7`).
47
+ * - A bigint writes its digits; a Decimal writes its plain digit form
48
+ * (`1000.0`, `0.1`, `-1.5`) -- no `0d` marker, since that belongs to
49
+ * canon and is not JSON, but an integral bigdecimal keeps its `.0`
50
+ * so the JSON still shows a decimal. Go's `*Decimal.MarshalJSON`
51
+ * mirrors this.
52
+ *
53
+ * NOTE that byte-exact output CANNOT check the other half of D9: an
54
+ * integral bigdecimal and a biginteger can serialise to different text,
55
+ * but a biginteger and an ordinary integer serialise to the SAME text
56
+ * while `generate()` returned the wrong runtime type. Canon pins the AST
57
+ * kind, `gens` pins these bytes, and only the per-port API tests
58
+ * (ts/test/exactjson.test.ts here) pin the returned object.
59
+ */
60
+
61
+ import { Decimal } from './val/Decimal'
62
+ import { AontuError } from './err'
63
+ import { cmpCodePoint } from './keyorder'
64
+
65
+
66
+ // JS leaves U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
67
+ // literal in a JSON string; Go's encoder escapes them. Byte parity wins.
68
+ const LSPS_RE = new RegExp('[\u2028\u2029]', 'g')
69
+
70
+
71
+ // JSON.stringify's `space` argument, normalised the way the spec does:
72
+ // a number is clamped to 0..10 spaces, a string is truncated to 10
73
+ // characters, anything else means "compact".
74
+ function indentUnit(indent: number | string | undefined): string {
75
+ if ('number' === typeof indent) {
76
+ const n = Math.min(10, Math.floor(indent))
77
+ return 0 < n ? ' '.repeat(n) : ''
78
+ }
79
+ if ('string' === typeof indent) {
80
+ return indent.slice(0, 10)
81
+ }
82
+ return ''
83
+ }
84
+
85
+
86
+ function quote(s: string): string {
87
+ const out = JSON.stringify(s)
88
+ // A raw separator can only appear as itself in the output (never
89
+ // inside an escape), so a straight replace is safe.
90
+ return out.replace(LSPS_RE, (c: string) =>
91
+ '\u2028' === c ? '\\u2028' : '\\u2029')
92
+ }
93
+
94
+
95
+ // True for a value JSON.stringify omits from an object and writes as
96
+ // `null` inside an array. generate() never produces one, but a consumer
97
+ // splicing its own data into the result might.
98
+ function skipped(v: any): boolean {
99
+ return undefined === v || 'function' === typeof v || 'symbol' === typeof v
100
+ }
101
+
102
+
103
+ // `seen` tracks the containers on the CURRENT path only (added on entry,
104
+ // removed on exit), so a shared subtree -- which unification can easily
105
+ // produce -- serialises fine and only a true cycle is refused, exactly
106
+ // as JSON.stringify does it.
107
+ function emit(v: any, unit: string, pad: string, seen: Set<any>): string {
108
+ if (null === v) {
109
+ return 'null'
110
+ }
111
+
112
+ const t = typeof v
113
+
114
+ if ('string' === t) {
115
+ return quote(v)
116
+ }
117
+ if ('number' === t) {
118
+ // JSON has no NaN, no Infinity and no negative zero: JSON.stringify
119
+ // writes `null` for the first two and `0` for the last, and gen()
120
+ // has already normalised -0 away (R2).
121
+ return JSON.stringify(v) as string
122
+ }
123
+ if ('bigint' === t) {
124
+ // THE POINT OF THIS MODULE: exact digits, as a raw JSON number.
125
+ return v.toString()
126
+ }
127
+ if ('boolean' === t) {
128
+ return v ? 'true' : 'false'
129
+ }
130
+ if (skipped(v)) {
131
+ return 'null'
132
+ }
133
+
134
+ // Before any other object handling: a Decimal is a value, not a
135
+ // structure, and its plain digit form is the JSON number.
136
+ if (v instanceof Decimal) {
137
+ return v.toString()
138
+ }
139
+
140
+ if ('object' === t && 'function' === typeof v.toJSON) {
141
+ return emit(v.toJSON(), unit, pad, seen)
142
+ }
143
+
144
+ if (seen.has(v)) {
145
+ throw new AontuError('aontu: cannot convert circular structure to JSON')
146
+ }
147
+ seen.add(v)
148
+
149
+ const inner = pad + unit
150
+ const nl = '' === unit ? '' : '\n'
151
+ let out: string
152
+
153
+ if (Array.isArray(v)) {
154
+ if (0 === v.length) {
155
+ out = '[]'
156
+ }
157
+ else {
158
+ const parts = v.map((e: any) =>
159
+ inner + emit(skipped(e) ? null : e, unit, inner, seen))
160
+ out = '[' + nl + parts.join(',' + nl) + nl + pad + ']'
161
+ }
162
+ }
163
+ else {
164
+ const colon = '' === unit ? ':' : ': '
165
+ const parts: string[] = []
166
+ // OWN enumerable keys, SORTED. A `for ... in` here would additionally
167
+ // walk the prototype chain.
168
+ //
169
+ // WHY THE SORT IS HERE AND NOT IN gen(). BagVal.gen already sorts map
170
+ // entries lexicographically -- and then writes them into a plain JS
171
+ // object, where ECMAScript throws that sort away. OrdinaryOwnPropertyKeys
172
+ // lists canonical ARRAY-INDEX keys first, in ascending NUMERIC order,
173
+ // and only then the remaining string keys in insertion order. So
174
+ // `{9:1,10:2}` came back out as 9 before 10, and `{"!":1,9:2}` put the
175
+ // digit key ahead of the punctuation one.
176
+ //
177
+ // This is not a V8 quirk and not a JSON.stringify quirk; it is the
178
+ // object's own key order, and it CANNOT be fixed upstream, because no
179
+ // JavaScript object can hold "10" before "9". The emitter is the last
180
+ // point that still has the freedom to choose, so it chooses here.
181
+ //
182
+ // The giveaway that it was never a design rule: only indices up to
183
+ // 2^32-2 are hoisted, so `{4294967295:1,4294967296:2,5:3}` emitted as
184
+ // 5, 4294967295, 4294967296 -- and TypeScript's own CANON, which builds
185
+ // text from a sorted list rather than from an object, was lexicographic
186
+ // all along. The port contradicted itself.
187
+ //
188
+ // Lexicographic by UTF-16 code unit, which is what `<` on JS strings
189
+ // does and what MapVal.canon/BagVal.gen already sort by. Go sorts map
190
+ // keys by UTF-8 byte, and the two orders agree on every key that stays
191
+ // inside the BMP; astral-plane keys are a separate, tracked divergence.
192
+ // CODE POINT order (cmpCodePoint), which is what Go's UTF-8 byte sort
193
+ // produces. A bare .sort() is UTF-16 code-unit order, which puts an
194
+ // astral key ahead of everything in U+E000-U+FFFF.
195
+ for (const k of Object.keys(v).sort(cmpCodePoint)) {
196
+ const cv = v[k]
197
+ if (skipped(cv)) {
198
+ continue
199
+ }
200
+ parts.push(inner + quote(k) + colon + emit(cv, unit, inner, seen))
201
+ }
202
+ out = 0 === parts.length ? '{}' :
203
+ '{' + nl + parts.join(',' + nl) + nl + pad + '}'
204
+ }
205
+
206
+ seen.delete(v)
207
+
208
+ return out
209
+ }
210
+
211
+
212
+ /**
213
+ * Serialise a value produced by `Aontu.generate()` as JSON text,
214
+ * preserving EXACT numbers.
215
+ *
216
+ * Use this instead of `JSON.stringify` on generated output. A document
217
+ * that uses the `0d` exact leaves generates native `bigint` and
218
+ * `Decimal` values; `JSON.stringify` throws on the first and mangles the
219
+ * second, while this emitter writes both as raw JSON numbers with their
220
+ * exact digits -- which is what the D9 generate contract promises, and
221
+ * what the Go port's marshallers produce for the same document.
222
+ *
223
+ * ```ts
224
+ * import { Aontu, exactJSON } from 'aontu'
225
+ *
226
+ * const out = new Aontu().generate('x:0d9007199254740993')
227
+ * typeof out.x // 'bigint'
228
+ * exactJSON(out) // '{"x":9007199254740993}'
229
+ * exactJSON(out, 2) // '{\n "x": 9007199254740993\n}'
230
+ * JSON.stringify(out) // TypeError: Do not know how to serialize a BigInt
231
+ * ```
232
+ *
233
+ * @param value The value to serialise -- normally `generate()` output:
234
+ * `null`, booleans, numbers, strings, `bigint`,
235
+ * `Decimal`, arrays and plain objects. An object with a
236
+ * `toJSON` method is asked for its replacement first
237
+ * (`Decimal` is handled as a number before that check).
238
+ * @param indent Optional indentation, with `JSON.stringify`'s `space`
239
+ * semantics: a number of spaces (clamped to 0..10) or a
240
+ * literal string. Omitted or `0` gives COMPACT output
241
+ * (no spaces, no newlines) -- the form the shared spec
242
+ * suite's `gens` mode compares byte for byte.
243
+ * @returns The JSON text. Unlike `JSON.stringify` this always
244
+ * returns a string: a top-level `undefined` is `null`.
245
+ * @throws {AontuError} if the value contains a reference cycle.
246
+ */
247
+ function exactJSON(value: any, indent?: number | string): string {
248
+ return emit(value, indentUnit(indent), '', new Set())
249
+ }
250
+
251
+
252
+ export {
253
+ exactJSON,
254
+ }
package/src/hints.ts CHANGED
@@ -16,14 +16,14 @@ const hints: Record<string, string> = {
16
16
  ' 1 & 1 -> 1 # Does unify (equal Integers);\n' +
17
17
  ' a & a -> a # Does unify (equal Strings);\n' +
18
18
  ' 1 & 2 -> nil # Does not unify (unequal Integers);\n' +
19
- ' 1 & 1.0 -> nil # Does not unify (kinds: Integer & Number).',
19
+ ' 1 & 1.0 -> nil # Does not unify (kinds: Integer & Float).',
20
20
 
21
21
  scalar_kind:
22
22
  'Literal scalar values of different kinds cannot unify.' +
23
23
  '\n \nExamples:\n' +
24
24
  ' 1 & 1 -> 1 # Does unify (equal Integers);\n' +
25
25
  ' 1 & a -> nil # Does not unify (Kinds: Integer & String);\n' +
26
- ' 1 & 1.0 -> nil # Does not unify (kinds: Integer & Number).',
26
+ ' 1 & 1.0 -> nil # Does not unify (kinds: Integer & Float).',
27
27
 
28
28
  nil_gen:
29
29
  'The nil value was present after unification, and nil cannot be\n' +
@@ -82,8 +82,76 @@ const hints: Record<string, string> = {
82
82
 
83
83
  // Type mismatch errors
84
84
 
85
- 'scalar-type': 'Scalar type mismatch. The scalar types are incompatible.',
86
- 'no_scalar_unify': 'Cannot unify scalar values. The scalar values have incompatible types.',
85
+ decimal_budget:
86
+ 'This exact decimal exceeds the exactness budget: at most 4096\n' +
87
+ 'coefficient digits and an absolute scale of at most 4096. The\n' +
88
+ 'budget applies to computed results as well as to literals.\n' +
89
+ 'Aontu never rounds, so a value beyond the budget is refused\n' +
90
+ 'rather than approximated.' +
91
+ '\n \nExamples:\n' +
92
+ ' 0d1e1000000000 -> nil # Scale far beyond the budget;\n' +
93
+ ' 0d1e4000+0d1e-4000 -> nil # An exact sum too wide to hold;\n' +
94
+ ' 0d1e-1 -> 0d0.1 # Well within it.',
95
+
96
+ lossy_integer_literal:
97
+ 'This integer literal, {src}, is not exactly representable in\n' +
98
+ 'binary64, so storing it would silently round it to a DIFFERENT\n' +
99
+ 'number. Aontu refuses rather than corrupts: write it as a `0d`\n' +
100
+ 'literal to get the exact integer.\n' +
101
+ 'The rule is exactness, not magnitude -- a literal far outside the\n' +
102
+ 'int64 window is still a value when it lands exactly on a binary64.' +
103
+ '\n \nExamples:\n' +
104
+ ' 9007199254740992 -> 9007199254740992 # 2^53, exact;\n' +
105
+ ' 9007199254740993 -> nil # 2^53+1 is not;\n' +
106
+ ' 0d9007199254740993 -> 0d9007199254740993 # ... the exact escape;\n' +
107
+ ' 0x7fffffffffffffff -> nil # 2^63-1 rounds up to 2^63;\n' +
108
+ ' 100000000000000000000 -> 1e20 # 10^20 is huge and exact.',
109
+
110
+ exact_float_mix:
111
+ 'Aontu cannot mix an exact number with a binary float.\n' +
112
+ 'Here the operands are {left} and {right}, in that order.\n' +
113
+ 'A big type never silently becomes a binary float, in either\n' +
114
+ 'operand order -- binary64 cannot hold every exact value, so the\n' +
115
+ 'promotion would throw away the exactness the `0d` leaves exist to\n' +
116
+ 'guarantee. Write both operands in the same family (`0d1.0` for the\n' +
117
+ 'float, or a plain integer for the big).' +
118
+ '\n \nExamples:\n' +
119
+ ' 0d2 + 0d0.5 -> 0d2.5 # Exact with exact (widest leaf wins);\n' +
120
+ ' 1 + 0d0.5 -> 0d1.5 # integer is on the exact ladder;\n' +
121
+ ' 1 + 2.0 -> 3.0 # ... and float still mixes with integer;\n' +
122
+ ' 1.0 + 0d2 -> nil # float with biginteger;\n' +
123
+ ' 0d0.5 + 1.0 -> nil # ... and the same the other way round.',
124
+
125
+ inexact_integer_sum:
126
+ 'The `integer` leaf holds a value only when it is integral, within\n' +
127
+ 'the int64 range, and exactly representable in binary64. This sum\n' +
128
+ 'is not: {sum}.\n' +
129
+ 'Aontu adds integers exactly and refuses to store a rounded answer\n' +
130
+ '-- write `0d<digits>` for an exact integer beyond that window.' +
131
+ '\n \nExamples:\n' +
132
+ ' 4503599627370496 + 4503599627370496 -> 9007199254740992 # Exact;\n' +
133
+ ' 4503599627370496 + 4503599627370497 -> nil # 2^53+1 is not;\n' +
134
+ ' 0d4503599627370496 + 0d4503599627370497 -> 0d9007199254740993.',
135
+
136
+ 'scalar-type':
137
+ 'Scalar kinds only unify when one contains the other. `number` is\n' +
138
+ 'the supertype of the numeric leaves (integer, float, biginteger,\n' +
139
+ 'bigdecimal), so meeting it with a leaf gives that leaf; two distinct\n' +
140
+ 'leaves describe disjoint sets of values and so have no common lower\n' +
141
+ 'bound.' +
142
+ '\n \nExamples:\n' +
143
+ ' number & integer -> integer # Does unify (integer is a number);\n' +
144
+ ' number & number -> number # Does unify (same kind);\n' +
145
+ ' float & integer -> nil # Does not unify (disjoint leaves).',
146
+
147
+ 'no_scalar_unify':
148
+ 'Cannot unify scalar values. The scalar values have incompatible types.' +
149
+ '\n \nExamples:\n' +
150
+ ' number & 1 -> 1 # Does unify (1 is a number);\n' +
151
+ ' integer & 1 -> 1 # Does unify (1 is an integer);\n' +
152
+ ' float & 1 -> nil # Does not unify (1 is an integer, not a float);\n' +
153
+ ' integer & 1.5 -> nil # Does not unify (1.5 is a float, not an integer).',
154
+
87
155
  'not-scalar-type': 'Expected a scalar type but got a non-scalar type.',
88
156
  'map': 'Type mismatch: expected a map value but got a different type.',
89
157
  'list': 'Type mismatch: expected a list value but got a different type.',
@@ -91,6 +159,7 @@ const hints: Record<string, string> = {
91
159
  // Argument errors
92
160
  'arg': 'Missing required argument. A function requires an argument but none was provided.',
93
161
  'invalid-arg': 'Invalid argument provided. The argument does not match the expected type or format.',
162
+ 'key_level': 'The argument to key() is a LEVEL: how many steps up the path to look, where 0 is the key of the value itself and the default 1 is its parent. It must therefore be an integer -- `key(2)`, or `key(0d2)` for the exact leaf. A float, a decimal, a string, a boolean, a map or a list is not a level. A level beyond the top of the path is not an error; it yields the empty string.',
94
163
  'no_first_arg': 'Missing first argument. The function requires a first argument but none was provided.',
95
164
 
96
165
  // Variable errors
@@ -0,0 +1,73 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ /*
4
+ * THE ONE MAP-KEY ORDER, shared by every site that emits keys.
5
+ *
6
+ * Map key order is not meaningful in unification, so both ports sort to
7
+ * make output independent of insertion and unification order. The order
8
+ * they sort into has to be the SAME order, and it was not.
9
+ *
10
+ * JavaScript compares strings by UTF-16 CODE UNIT. An astral character is
11
+ * stored as a surrogate pair beginning D800-DBFF, so `<` puts it BEFORE
12
+ * every character in U+E000-U+FFFF -- ahead of the private use area, of
13
+ * U+FFFD, of U+FFFF. Go sorts map keys as UTF-8 bytes, which is code
14
+ * POINT order, so it puts the astral character last. The two agree on
15
+ * everything below U+D800 and disagree above U+DFFF, which is why this
16
+ * survived: the shared suite had no key above the BMP at all, and three
17
+ * separate comments in the tree asserted the ports already agreed.
18
+ *
19
+ * CODE POINT ORDER WINS, and not merely because Go had it. Go's
20
+ * `generate()` returns a native map, so a consumer's own `json.Marshal`
21
+ * sorts byte-wise no matter what this engine chooses -- picking UTF-16
22
+ * order would leave the TypeScript output disagreeing with what a Go
23
+ * consumer produces from the same document.
24
+ *
25
+ * NOT Buffer.compare, which would be the obvious shortcut. A lone
26
+ * surrogate has no valid UTF-8 encoding, so Buffer.from replaces it with
27
+ * U+FFFD and two DISTINCT keys compare equal -- reintroducing exactly the
28
+ * insertion-order dependence the sort exists to remove. Iterating code
29
+ * points keeps distinct keys distinct.
30
+ */
31
+
32
+
33
+ /**
34
+ * Compare two map keys by Unicode CODE POINT.
35
+ *
36
+ * `String.prototype[Symbol.iterator]` yields whole code points, pairing
37
+ * surrogates, so this is a plain lexicographic compare of code point
38
+ * sequences -- the order Go's UTF-8 byte sort produces.
39
+ */
40
+ function cmpCodePoint(a: string, b: string): number {
41
+ // Fast path: identical strings, and the common all-BMP case where the
42
+ // two orders coincide, still walk the iterator -- but the loop exits at
43
+ // the first differing code point, so it is one pass over the shared
44
+ // prefix rather than any conversion or allocation.
45
+ const ai = a[Symbol.iterator]()
46
+ const bi = b[Symbol.iterator]()
47
+
48
+ for (; ;) {
49
+ const x = ai.next()
50
+ const y = bi.next()
51
+
52
+ // A prefix sorts before the string it is a prefix of, and two equal
53
+ // strings compare 0 -- the same rule a byte compare gives.
54
+ if (x.done) {
55
+ return y.done ? 0 : -1
56
+ }
57
+ if (y.done) {
58
+ return 1
59
+ }
60
+
61
+ const xc = (x.value as string).codePointAt(0) as number
62
+ const yc = (y.value as string).codePointAt(0) as number
63
+
64
+ if (xc !== yc) {
65
+ return xc < yc ? -1 : 1
66
+ }
67
+ }
68
+ }
69
+
70
+
71
+ export {
72
+ cmpCodePoint,
73
+ }