@elaraai/east 0.0.1-beta.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.
- package/LICENSE.md +682 -0
- package/README.md +276 -0
- package/dist/src/analyze.d.ts +95 -0
- package/dist/src/analyze.d.ts.map +1 -0
- package/dist/src/analyze.js +1110 -0
- package/dist/src/analyze.js.map +1 -0
- package/dist/src/ast.d.ts +263 -0
- package/dist/src/ast.d.ts.map +1 -0
- package/dist/src/ast.js +151 -0
- package/dist/src/ast.js.map +1 -0
- package/dist/src/ast_to_ir.d.ts +24 -0
- package/dist/src/ast_to_ir.d.ts.map +1 -0
- package/dist/src/ast_to_ir.js +834 -0
- package/dist/src/ast_to_ir.js.map +1 -0
- package/dist/src/builtins.d.ts +18 -0
- package/dist/src/builtins.d.ts.map +1 -0
- package/dist/src/builtins.js +1105 -0
- package/dist/src/builtins.js.map +1 -0
- package/dist/src/comparison.d.ts +28 -0
- package/dist/src/comparison.d.ts.map +1 -0
- package/dist/src/comparison.js +1017 -0
- package/dist/src/comparison.js.map +1 -0
- package/dist/src/compile.d.ts +22 -0
- package/dist/src/compile.d.ts.map +1 -0
- package/dist/src/compile.js +3260 -0
- package/dist/src/compile.js.map +1 -0
- package/dist/src/containers/ref.d.ts +106 -0
- package/dist/src/containers/ref.d.ts.map +1 -0
- package/dist/src/containers/ref.js +100 -0
- package/dist/src/containers/ref.js.map +1 -0
- package/dist/src/containers/sortedmap.d.ts +165 -0
- package/dist/src/containers/sortedmap.d.ts.map +1 -0
- package/dist/src/containers/sortedmap.js +237 -0
- package/dist/src/containers/sortedmap.js.map +1 -0
- package/dist/src/containers/sortedset.d.ts +185 -0
- package/dist/src/containers/sortedset.d.ts.map +1 -0
- package/dist/src/containers/sortedset.js +312 -0
- package/dist/src/containers/sortedset.js.map +1 -0
- package/dist/src/containers/variant.d.ts +131 -0
- package/dist/src/containers/variant.d.ts.map +1 -0
- package/dist/src/containers/variant.js +68 -0
- package/dist/src/containers/variant.js.map +1 -0
- package/dist/src/datetime_format/parse.d.ts +50 -0
- package/dist/src/datetime_format/parse.d.ts.map +1 -0
- package/dist/src/datetime_format/parse.js +908 -0
- package/dist/src/datetime_format/parse.js.map +1 -0
- package/dist/src/datetime_format/print.d.ts +35 -0
- package/dist/src/datetime_format/print.d.ts.map +1 -0
- package/dist/src/datetime_format/print.js +157 -0
- package/dist/src/datetime_format/print.js.map +1 -0
- package/dist/src/datetime_format/tokenize.d.ts +76 -0
- package/dist/src/datetime_format/tokenize.d.ts.map +1 -0
- package/dist/src/datetime_format/tokenize.js +271 -0
- package/dist/src/datetime_format/tokenize.js.map +1 -0
- package/dist/src/datetime_format/types.d.ts +99 -0
- package/dist/src/datetime_format/types.d.ts.map +1 -0
- package/dist/src/datetime_format/types.js +103 -0
- package/dist/src/datetime_format/types.js.map +1 -0
- package/dist/src/datetime_format/validate.d.ts +51 -0
- package/dist/src/datetime_format/validate.d.ts.map +1 -0
- package/dist/src/datetime_format/validate.js +208 -0
- package/dist/src/datetime_format/validate.js.map +1 -0
- package/dist/src/default.d.ts +21 -0
- package/dist/src/default.d.ts.map +1 -0
- package/dist/src/default.js +82 -0
- package/dist/src/default.js.map +1 -0
- package/dist/src/eastir.d.ts +33 -0
- package/dist/src/eastir.d.ts.map +1 -0
- package/dist/src/eastir.js +92 -0
- package/dist/src/eastir.js.map +1 -0
- package/dist/src/error.d.ts +13 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +8 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/expr/array.d.ts +1711 -0
- package/dist/src/expr/array.d.ts.map +1 -0
- package/dist/src/expr/array.js +1805 -0
- package/dist/src/expr/array.js.map +1 -0
- package/dist/src/expr/ast.d.ts +17 -0
- package/dist/src/expr/ast.d.ts.map +1 -0
- package/dist/src/expr/ast.js +302 -0
- package/dist/src/expr/ast.js.map +1 -0
- package/dist/src/expr/blob.d.ts +141 -0
- package/dist/src/expr/blob.d.ts.map +1 -0
- package/dist/src/expr/blob.js +198 -0
- package/dist/src/expr/blob.js.map +1 -0
- package/dist/src/expr/block.d.ts +201 -0
- package/dist/src/expr/block.d.ts.map +1 -0
- package/dist/src/expr/block.js +1505 -0
- package/dist/src/expr/block.js.map +1 -0
- package/dist/src/expr/boolean.d.ts +207 -0
- package/dist/src/expr/boolean.d.ts.map +1 -0
- package/dist/src/expr/boolean.js +261 -0
- package/dist/src/expr/boolean.js.map +1 -0
- package/dist/src/expr/datetime.d.ts +544 -0
- package/dist/src/expr/datetime.d.ts.map +1 -0
- package/dist/src/expr/datetime.js +980 -0
- package/dist/src/expr/datetime.js.map +1 -0
- package/dist/src/expr/dict.d.ts +1242 -0
- package/dist/src/expr/dict.d.ts.map +1 -0
- package/dist/src/expr/dict.js +1492 -0
- package/dist/src/expr/dict.js.map +1 -0
- package/dist/src/expr/expr.d.ts +95 -0
- package/dist/src/expr/expr.d.ts.map +1 -0
- package/dist/src/expr/expr.js +171 -0
- package/dist/src/expr/expr.js.map +1 -0
- package/dist/src/expr/float.d.ts +357 -0
- package/dist/src/expr/float.d.ts.map +1 -0
- package/dist/src/expr/float.js +637 -0
- package/dist/src/expr/float.js.map +1 -0
- package/dist/src/expr/function.d.ts +46 -0
- package/dist/src/expr/function.d.ts.map +1 -0
- package/dist/src/expr/function.js +58 -0
- package/dist/src/expr/function.js.map +1 -0
- package/dist/src/expr/index.d.ts +450 -0
- package/dist/src/expr/index.d.ts.map +1 -0
- package/dist/src/expr/index.js +423 -0
- package/dist/src/expr/index.js.map +1 -0
- package/dist/src/expr/integer.d.ts +256 -0
- package/dist/src/expr/integer.d.ts.map +1 -0
- package/dist/src/expr/integer.js +311 -0
- package/dist/src/expr/integer.js.map +1 -0
- package/dist/src/expr/libs/array.d.ts +106 -0
- package/dist/src/expr/libs/array.d.ts.map +1 -0
- package/dist/src/expr/libs/array.js +140 -0
- package/dist/src/expr/libs/array.js.map +1 -0
- package/dist/src/expr/libs/blob.d.ts +42 -0
- package/dist/src/expr/libs/blob.d.ts.map +1 -0
- package/dist/src/expr/libs/blob.js +70 -0
- package/dist/src/expr/libs/blob.js.map +1 -0
- package/dist/src/expr/libs/datetime.d.ts +479 -0
- package/dist/src/expr/libs/datetime.d.ts.map +1 -0
- package/dist/src/expr/libs/datetime.js +624 -0
- package/dist/src/expr/libs/datetime.js.map +1 -0
- package/dist/src/expr/libs/dict.d.ts +66 -0
- package/dist/src/expr/libs/dict.d.ts.map +1 -0
- package/dist/src/expr/libs/dict.js +77 -0
- package/dist/src/expr/libs/dict.js.map +1 -0
- package/dist/src/expr/libs/float.d.ts +299 -0
- package/dist/src/expr/libs/float.d.ts.map +1 -0
- package/dist/src/expr/libs/float.js +564 -0
- package/dist/src/expr/libs/float.js.map +1 -0
- package/dist/src/expr/libs/integer.d.ts +228 -0
- package/dist/src/expr/libs/integer.d.ts.map +1 -0
- package/dist/src/expr/libs/integer.js +398 -0
- package/dist/src/expr/libs/integer.js.map +1 -0
- package/dist/src/expr/libs/set.d.ts +59 -0
- package/dist/src/expr/libs/set.d.ts.map +1 -0
- package/dist/src/expr/libs/set.js +69 -0
- package/dist/src/expr/libs/set.js.map +1 -0
- package/dist/src/expr/libs/string.d.ts +71 -0
- package/dist/src/expr/libs/string.d.ts.map +1 -0
- package/dist/src/expr/libs/string.js +75 -0
- package/dist/src/expr/libs/string.js.map +1 -0
- package/dist/src/expr/never.d.ts +15 -0
- package/dist/src/expr/never.d.ts.map +1 -0
- package/dist/src/expr/never.js +12 -0
- package/dist/src/expr/never.js.map +1 -0
- package/dist/src/expr/null.d.ts +15 -0
- package/dist/src/expr/null.d.ts.map +1 -0
- package/dist/src/expr/null.js +12 -0
- package/dist/src/expr/null.js.map +1 -0
- package/dist/src/expr/ref.d.ts +103 -0
- package/dist/src/expr/ref.d.ts.map +1 -0
- package/dist/src/expr/ref.js +131 -0
- package/dist/src/expr/ref.js.map +1 -0
- package/dist/src/expr/regex_validation.d.ts +25 -0
- package/dist/src/expr/regex_validation.d.ts.map +1 -0
- package/dist/src/expr/regex_validation.js +130 -0
- package/dist/src/expr/regex_validation.js.map +1 -0
- package/dist/src/expr/set.d.ts +1071 -0
- package/dist/src/expr/set.d.ts.map +1 -0
- package/dist/src/expr/set.js +1137 -0
- package/dist/src/expr/set.js.map +1 -0
- package/dist/src/expr/string.d.ts +414 -0
- package/dist/src/expr/string.d.ts.map +1 -0
- package/dist/src/expr/string.js +683 -0
- package/dist/src/expr/string.js.map +1 -0
- package/dist/src/expr/struct.d.ts +48 -0
- package/dist/src/expr/struct.d.ts.map +1 -0
- package/dist/src/expr/struct.js +65 -0
- package/dist/src/expr/struct.js.map +1 -0
- package/dist/src/expr/types.d.ts +68 -0
- package/dist/src/expr/types.d.ts.map +1 -0
- package/dist/src/expr/types.js +6 -0
- package/dist/src/expr/types.js.map +1 -0
- package/dist/src/expr/variant.d.ts +137 -0
- package/dist/src/expr/variant.d.ts.map +1 -0
- package/dist/src/expr/variant.js +105 -0
- package/dist/src/expr/variant.js.map +1 -0
- package/dist/src/fuzz.d.ts +80 -0
- package/dist/src/fuzz.d.ts.map +1 -0
- package/dist/src/fuzz.js +300 -0
- package/dist/src/fuzz.js.map +1 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/internal.d.ts +36 -0
- package/dist/src/internal.d.ts.map +1 -0
- package/dist/src/internal.js +11 -0
- package/dist/src/internal.js.map +1 -0
- package/dist/src/ir.d.ts +1571 -0
- package/dist/src/ir.d.ts.map +1 -0
- package/dist/src/ir.js +56 -0
- package/dist/src/ir.js.map +1 -0
- package/dist/src/location.d.ts +48 -0
- package/dist/src/location.d.ts.map +1 -0
- package/dist/src/location.js +62 -0
- package/dist/src/location.js.map +1 -0
- package/dist/src/platform.d.ts +21 -0
- package/dist/src/platform.d.ts.map +1 -0
- package/dist/src/platform.js +8 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/serialization/beast.d.ts +39 -0
- package/dist/src/serialization/beast.d.ts.map +1 -0
- package/dist/src/serialization/beast.js +555 -0
- package/dist/src/serialization/beast.js.map +1 -0
- package/dist/src/serialization/beast2-stream.d.ts +38 -0
- package/dist/src/serialization/beast2-stream.d.ts.map +1 -0
- package/dist/src/serialization/beast2-stream.js +665 -0
- package/dist/src/serialization/beast2-stream.js.map +1 -0
- package/dist/src/serialization/beast2.d.ts +41 -0
- package/dist/src/serialization/beast2.d.ts.map +1 -0
- package/dist/src/serialization/beast2.js +489 -0
- package/dist/src/serialization/beast2.js.map +1 -0
- package/dist/src/serialization/binary-utils.d.ts +151 -0
- package/dist/src/serialization/binary-utils.d.ts.map +1 -0
- package/dist/src/serialization/binary-utils.js +929 -0
- package/dist/src/serialization/binary-utils.js.map +1 -0
- package/dist/src/serialization/east.d.ts +84 -0
- package/dist/src/serialization/east.d.ts.map +1 -0
- package/dist/src/serialization/east.js +1802 -0
- package/dist/src/serialization/east.js.map +1 -0
- package/dist/src/serialization/index.d.ts +11 -0
- package/dist/src/serialization/index.d.ts.map +1 -0
- package/dist/src/serialization/index.js +12 -0
- package/dist/src/serialization/index.js.map +1 -0
- package/dist/src/serialization/json.d.ts +36 -0
- package/dist/src/serialization/json.d.ts.map +1 -0
- package/dist/src/serialization/json.js +849 -0
- package/dist/src/serialization/json.js.map +1 -0
- package/dist/src/type_of_type.d.ts +115 -0
- package/dist/src/type_of_type.d.ts.map +1 -0
- package/dist/src/type_of_type.js +362 -0
- package/dist/src/type_of_type.js.map +1 -0
- package/dist/src/types.d.ts +648 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1631 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1802 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
import { printIdentifier, TypeWiden, isTypeEqual, NeverType, NullType, BooleanType, IntegerType, FloatType, StringType, DateTimeType, BlobType, ArrayType, SetType, DictType, StructType, VariantType } from "../types.js";
|
|
6
|
+
import { compareFor } from "../comparison.js";
|
|
7
|
+
import { SortedMap } from "../containers/sortedmap.js";
|
|
8
|
+
import { SortedSet } from "../containers/sortedset.js";
|
|
9
|
+
import { isVariant, variant } from "../containers/variant.js";
|
|
10
|
+
import { EastTypeValueType, toEastTypeValue } from "../type_of_type.js";
|
|
11
|
+
import { ref } from "../containers/ref.js";
|
|
12
|
+
class ParseError extends Error {
|
|
13
|
+
position;
|
|
14
|
+
path;
|
|
15
|
+
constructor(message, position, path = '') {
|
|
16
|
+
super(message);
|
|
17
|
+
this.position = position;
|
|
18
|
+
this.path = path;
|
|
19
|
+
this.name = "ParseError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Find the length of the common prefix between two path component arrays.
|
|
24
|
+
*/
|
|
25
|
+
function _commonPrefixLength(a, b) {
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < a.length && i < b.length && a[i] === b[i]) {
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert path component array to punctuated string for lookup/storage.
|
|
34
|
+
* Example: [".field", "[3]"] -> ".field[3]"
|
|
35
|
+
*/
|
|
36
|
+
function _pathToPunctuated(path) {
|
|
37
|
+
return path.join("");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute a relative reference string from currentPath to targetPath.
|
|
41
|
+
* Returns a string like "2#.foo[0]" or "1#"
|
|
42
|
+
*
|
|
43
|
+
* The format is: "upLevels#remaining_path_components_as_punctuated"
|
|
44
|
+
*/
|
|
45
|
+
function _encodeRelativeRef(currentPath, targetPath) {
|
|
46
|
+
const commonLen = _commonPrefixLength(currentPath, targetPath);
|
|
47
|
+
const upLevels = currentPath.length - commonLen;
|
|
48
|
+
const remaining = targetPath.slice(commonLen);
|
|
49
|
+
if (remaining.length === 0) {
|
|
50
|
+
return `${upLevels}#`;
|
|
51
|
+
}
|
|
52
|
+
const remainingStr = remaining.join("");
|
|
53
|
+
return `${upLevels}#${remainingStr}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decode a relative reference string and return the target path array.
|
|
57
|
+
* Input like "2#.foo[0]" returns the target path array.
|
|
58
|
+
* Input like "1#" returns the target path array.
|
|
59
|
+
*/
|
|
60
|
+
function _decodeRelativeRef(refStr, currentPath) {
|
|
61
|
+
const hashIdx = refStr.indexOf('#');
|
|
62
|
+
if (hashIdx === -1) {
|
|
63
|
+
throw new Error(`Invalid relative reference: ${refStr}`);
|
|
64
|
+
}
|
|
65
|
+
const upLevelStr = refStr.substring(0, hashIdx);
|
|
66
|
+
const remainingStr = refStr.substring(hashIdx + 1);
|
|
67
|
+
let upLevels;
|
|
68
|
+
try {
|
|
69
|
+
upLevels = parseInt(upLevelStr);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`Invalid relative reference: ${refStr}`);
|
|
73
|
+
}
|
|
74
|
+
if (upLevels < 0 || upLevels > currentPath.length) {
|
|
75
|
+
throw new Error(`Invalid relative reference: going up ${upLevels} levels from depth ${currentPath.length}`);
|
|
76
|
+
}
|
|
77
|
+
// Build target path
|
|
78
|
+
const targetPath = currentPath.slice(0, currentPath.length - upLevels);
|
|
79
|
+
// Add remaining components if any
|
|
80
|
+
if (remainingStr.length > 0) {
|
|
81
|
+
// Parse the remaining punctuated path
|
|
82
|
+
// Format: .field[0][key] etc.
|
|
83
|
+
let pos = 0;
|
|
84
|
+
while (pos < remainingStr.length) {
|
|
85
|
+
if (remainingStr[pos] === '.') {
|
|
86
|
+
// Identifier follows
|
|
87
|
+
pos++;
|
|
88
|
+
let end = pos;
|
|
89
|
+
while (end < remainingStr.length && /[a-zA-Z0-9_]/.test(remainingStr[end])) {
|
|
90
|
+
end++;
|
|
91
|
+
}
|
|
92
|
+
targetPath.push(`.${remainingStr.substring(pos, end)}`);
|
|
93
|
+
pos = end;
|
|
94
|
+
}
|
|
95
|
+
else if (remainingStr[pos] === '[') {
|
|
96
|
+
// Bracket expression
|
|
97
|
+
let end = pos + 1;
|
|
98
|
+
let depth = 1;
|
|
99
|
+
while (end < remainingStr.length && depth > 0) {
|
|
100
|
+
if (remainingStr[end] === '[')
|
|
101
|
+
depth++;
|
|
102
|
+
else if (remainingStr[end] === ']')
|
|
103
|
+
depth--;
|
|
104
|
+
end++;
|
|
105
|
+
}
|
|
106
|
+
targetPath.push(remainingStr.substring(pos, end));
|
|
107
|
+
pos = end;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
pos++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return targetPath;
|
|
115
|
+
}
|
|
116
|
+
export function encodeEastFor(type) {
|
|
117
|
+
// Convert EastType to EastTypeValue if necessary
|
|
118
|
+
if (!isVariant(type)) {
|
|
119
|
+
type = toEastTypeValue(type);
|
|
120
|
+
}
|
|
121
|
+
const textEncoder = new TextEncoder();
|
|
122
|
+
const printer = printFor(type);
|
|
123
|
+
return (x) => textEncoder.encode(printer(x, { refs: new Map(), currentPath: [] }));
|
|
124
|
+
}
|
|
125
|
+
export function decodeEastFor(type) {
|
|
126
|
+
// Convert EastType to EastTypeValue if necessary
|
|
127
|
+
if (!isVariant(type)) {
|
|
128
|
+
type = toEastTypeValue(type);
|
|
129
|
+
}
|
|
130
|
+
const textDecoder = new TextDecoder();
|
|
131
|
+
const parser = parseFor(type);
|
|
132
|
+
return (x) => {
|
|
133
|
+
const result = parser(textDecoder.decode(x));
|
|
134
|
+
if (result.success) {
|
|
135
|
+
return result.value;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new Error(`Failed to decode East value of type ${printFor(EastTypeValueType)(type)}: ${result.error}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function printFor(type, typeCtx = []) {
|
|
143
|
+
// Convert EastType to EastTypeValue if necessary
|
|
144
|
+
if (!isVariant(type)) {
|
|
145
|
+
type = toEastTypeValue(type);
|
|
146
|
+
}
|
|
147
|
+
if (type.type === "Never") {
|
|
148
|
+
return (_, _ctx) => { throw new Error(`Attempted to print value of type .Never`); };
|
|
149
|
+
}
|
|
150
|
+
else if (type.type === "Null") {
|
|
151
|
+
return (_, _ctx) => "null";
|
|
152
|
+
}
|
|
153
|
+
else if (type.type === "Boolean") {
|
|
154
|
+
return (x, _ctx) => x.toString();
|
|
155
|
+
}
|
|
156
|
+
else if (type.type === "Integer") {
|
|
157
|
+
return (x, _ctx) => x.toString();
|
|
158
|
+
}
|
|
159
|
+
else if (type.type === "Float") {
|
|
160
|
+
return (x, _ctx) => {
|
|
161
|
+
// Handle negative zero specially
|
|
162
|
+
if (Object.is(x, -0)) {
|
|
163
|
+
return "-0.0";
|
|
164
|
+
}
|
|
165
|
+
const str = x.toString();
|
|
166
|
+
// check if str is integer-like
|
|
167
|
+
if (/^-?\d+$/.test(str)) {
|
|
168
|
+
// append a .0 to indicate it's a float
|
|
169
|
+
return str + ".0";
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return str;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
else if (type.type === "String") {
|
|
177
|
+
return (x, _ctx) => JSON.stringify(x);
|
|
178
|
+
}
|
|
179
|
+
else if (type.type === "DateTime") {
|
|
180
|
+
return (x, _ctx) => x.toISOString().substring(0, 23);
|
|
181
|
+
}
|
|
182
|
+
else if (type.type === "Blob") {
|
|
183
|
+
return (x, _ctx) => `0x${[...x].map(b => b.toString(16).padStart(2, "0")).join("")}`;
|
|
184
|
+
}
|
|
185
|
+
else if (type.type === "Ref") {
|
|
186
|
+
let value_printer;
|
|
187
|
+
const ret = (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
188
|
+
// Check for mutable alias
|
|
189
|
+
if (ctx.refs.has(x)) {
|
|
190
|
+
const targetPath = ctx.refs.get(x);
|
|
191
|
+
const refStr = _encodeRelativeRef(ctx.currentPath, targetPath);
|
|
192
|
+
return refStr;
|
|
193
|
+
}
|
|
194
|
+
// Register this ref
|
|
195
|
+
ctx.refs.set(x, [...ctx.currentPath]);
|
|
196
|
+
// Serialize ref
|
|
197
|
+
ctx.currentPath.push(`[]`);
|
|
198
|
+
const str = value_printer(x.value, ctx);
|
|
199
|
+
ctx.currentPath.pop();
|
|
200
|
+
return `&${str}`;
|
|
201
|
+
};
|
|
202
|
+
typeCtx.push(ret);
|
|
203
|
+
value_printer = printFor(type.value, typeCtx);
|
|
204
|
+
typeCtx.pop();
|
|
205
|
+
return ret;
|
|
206
|
+
}
|
|
207
|
+
else if (type.type === "Array") {
|
|
208
|
+
let value_printer;
|
|
209
|
+
const ret = (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
210
|
+
// Check for mutable alias
|
|
211
|
+
if (ctx.refs.has(x)) {
|
|
212
|
+
const targetPath = ctx.refs.get(x);
|
|
213
|
+
const refStr = _encodeRelativeRef(ctx.currentPath, targetPath);
|
|
214
|
+
return refStr;
|
|
215
|
+
}
|
|
216
|
+
// Register this array
|
|
217
|
+
ctx.refs.set(x, [...ctx.currentPath]);
|
|
218
|
+
// Serialize elements
|
|
219
|
+
const parts = [];
|
|
220
|
+
for (let i = 0; i < x.length; i++) {
|
|
221
|
+
ctx.currentPath.push(`[${i}]`);
|
|
222
|
+
parts.push(value_printer(x[i], ctx));
|
|
223
|
+
ctx.currentPath.pop();
|
|
224
|
+
}
|
|
225
|
+
return `[${parts.join(", ")}]`;
|
|
226
|
+
};
|
|
227
|
+
typeCtx.push(ret);
|
|
228
|
+
value_printer = printFor(type.value, typeCtx);
|
|
229
|
+
typeCtx.pop();
|
|
230
|
+
return ret;
|
|
231
|
+
}
|
|
232
|
+
else if (type.type === "Set") {
|
|
233
|
+
const key_printer = printFor(type.value, typeCtx);
|
|
234
|
+
return (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
235
|
+
// Check for mutable alias
|
|
236
|
+
if (ctx.refs.has(x)) {
|
|
237
|
+
const targetPath = ctx.refs.get(x);
|
|
238
|
+
const refStr = _encodeRelativeRef(ctx.currentPath, targetPath);
|
|
239
|
+
return refStr;
|
|
240
|
+
}
|
|
241
|
+
// Register this set
|
|
242
|
+
ctx.refs.set(x, [...ctx.currentPath]);
|
|
243
|
+
// Serialize elements
|
|
244
|
+
const parts = [];
|
|
245
|
+
for (const k of x) {
|
|
246
|
+
// Use fresh context for keys - no path syntax to reference inside keys
|
|
247
|
+
parts.push(key_printer(k));
|
|
248
|
+
}
|
|
249
|
+
return `{${parts.join(",")}}`;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
else if (type.type === "Dict") {
|
|
253
|
+
const key_printer = printFor(type.value.key, typeCtx);
|
|
254
|
+
let value_printer;
|
|
255
|
+
const ret = (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
256
|
+
// Check for mutable alias
|
|
257
|
+
if (ctx.refs.has(x)) {
|
|
258
|
+
const targetPath = ctx.refs.get(x);
|
|
259
|
+
const refStr = _encodeRelativeRef(ctx.currentPath, targetPath);
|
|
260
|
+
return refStr;
|
|
261
|
+
}
|
|
262
|
+
// Register this dict
|
|
263
|
+
ctx.refs.set(x, [...ctx.currentPath]);
|
|
264
|
+
if (x.size === 0)
|
|
265
|
+
return "{:}";
|
|
266
|
+
// Serialize entries
|
|
267
|
+
const parts = [];
|
|
268
|
+
for (const [k, v] of x) {
|
|
269
|
+
// Use fresh context for keys - no path syntax to reference inside keys
|
|
270
|
+
const keyStr = key_printer(k);
|
|
271
|
+
// Print value with path tracking
|
|
272
|
+
ctx.currentPath.push(`[${keyStr}]`);
|
|
273
|
+
const valueStr = value_printer(v, ctx);
|
|
274
|
+
ctx.currentPath.pop();
|
|
275
|
+
parts.push(`${keyStr}:${valueStr}`);
|
|
276
|
+
}
|
|
277
|
+
return `{${parts.join(",")}}`;
|
|
278
|
+
};
|
|
279
|
+
typeCtx.push(ret);
|
|
280
|
+
value_printer = printFor(type.value.value, typeCtx);
|
|
281
|
+
typeCtx.pop();
|
|
282
|
+
return ret;
|
|
283
|
+
}
|
|
284
|
+
else if (type.type === "Struct") {
|
|
285
|
+
const field_printers = [];
|
|
286
|
+
const ret = (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
287
|
+
const parts = [];
|
|
288
|
+
for (const [k, printer] of field_printers) {
|
|
289
|
+
const fieldName = printIdentifier(k);
|
|
290
|
+
const pathComponent = k === fieldName ? `.${k}` : `.${fieldName}`;
|
|
291
|
+
ctx.currentPath.push(pathComponent);
|
|
292
|
+
const printed = printer(x[k], ctx);
|
|
293
|
+
ctx.currentPath.pop();
|
|
294
|
+
parts.push(`${fieldName}=${printed}`);
|
|
295
|
+
}
|
|
296
|
+
return `(${parts.join(", ")})`;
|
|
297
|
+
};
|
|
298
|
+
typeCtx.push(ret);
|
|
299
|
+
for (const { name: k, type: t } of type.value) {
|
|
300
|
+
field_printers.push([k, printFor(t, typeCtx)]);
|
|
301
|
+
}
|
|
302
|
+
typeCtx.pop();
|
|
303
|
+
return ret;
|
|
304
|
+
}
|
|
305
|
+
else if (type.type === "Variant") {
|
|
306
|
+
const case_printers = {};
|
|
307
|
+
const ret = (x, ctx = { refs: new Map(), currentPath: [] }) => {
|
|
308
|
+
// Add variant case to path
|
|
309
|
+
ctx.currentPath.push(`.${x.type}`);
|
|
310
|
+
const result = case_printers[x.type](x.value, ctx);
|
|
311
|
+
ctx.currentPath.pop();
|
|
312
|
+
return result;
|
|
313
|
+
};
|
|
314
|
+
typeCtx.push(ret);
|
|
315
|
+
for (const { name: k, type: t } of type.value) {
|
|
316
|
+
if (t.type === "Null") {
|
|
317
|
+
const prefix = `.${printIdentifier(k)}`;
|
|
318
|
+
case_printers[k] = (_value, _ctx) => prefix;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const prefix = `.${printIdentifier(k)} `;
|
|
322
|
+
const print_data = printFor(t, typeCtx);
|
|
323
|
+
case_printers[k] = (value, ctx) => prefix + print_data(value, ctx);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
typeCtx.pop();
|
|
327
|
+
return ret;
|
|
328
|
+
}
|
|
329
|
+
else if (type.type === "Function") {
|
|
330
|
+
return (_, _ctx) => "λ";
|
|
331
|
+
}
|
|
332
|
+
else if (type.type === "Recursive") {
|
|
333
|
+
const ret = typeCtx[typeCtx.length - Number(type.value)];
|
|
334
|
+
if (ret === undefined) {
|
|
335
|
+
throw new Error(`Internal error: Recursive type context not found`);
|
|
336
|
+
}
|
|
337
|
+
return ret;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new Error(`Unhandled type ${type.type}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** Create a parser for a specific EastType.
|
|
344
|
+
*
|
|
345
|
+
* You can optional specify that the resulting value should be frozen (making the collection types immutable).
|
|
346
|
+
*/
|
|
347
|
+
// Helper to convert position to line and column
|
|
348
|
+
function getLineAndColumn(input, position) {
|
|
349
|
+
let line = 1;
|
|
350
|
+
let column = 1;
|
|
351
|
+
for (let i = 0; i < position && i < input.length; i++) {
|
|
352
|
+
if (input[i] === '\n') {
|
|
353
|
+
line++;
|
|
354
|
+
column = 1;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
column++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { line, column };
|
|
361
|
+
}
|
|
362
|
+
export function parseFor(type, frozen = false) {
|
|
363
|
+
// Convert EastType to EastTypeValue if necessary
|
|
364
|
+
if (!isVariant(type)) {
|
|
365
|
+
type = toEastTypeValue(type);
|
|
366
|
+
}
|
|
367
|
+
const parser = createParser(type, frozen);
|
|
368
|
+
return (x) => {
|
|
369
|
+
try {
|
|
370
|
+
const { value, position } = parser(x, 0);
|
|
371
|
+
const pos = consumeWhitespace(x, position);
|
|
372
|
+
if (pos < x.length) {
|
|
373
|
+
const { line, column } = getLineAndColumn(x, pos);
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: `Error occurred because unexpected input after parsed value (line ${line}, col ${column}) while parsing value of type "${printFor(EastTypeValueType)(type)}"`,
|
|
377
|
+
position: pos,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { success: true, value, position };
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
if (e instanceof ParseError) {
|
|
384
|
+
const { line, column } = getLineAndColumn(x, e.position);
|
|
385
|
+
const pathStr = e.path ? ` at ${e.path}` : '';
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: `Error occurred because ${e.message}${pathStr} (line ${line}, col ${column}) while parsing value of type "${printFor(EastTypeValueType)(type)}"`,
|
|
389
|
+
position: e.position,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
throw e;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// Consume whitespace by returning new position
|
|
397
|
+
const consumeWhitespace = (input, pos) => {
|
|
398
|
+
while (pos < input.length && /\s/.test(input[pos])) {
|
|
399
|
+
pos++;
|
|
400
|
+
}
|
|
401
|
+
return pos;
|
|
402
|
+
};
|
|
403
|
+
const createParser = (type, frozen, typeCtx = []) => {
|
|
404
|
+
switch (type.type) {
|
|
405
|
+
case "Null":
|
|
406
|
+
return parseNull;
|
|
407
|
+
case "Boolean":
|
|
408
|
+
return parseBoolean;
|
|
409
|
+
case "Integer":
|
|
410
|
+
return parseInteger;
|
|
411
|
+
case "Float":
|
|
412
|
+
return parseFloat;
|
|
413
|
+
case "String":
|
|
414
|
+
return parseString;
|
|
415
|
+
case "DateTime":
|
|
416
|
+
return parseDateTime(frozen);
|
|
417
|
+
case "Blob":
|
|
418
|
+
return parseBlob(frozen);
|
|
419
|
+
case "Ref":
|
|
420
|
+
return createRefParser(type.value, frozen, typeCtx);
|
|
421
|
+
case "Array":
|
|
422
|
+
return createArrayParser(type.value, frozen, typeCtx);
|
|
423
|
+
case "Set":
|
|
424
|
+
return createSetParser(type.value, frozen, typeCtx);
|
|
425
|
+
case "Dict":
|
|
426
|
+
return createDictParser(type.value.key, type.value.value, frozen, typeCtx);
|
|
427
|
+
case "Struct":
|
|
428
|
+
return createStructParser(type.value, frozen, typeCtx);
|
|
429
|
+
case "Variant":
|
|
430
|
+
return createVariantParser(type.value, frozen, typeCtx);
|
|
431
|
+
case "Function":
|
|
432
|
+
throw new Error(`Cannot parse .Function`);
|
|
433
|
+
case "Never":
|
|
434
|
+
return (_input, pos, _ctx) => { throw new ParseError(`Attempted to parse value of type .Never`, pos); };
|
|
435
|
+
case "Recursive":
|
|
436
|
+
const ret = typeCtx[typeCtx.length - Number(type.value)];
|
|
437
|
+
if (ret === undefined) {
|
|
438
|
+
throw new Error(`Internal error: Recursive type context not found`);
|
|
439
|
+
}
|
|
440
|
+
return ret;
|
|
441
|
+
default:
|
|
442
|
+
throw new Error(`Unhandled type: ${type.type}`);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const parseInteger = (input, pos, _ctx) => {
|
|
446
|
+
pos = consumeWhitespace(input, pos);
|
|
447
|
+
if (pos >= input.length) {
|
|
448
|
+
throw new ParseError("expected integer, got end of input", pos);
|
|
449
|
+
}
|
|
450
|
+
let start = pos;
|
|
451
|
+
if (input[pos] === '-') {
|
|
452
|
+
pos++;
|
|
453
|
+
}
|
|
454
|
+
let hasDigits = false;
|
|
455
|
+
while (pos < input.length && /\d/.test(input[pos])) {
|
|
456
|
+
hasDigits = true;
|
|
457
|
+
pos++;
|
|
458
|
+
}
|
|
459
|
+
if (!hasDigits) {
|
|
460
|
+
const got = start >= input.length ? 'end of input' : `'${input[start]}'`;
|
|
461
|
+
throw new ParseError(`expected integer, got ${got}`, start);
|
|
462
|
+
}
|
|
463
|
+
const intStr = input.slice(start, pos);
|
|
464
|
+
let value;
|
|
465
|
+
try {
|
|
466
|
+
value = BigInt(intStr);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// BigInt() throws if the string is invalid (shouldn't happen given our parsing logic)
|
|
470
|
+
throw new ParseError(`expected integer, got ${JSON.stringify(intStr)}`, start);
|
|
471
|
+
}
|
|
472
|
+
// Check for 64-bit signed integer range: -2^63 to 2^63-1
|
|
473
|
+
if (value < -9223372036854775808n || value > 9223372036854775807n) {
|
|
474
|
+
throw new ParseError(`integer out of range (must be 64-bit signed), got ${intStr}`, start);
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
value,
|
|
478
|
+
position: pos
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
const parseFloat = (input, pos, _ctx) => {
|
|
482
|
+
pos = consumeWhitespace(input, pos);
|
|
483
|
+
if (pos >= input.length) {
|
|
484
|
+
throw new ParseError("expected float, got end of input", pos);
|
|
485
|
+
}
|
|
486
|
+
// Accept NaN and Infinity
|
|
487
|
+
if (input.startsWith('NaN', pos)) {
|
|
488
|
+
return { value: NaN, position: pos + 3 };
|
|
489
|
+
}
|
|
490
|
+
if (input.startsWith('Infinity', pos)) {
|
|
491
|
+
return { value: Infinity, position: pos + 8 };
|
|
492
|
+
}
|
|
493
|
+
let start = pos;
|
|
494
|
+
if (input[pos] === '-') {
|
|
495
|
+
pos++;
|
|
496
|
+
if (input.startsWith('Infinity', pos)) {
|
|
497
|
+
return { value: -Infinity, position: pos + 8 };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let hasDigits = false;
|
|
501
|
+
while (pos < input.length && /\d/.test(input[pos])) {
|
|
502
|
+
hasDigits = true;
|
|
503
|
+
pos++;
|
|
504
|
+
}
|
|
505
|
+
if (!hasDigits) {
|
|
506
|
+
const got = start >= input.length ? 'end of input' : `'${input[start]}'`;
|
|
507
|
+
throw new ParseError(`expected float, got ${got}`, start);
|
|
508
|
+
}
|
|
509
|
+
if (pos < input.length && input[pos] === '.') {
|
|
510
|
+
pos++;
|
|
511
|
+
while (pos < input.length && /\d/.test(input[pos])) {
|
|
512
|
+
pos++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (pos < input.length && (input[pos] === 'e' || input[pos] === 'E')) {
|
|
516
|
+
pos++;
|
|
517
|
+
if (pos < input.length && (input[pos] === '+' || input[pos] === '-')) {
|
|
518
|
+
pos++;
|
|
519
|
+
}
|
|
520
|
+
let expDigits = false;
|
|
521
|
+
while (pos < input.length && /\d/.test(input[pos])) {
|
|
522
|
+
expDigits = true;
|
|
523
|
+
pos++;
|
|
524
|
+
}
|
|
525
|
+
if (!expDigits) {
|
|
526
|
+
throw new ParseError("expected digits in float exponent", pos);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
value: Number(input.slice(start, pos)),
|
|
531
|
+
position: pos
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Checks if the given position is at a valid token terminator.
|
|
536
|
+
* Valid terminators are: EOF, whitespace, comma, or closing brackets/parens/braces.
|
|
537
|
+
*/
|
|
538
|
+
const isTokenTerminator = (input, pos) => {
|
|
539
|
+
if (pos >= input.length)
|
|
540
|
+
return true; // EOF
|
|
541
|
+
const ch = input[pos];
|
|
542
|
+
return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' ||
|
|
543
|
+
ch === ',' || ch === ')' || ch === ']' || ch === '}';
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Parses a $.path reference and returns the referenced value.
|
|
547
|
+
* Returns null if not at a reference (doesn't start with '$').
|
|
548
|
+
* Throws ParseError if the reference is invalid or undefined.
|
|
549
|
+
*/
|
|
550
|
+
const parseReference = (input, pos, ctx) => {
|
|
551
|
+
// Check if this looks like a reference: either "#..." or "N#..." where N is a digit
|
|
552
|
+
let refStart = pos;
|
|
553
|
+
let isRelative = false;
|
|
554
|
+
if (pos >= input.length || input[pos] !== '#') {
|
|
555
|
+
// Check if it starts with a digit followed by '#' (relative reference)
|
|
556
|
+
let digitEnd = pos;
|
|
557
|
+
while (digitEnd < input.length && /[0-9]/.test(input[digitEnd])) {
|
|
558
|
+
digitEnd++;
|
|
559
|
+
}
|
|
560
|
+
if (digitEnd > pos && digitEnd < input.length && input[digitEnd] === '#') {
|
|
561
|
+
isRelative = true;
|
|
562
|
+
refStart = pos;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
return null; // Not a reference
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!ctx) {
|
|
569
|
+
throw new ParseError("#reference outside recursive context", pos);
|
|
570
|
+
}
|
|
571
|
+
// Parse the path - it can contain:
|
|
572
|
+
// - #.field (dot followed by identifier)
|
|
573
|
+
// - #[index] (bracket with number or value)
|
|
574
|
+
// - combinations like #.a.b[0].c
|
|
575
|
+
// OR for relative: N#.field etc.
|
|
576
|
+
let pathStart;
|
|
577
|
+
if (isRelative) {
|
|
578
|
+
// Find the # position
|
|
579
|
+
pathStart = input.indexOf('#', pos);
|
|
580
|
+
refStart = pathStart + 1; // Start after '#'
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
refStart = pos + 1; // Start after '#'
|
|
584
|
+
}
|
|
585
|
+
let pathEnd = refStart;
|
|
586
|
+
while (pathEnd < input.length) {
|
|
587
|
+
const ch = input[pathEnd];
|
|
588
|
+
if (ch === '.') {
|
|
589
|
+
// Dot followed by identifier - keep going until we hit non-identifier char
|
|
590
|
+
pathEnd++;
|
|
591
|
+
while (pathEnd < input.length && /[a-zA-Z0-9_]/.test(input[pathEnd])) {
|
|
592
|
+
pathEnd++;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else if (ch === '[') {
|
|
596
|
+
// Bracket - find the matching close bracket
|
|
597
|
+
pathEnd++;
|
|
598
|
+
let depth = 1;
|
|
599
|
+
while (pathEnd < input.length && depth > 0) {
|
|
600
|
+
if (input[pathEnd] === '[')
|
|
601
|
+
depth++;
|
|
602
|
+
else if (input[pathEnd] === ']')
|
|
603
|
+
depth--;
|
|
604
|
+
pathEnd++;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// Hit something that's not part of the path
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const refStr = input.substring(pos, pathEnd);
|
|
613
|
+
// Resolve the target path
|
|
614
|
+
let targetPathStr;
|
|
615
|
+
try {
|
|
616
|
+
if (isRelative) {
|
|
617
|
+
// refStr is like "1#.a" - pass it directly to _decodeRelativeRef
|
|
618
|
+
const targetPath = _decodeRelativeRef(refStr, ctx.currentPath);
|
|
619
|
+
targetPathStr = _pathToPunctuated(targetPath);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// Absolute reference: just use the path part directly (without the leading #)
|
|
623
|
+
targetPathStr = refStr.substring(1);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
throw new ParseError(`invalid reference ${refStr}: ${e instanceof Error ? e.message : String(e)}`, pos);
|
|
628
|
+
}
|
|
629
|
+
// Look up in refs map
|
|
630
|
+
if (!ctx.refs.has(targetPathStr)) {
|
|
631
|
+
throw new ParseError(`undefined reference ${refStr}`, pos);
|
|
632
|
+
}
|
|
633
|
+
return { value: ctx.refs.get(targetPathStr), position: pathEnd };
|
|
634
|
+
};
|
|
635
|
+
const parseBoolean = (input, pos, _ctx) => {
|
|
636
|
+
pos = consumeWhitespace(input, pos);
|
|
637
|
+
if (input.startsWith('true', pos)) {
|
|
638
|
+
return { value: true, position: pos + 4 };
|
|
639
|
+
}
|
|
640
|
+
if (input.startsWith('false', pos)) {
|
|
641
|
+
return { value: false, position: pos + 5 };
|
|
642
|
+
}
|
|
643
|
+
const got = pos >= input.length ? 'end of input' : `'${input[pos]}'`;
|
|
644
|
+
throw new ParseError(`expected boolean, got ${got}`, pos);
|
|
645
|
+
};
|
|
646
|
+
const parseNull = (input, pos, _ctx) => {
|
|
647
|
+
pos = consumeWhitespace(input, pos);
|
|
648
|
+
if (input.startsWith('null', pos) && isTokenTerminator(input, pos + 4)) {
|
|
649
|
+
return { value: null, position: pos + 4 };
|
|
650
|
+
}
|
|
651
|
+
const got = pos >= input.length ? 'end of input' : `'${input[pos]}'`;
|
|
652
|
+
throw new ParseError(`expected null, got ${got}`, pos);
|
|
653
|
+
};
|
|
654
|
+
const parseString = (input, pos, _ctx) => {
|
|
655
|
+
pos = consumeWhitespace(input, pos);
|
|
656
|
+
if (input[pos] !== '"') {
|
|
657
|
+
const got = pos >= input.length ? 'end of input' : `'${input[pos]}'`;
|
|
658
|
+
throw new ParseError(`expected '"', got ${got}`, pos);
|
|
659
|
+
}
|
|
660
|
+
pos += 1;
|
|
661
|
+
let rangeStart = pos;
|
|
662
|
+
let result = "";
|
|
663
|
+
// search through for escape characters, copy string out in ranges
|
|
664
|
+
while (pos < input.length) {
|
|
665
|
+
const char = input[pos];
|
|
666
|
+
if (char === '\\') {
|
|
667
|
+
result = result + input.substring(rangeStart, pos);
|
|
668
|
+
pos += 1;
|
|
669
|
+
if (pos < input.length) {
|
|
670
|
+
const char2 = input[pos];
|
|
671
|
+
if (char2 === '\\' || char2 === '\"') {
|
|
672
|
+
result = result + char2;
|
|
673
|
+
pos += 1;
|
|
674
|
+
rangeStart = pos;
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// TODO we need to (a) deal with escape sequences at least for unprintable characters and (b) safely handle LTR-RTL transition characters
|
|
678
|
+
throw new ParseError("unexpected escape sequence in string", pos);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
throw new ParseError("unterminated string (missing closing quote)", pos);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (char === '\"') {
|
|
686
|
+
result = result + input.substring(rangeStart, pos);
|
|
687
|
+
return { value: result, position: pos + 1 };
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
pos += 1;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
throw new ParseError("unterminated string (missing closing quote)", pos);
|
|
694
|
+
};
|
|
695
|
+
const parseDateTime = (frozen) => (input, pos, _ctx) => {
|
|
696
|
+
// Datetime format is YYYY-MM-DDTHH:MM:SS.sss (optional miliseconds, no quotes, no timezone or Z)
|
|
697
|
+
pos = consumeWhitespace(input, pos);
|
|
698
|
+
const datetimeRegex = /^-?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?/;
|
|
699
|
+
const match = datetimeRegex.exec(input.slice(pos));
|
|
700
|
+
if (!match) {
|
|
701
|
+
throw new ParseError("expected DateTime in format YYYY-MM-DDTHH:MM:SS.sss", pos);
|
|
702
|
+
}
|
|
703
|
+
const datetimeStr = match[0];
|
|
704
|
+
const date = new Date(datetimeStr + 'Z'); // Our naive datetimes are treated as UTC
|
|
705
|
+
if (isNaN(date.getTime())) {
|
|
706
|
+
throw new ParseError(`invalid DateTime value, got ${JSON.stringify(datetimeStr)}`, pos);
|
|
707
|
+
}
|
|
708
|
+
if (frozen) {
|
|
709
|
+
Object.freeze(date);
|
|
710
|
+
}
|
|
711
|
+
return { value: date, position: pos + datetimeStr.length };
|
|
712
|
+
};
|
|
713
|
+
const parseBlob = (frozen) => (input, pos, _ctx) => {
|
|
714
|
+
pos = consumeWhitespace(input, pos);
|
|
715
|
+
if (pos >= input.length || input[pos] !== '0' || input[pos + 1] !== 'x') {
|
|
716
|
+
throw new ParseError("expected Blob starting with 0x", pos);
|
|
717
|
+
}
|
|
718
|
+
pos += 2;
|
|
719
|
+
let start = pos;
|
|
720
|
+
while (pos < input.length && /[0-9a-fA-F]/.test(input[pos])) {
|
|
721
|
+
pos++;
|
|
722
|
+
}
|
|
723
|
+
const hexStr = input.slice(start, pos);
|
|
724
|
+
// Check for odd length or invalid hex characters
|
|
725
|
+
if (hexStr.length % 2 !== 0) {
|
|
726
|
+
throw new ParseError(`invalid hex string (odd length), got "0x${hexStr}"`, start - 2);
|
|
727
|
+
}
|
|
728
|
+
// Note: The while loop above already validates that all characters are valid hex,
|
|
729
|
+
// so we don't need an additional check here. If there were invalid characters,
|
|
730
|
+
// they would be left in the input and caught by downstream parsing.
|
|
731
|
+
const byteLength = hexStr.length / 2;
|
|
732
|
+
const bytes = new Uint8Array(byteLength);
|
|
733
|
+
for (let i = 0; i < byteLength; i++) {
|
|
734
|
+
bytes[i] = parseInt(hexStr.substring(i * 2, i * 2 + 2), 16);
|
|
735
|
+
}
|
|
736
|
+
if (frozen) {
|
|
737
|
+
Object.freeze(bytes);
|
|
738
|
+
}
|
|
739
|
+
return { value: bytes, position: pos };
|
|
740
|
+
};
|
|
741
|
+
const createRefParser = (value_type, frozen, typeCtx) => {
|
|
742
|
+
let valueParser;
|
|
743
|
+
const ret = (input, pos, ctx) => {
|
|
744
|
+
pos = consumeWhitespace(input, pos);
|
|
745
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
746
|
+
const needsOwnContext = !ctx;
|
|
747
|
+
if (needsOwnContext) {
|
|
748
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
749
|
+
}
|
|
750
|
+
// Check for reference first
|
|
751
|
+
const reference = parseReference(input, pos, ctx);
|
|
752
|
+
if (reference) {
|
|
753
|
+
return reference;
|
|
754
|
+
}
|
|
755
|
+
if (pos >= input.length || input[pos] !== '&') {
|
|
756
|
+
throw new ParseError("expected '&' to start ref", pos);
|
|
757
|
+
}
|
|
758
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
759
|
+
const r = ref(undefined);
|
|
760
|
+
// Pre-register this ref if we have context
|
|
761
|
+
if (ctx) {
|
|
762
|
+
const pathStr = _pathToPunctuated(ctx.currentPath);
|
|
763
|
+
ctx.refs.set(pathStr, r);
|
|
764
|
+
}
|
|
765
|
+
if (ctx)
|
|
766
|
+
ctx.currentPath.push(`[]`);
|
|
767
|
+
let value;
|
|
768
|
+
let position;
|
|
769
|
+
try {
|
|
770
|
+
const result = valueParser(input, pos, ctx);
|
|
771
|
+
value = result.value;
|
|
772
|
+
position = result.position;
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
if (e instanceof ParseError) {
|
|
776
|
+
const newPath = `[]` + (e.path ? e.path : '');
|
|
777
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
778
|
+
}
|
|
779
|
+
throw e;
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
if (ctx)
|
|
783
|
+
ctx.currentPath.pop();
|
|
784
|
+
}
|
|
785
|
+
r.value = value;
|
|
786
|
+
return { value: r, position };
|
|
787
|
+
};
|
|
788
|
+
typeCtx.push(ret);
|
|
789
|
+
valueParser = createParser(value_type, frozen, typeCtx);
|
|
790
|
+
typeCtx.pop();
|
|
791
|
+
return ret;
|
|
792
|
+
};
|
|
793
|
+
const createArrayParser = (value_type, frozen, typeCtx) => {
|
|
794
|
+
let valueParser;
|
|
795
|
+
const ret = (input, pos, ctx) => {
|
|
796
|
+
pos = consumeWhitespace(input, pos);
|
|
797
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
798
|
+
const needsOwnContext = !ctx;
|
|
799
|
+
if (needsOwnContext) {
|
|
800
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
801
|
+
}
|
|
802
|
+
// Check for reference first
|
|
803
|
+
const ref = parseReference(input, pos, ctx);
|
|
804
|
+
if (ref) {
|
|
805
|
+
return ref;
|
|
806
|
+
}
|
|
807
|
+
if (pos >= input.length || input[pos] !== '[') {
|
|
808
|
+
throw new ParseError("expected '[' to start array", pos);
|
|
809
|
+
}
|
|
810
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
811
|
+
const values = [];
|
|
812
|
+
// Pre-register this array if we have context
|
|
813
|
+
if (ctx) {
|
|
814
|
+
const pathStr = _pathToPunctuated(ctx.currentPath);
|
|
815
|
+
ctx.refs.set(pathStr, values);
|
|
816
|
+
}
|
|
817
|
+
// Handle empty array
|
|
818
|
+
if (input[pos] === ']') {
|
|
819
|
+
if (frozen) {
|
|
820
|
+
Object.freeze(values);
|
|
821
|
+
}
|
|
822
|
+
return { value: values, position: pos + 1 };
|
|
823
|
+
}
|
|
824
|
+
let elementIndex = 0;
|
|
825
|
+
while (true) {
|
|
826
|
+
if (ctx)
|
|
827
|
+
ctx.currentPath.push(`[${elementIndex}]`);
|
|
828
|
+
try {
|
|
829
|
+
const { value: elementValue, position: elementPos } = valueParser(input, pos, ctx);
|
|
830
|
+
values.push(elementValue);
|
|
831
|
+
pos = consumeWhitespace(input, elementPos);
|
|
832
|
+
}
|
|
833
|
+
catch (e) {
|
|
834
|
+
if (e instanceof ParseError) {
|
|
835
|
+
const newPath = `[${elementIndex}]` + (e.path ? e.path : '');
|
|
836
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
837
|
+
}
|
|
838
|
+
throw e;
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
if (ctx)
|
|
842
|
+
ctx.currentPath.pop();
|
|
843
|
+
}
|
|
844
|
+
if (input[pos] === ']') {
|
|
845
|
+
if (frozen) {
|
|
846
|
+
Object.freeze(values);
|
|
847
|
+
}
|
|
848
|
+
return { value: values, position: pos + 1 };
|
|
849
|
+
}
|
|
850
|
+
if (input[pos] !== ',') {
|
|
851
|
+
throw new ParseError("expected ',' or ']' after array element", pos);
|
|
852
|
+
}
|
|
853
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
854
|
+
elementIndex++;
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
typeCtx.push(ret);
|
|
858
|
+
valueParser = createParser(value_type, frozen, typeCtx);
|
|
859
|
+
typeCtx.pop();
|
|
860
|
+
return ret;
|
|
861
|
+
};
|
|
862
|
+
const createSetParser = (key_type, frozen, typeCtx) => {
|
|
863
|
+
const keyParser = createParser(key_type, frozen, typeCtx);
|
|
864
|
+
const keyCompare = compareFor(key_type);
|
|
865
|
+
return (input, pos, ctx) => {
|
|
866
|
+
pos = consumeWhitespace(input, pos);
|
|
867
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
868
|
+
const needsOwnContext = !ctx;
|
|
869
|
+
if (needsOwnContext) {
|
|
870
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
871
|
+
}
|
|
872
|
+
// Check for reference first
|
|
873
|
+
const ref = parseReference(input, pos, ctx);
|
|
874
|
+
if (ref) {
|
|
875
|
+
return ref;
|
|
876
|
+
}
|
|
877
|
+
if (pos >= input.length || input[pos] !== '{') {
|
|
878
|
+
throw new ParseError("expected '{' to start set", pos);
|
|
879
|
+
}
|
|
880
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
881
|
+
const values = new SortedSet(undefined, keyCompare);
|
|
882
|
+
// Pre-register this set if we have context
|
|
883
|
+
if (ctx) {
|
|
884
|
+
const pathStr = _pathToPunctuated(ctx.currentPath);
|
|
885
|
+
ctx.refs.set(pathStr, values);
|
|
886
|
+
}
|
|
887
|
+
// Handle empty set
|
|
888
|
+
if (input[pos] === '}') {
|
|
889
|
+
if (frozen) {
|
|
890
|
+
Object.freeze(values);
|
|
891
|
+
}
|
|
892
|
+
return { value: values, position: pos + 1 };
|
|
893
|
+
}
|
|
894
|
+
let elementIndex = 0;
|
|
895
|
+
while (true) {
|
|
896
|
+
try {
|
|
897
|
+
const keyResult = keyParser(input, pos, ctx);
|
|
898
|
+
values.add(keyResult.value);
|
|
899
|
+
pos = consumeWhitespace(input, keyResult.position);
|
|
900
|
+
}
|
|
901
|
+
catch (e) {
|
|
902
|
+
if (e instanceof ParseError) {
|
|
903
|
+
const newPath = `[${elementIndex}]` + (e.path ? e.path : '');
|
|
904
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
905
|
+
}
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
908
|
+
if (input[pos] === '}') {
|
|
909
|
+
if (frozen) {
|
|
910
|
+
Object.freeze(values);
|
|
911
|
+
}
|
|
912
|
+
return { value: values, position: pos + 1 };
|
|
913
|
+
}
|
|
914
|
+
if (input[pos] !== ',') {
|
|
915
|
+
throw new ParseError("expected ',' or '}' after set element", pos);
|
|
916
|
+
}
|
|
917
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
918
|
+
elementIndex++;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
const createDictParser = (key_type, value_type, frozen, typeCtx) => {
|
|
923
|
+
const keyParser = createParser(key_type, frozen, typeCtx);
|
|
924
|
+
let valueParser;
|
|
925
|
+
const keyCompare = compareFor(key_type);
|
|
926
|
+
const keyPrinter = printFor(key_type);
|
|
927
|
+
const ret = (input, pos, ctx) => {
|
|
928
|
+
pos = consumeWhitespace(input, pos);
|
|
929
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
930
|
+
const needsOwnContext = !ctx;
|
|
931
|
+
if (needsOwnContext) {
|
|
932
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
933
|
+
}
|
|
934
|
+
// Check for reference first
|
|
935
|
+
const ref = parseReference(input, pos, ctx);
|
|
936
|
+
if (ref) {
|
|
937
|
+
return ref;
|
|
938
|
+
}
|
|
939
|
+
if (pos >= input.length || input[pos] !== '{') {
|
|
940
|
+
throw new ParseError("expected '{' to start dict", pos);
|
|
941
|
+
}
|
|
942
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
943
|
+
const values = new SortedMap(undefined, keyCompare);
|
|
944
|
+
// Pre-register this dict if we have context
|
|
945
|
+
if (ctx) {
|
|
946
|
+
const pathStr = _pathToPunctuated(ctx.currentPath);
|
|
947
|
+
ctx.refs.set(pathStr, values);
|
|
948
|
+
}
|
|
949
|
+
// Handle empty dict
|
|
950
|
+
if (input[pos] === ':') {
|
|
951
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
952
|
+
if (input[pos] === '}') {
|
|
953
|
+
if (frozen) {
|
|
954
|
+
Object.freeze(values);
|
|
955
|
+
}
|
|
956
|
+
return { value: values, position: pos + 1 };
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
throw new ParseError("expected '}' after ':' in empty dict", pos);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// be lenient and allow empty set syntax as well
|
|
963
|
+
if (input[pos] === '}') {
|
|
964
|
+
if (frozen) {
|
|
965
|
+
Object.freeze(values);
|
|
966
|
+
}
|
|
967
|
+
return { value: values, position: pos + 1 };
|
|
968
|
+
}
|
|
969
|
+
let entryIndex = 0;
|
|
970
|
+
while (true) {
|
|
971
|
+
let dictKey;
|
|
972
|
+
try {
|
|
973
|
+
const { value: keyValue, position: keyPos } = keyParser(input, pos, ctx);
|
|
974
|
+
dictKey = keyValue;
|
|
975
|
+
pos = consumeWhitespace(input, keyPos);
|
|
976
|
+
}
|
|
977
|
+
catch (e) {
|
|
978
|
+
if (e instanceof ParseError) {
|
|
979
|
+
const newPath = `[${entryIndex}](key)` + (e.path ? e.path : '');
|
|
980
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
981
|
+
}
|
|
982
|
+
throw e;
|
|
983
|
+
}
|
|
984
|
+
if (input[pos] !== ':') {
|
|
985
|
+
throw new ParseError(`expected ':' after dict key at entry ${entryIndex}`, pos);
|
|
986
|
+
}
|
|
987
|
+
// Track path for dict values
|
|
988
|
+
if (ctx) {
|
|
989
|
+
const keyStr = keyPrinter(dictKey);
|
|
990
|
+
ctx.currentPath.push(`[${keyStr}]`);
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
const { value: dictValue, position: valuePos } = valueParser(input, pos + 1, ctx);
|
|
994
|
+
values.set(dictKey, dictValue);
|
|
995
|
+
pos = consumeWhitespace(input, valuePos);
|
|
996
|
+
}
|
|
997
|
+
catch (e) {
|
|
998
|
+
if (e instanceof ParseError) {
|
|
999
|
+
const keyStr = keyPrinter(dictKey);
|
|
1000
|
+
const newPath = `[${keyStr}]` + (e.path ? e.path : '');
|
|
1001
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1002
|
+
}
|
|
1003
|
+
throw e;
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
if (ctx)
|
|
1007
|
+
ctx.currentPath.pop();
|
|
1008
|
+
}
|
|
1009
|
+
if (input[pos] === '}') {
|
|
1010
|
+
if (frozen) {
|
|
1011
|
+
Object.freeze(values);
|
|
1012
|
+
}
|
|
1013
|
+
return { value: values, position: pos + 1 };
|
|
1014
|
+
}
|
|
1015
|
+
if (input[pos] !== ',') {
|
|
1016
|
+
throw new ParseError("expected ',' or '}' after dict entry", pos);
|
|
1017
|
+
}
|
|
1018
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1019
|
+
entryIndex++;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
typeCtx.push(ret);
|
|
1023
|
+
valueParser = createParser(value_type, frozen, typeCtx);
|
|
1024
|
+
typeCtx.pop();
|
|
1025
|
+
return ret;
|
|
1026
|
+
};
|
|
1027
|
+
const parseQuotedIdentifier = (input, pos, _ctx) => {
|
|
1028
|
+
pos = consumeWhitespace(input, pos);
|
|
1029
|
+
if (input[pos] !== '`') {
|
|
1030
|
+
throw new ParseError("expected identifier (opening `)", pos);
|
|
1031
|
+
}
|
|
1032
|
+
pos += 1;
|
|
1033
|
+
let rangeStart = pos;
|
|
1034
|
+
let result = "";
|
|
1035
|
+
// search through for escape characters, copy string out in ranges
|
|
1036
|
+
while (pos < input.length) {
|
|
1037
|
+
const char = input[pos];
|
|
1038
|
+
if (char === '\\') {
|
|
1039
|
+
result = result + input.substring(rangeStart, pos);
|
|
1040
|
+
pos += 1;
|
|
1041
|
+
if (pos < input.length) {
|
|
1042
|
+
const char2 = input[pos];
|
|
1043
|
+
if (char2 === '\\' || char2 === '`') {
|
|
1044
|
+
rangeStart = pos;
|
|
1045
|
+
pos += 1;
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// TODO we need to (a) deal with escape sequences at least for unprintable characters and (b) safely handle LTR-RTL transition characters
|
|
1049
|
+
throw new ParseError("unexpected escape sequence in identifier", pos);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
throw new ParseError("unterminated identifier (missing closing `)", pos);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
else if (char === '`') {
|
|
1057
|
+
result = result + input.substring(rangeStart, pos);
|
|
1058
|
+
return { value: result, position: pos + 1 };
|
|
1059
|
+
}
|
|
1060
|
+
pos += 1;
|
|
1061
|
+
}
|
|
1062
|
+
throw new ParseError("unterminated identifier (missing closing `)", pos);
|
|
1063
|
+
};
|
|
1064
|
+
const parseIdentifier = (input, pos, _ctx) => {
|
|
1065
|
+
pos = consumeWhitespace(input, pos);
|
|
1066
|
+
if (input[pos] === '`') {
|
|
1067
|
+
return parseQuotedIdentifier(input, pos, _ctx);
|
|
1068
|
+
}
|
|
1069
|
+
if (pos < input.length && /[a-zA-Z_]/.test(input[pos])) {
|
|
1070
|
+
const initial_pos = pos;
|
|
1071
|
+
pos += 1;
|
|
1072
|
+
while (pos < input.length && /[a-zA-Z0-9_]/.test(input[pos])) {
|
|
1073
|
+
pos += 1;
|
|
1074
|
+
}
|
|
1075
|
+
// TODO possibly should check that the following character is valid (EOF, comma, bracket, etc)?
|
|
1076
|
+
return { value: input.substring(initial_pos, pos), position: pos };
|
|
1077
|
+
}
|
|
1078
|
+
throw new ParseError("expected identifier", pos);
|
|
1079
|
+
};
|
|
1080
|
+
const createStructParser = (fields, frozen, typeCtx) => {
|
|
1081
|
+
const fieldNames = fields.map(f => f.name);
|
|
1082
|
+
const fieldParsers = [];
|
|
1083
|
+
const ret = (input, pos, ctx) => {
|
|
1084
|
+
pos = consumeWhitespace(input, pos);
|
|
1085
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
1086
|
+
const needsOwnContext = !ctx;
|
|
1087
|
+
if (needsOwnContext) {
|
|
1088
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
1089
|
+
}
|
|
1090
|
+
if (pos >= input.length || input[pos] !== '(') {
|
|
1091
|
+
throw new ParseError("expected '(' to start struct", pos);
|
|
1092
|
+
}
|
|
1093
|
+
pos += 1;
|
|
1094
|
+
const result = {};
|
|
1095
|
+
let fieldIndex = 0;
|
|
1096
|
+
while (fieldIndex < fieldNames.length) {
|
|
1097
|
+
const fieldName = fieldNames[fieldIndex];
|
|
1098
|
+
pos = consumeWhitespace(input, pos);
|
|
1099
|
+
// Check for early closing paren (missing fields)
|
|
1100
|
+
if (input[pos] === ')') {
|
|
1101
|
+
throw new ParseError(`missing required field '${fieldName}'`, pos);
|
|
1102
|
+
}
|
|
1103
|
+
// Try to parse an identifier
|
|
1104
|
+
let parsedFieldName;
|
|
1105
|
+
let namePos;
|
|
1106
|
+
try {
|
|
1107
|
+
const identResult = parseIdentifier(input, pos, ctx);
|
|
1108
|
+
parsedFieldName = identResult.value;
|
|
1109
|
+
namePos = identResult.position;
|
|
1110
|
+
}
|
|
1111
|
+
catch (e) {
|
|
1112
|
+
if (e instanceof ParseError) {
|
|
1113
|
+
// If we can't parse an identifier, the field is missing
|
|
1114
|
+
throw new ParseError(`missing required field '${fieldName}'`, pos);
|
|
1115
|
+
}
|
|
1116
|
+
throw e;
|
|
1117
|
+
}
|
|
1118
|
+
// Check if the parsed field name matches what we expect
|
|
1119
|
+
if (parsedFieldName !== fieldName) {
|
|
1120
|
+
throw new ParseError(`unknown field '${parsedFieldName}', expected one of: ${fieldNames.join(', ')}`, pos);
|
|
1121
|
+
}
|
|
1122
|
+
pos = namePos;
|
|
1123
|
+
pos = consumeWhitespace(input, pos);
|
|
1124
|
+
if (pos >= input.length || input[pos] !== '=') {
|
|
1125
|
+
throw new ParseError(`expected '=' after field name '${fieldName}'`, pos);
|
|
1126
|
+
}
|
|
1127
|
+
pos += 1;
|
|
1128
|
+
const parser = fieldParsers[fieldIndex];
|
|
1129
|
+
// Track path for struct fields
|
|
1130
|
+
const fieldPath = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName) ? `.${fieldName}` : `[${JSON.stringify(fieldName)}]`;
|
|
1131
|
+
if (ctx)
|
|
1132
|
+
ctx.currentPath.push(fieldPath);
|
|
1133
|
+
try {
|
|
1134
|
+
const { value: fieldValue, position: valuePos } = parser(input, pos, ctx);
|
|
1135
|
+
result[fieldName] = fieldValue;
|
|
1136
|
+
pos = valuePos;
|
|
1137
|
+
}
|
|
1138
|
+
catch (e) {
|
|
1139
|
+
if (e instanceof ParseError) {
|
|
1140
|
+
const newPath = fieldPath + (e.path ? e.path : '');
|
|
1141
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1142
|
+
}
|
|
1143
|
+
throw e;
|
|
1144
|
+
}
|
|
1145
|
+
finally {
|
|
1146
|
+
if (ctx)
|
|
1147
|
+
ctx.currentPath.pop();
|
|
1148
|
+
}
|
|
1149
|
+
// Look for comma or closing paren
|
|
1150
|
+
pos = consumeWhitespace(input, pos);
|
|
1151
|
+
if (pos >= input.length) {
|
|
1152
|
+
throw new ParseError("unexpected end of input in struct", pos);
|
|
1153
|
+
}
|
|
1154
|
+
if (input[pos] === ',') {
|
|
1155
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1156
|
+
fieldIndex++;
|
|
1157
|
+
}
|
|
1158
|
+
else if (input[pos] === ')') {
|
|
1159
|
+
// Check if we've parsed all fields
|
|
1160
|
+
if (fieldIndex !== fieldNames.length - 1) {
|
|
1161
|
+
throw new ParseError(`missing required field '${fieldNames[fieldIndex + 1]}'`, pos);
|
|
1162
|
+
}
|
|
1163
|
+
// All fields parsed, break and handle closing paren after loop
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
throw new ParseError("expected ',' or ')' after struct field", pos);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// After loop, we should be at ')'
|
|
1171
|
+
pos = consumeWhitespace(input, pos);
|
|
1172
|
+
if (pos >= input.length || input[pos] !== ')') {
|
|
1173
|
+
// We've parsed all fields but there's more content - must be an unknown field
|
|
1174
|
+
if (pos < input.length) {
|
|
1175
|
+
try {
|
|
1176
|
+
const { value: extraField } = parseIdentifier(input, pos, ctx);
|
|
1177
|
+
throw new ParseError(`unknown field '${extraField}', expected one of: ${fieldNames.join(', ')}`, pos);
|
|
1178
|
+
}
|
|
1179
|
+
catch (e) {
|
|
1180
|
+
if (e instanceof ParseError && e.message.startsWith("Unknown field")) {
|
|
1181
|
+
throw e;
|
|
1182
|
+
}
|
|
1183
|
+
// Couldn't parse identifier, just say we expected ')'
|
|
1184
|
+
throw new ParseError("expected ')' to close struct", pos);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
throw new ParseError("expected ')' to close struct", pos);
|
|
1188
|
+
}
|
|
1189
|
+
if (frozen) {
|
|
1190
|
+
Object.freeze(result);
|
|
1191
|
+
}
|
|
1192
|
+
return { value: result, position: pos + 1 };
|
|
1193
|
+
};
|
|
1194
|
+
typeCtx.push(ret);
|
|
1195
|
+
for (const field of fields) {
|
|
1196
|
+
fieldParsers.push(createParser(field.type, frozen, typeCtx));
|
|
1197
|
+
}
|
|
1198
|
+
typeCtx.pop();
|
|
1199
|
+
return ret;
|
|
1200
|
+
};
|
|
1201
|
+
const createVariantParser = (cases, frozen, typeCtx) => {
|
|
1202
|
+
const caseParsers = {};
|
|
1203
|
+
const caseNames = cases.map(c => c.name);
|
|
1204
|
+
const ret = (input, pos, ctx) => {
|
|
1205
|
+
pos = consumeWhitespace(input, pos);
|
|
1206
|
+
// Create context if we don't have one (for top-level parsing with aliases)
|
|
1207
|
+
const needsOwnContext = !ctx;
|
|
1208
|
+
if (needsOwnContext) {
|
|
1209
|
+
ctx = { refs: new Map(), currentPath: [] };
|
|
1210
|
+
}
|
|
1211
|
+
if (pos >= input.length || input[pos] !== '.') {
|
|
1212
|
+
throw new ParseError("expected '.' to start variant case", pos);
|
|
1213
|
+
}
|
|
1214
|
+
pos += 1;
|
|
1215
|
+
// We cannot have whitespace
|
|
1216
|
+
const pos2 = consumeWhitespace(input, pos);
|
|
1217
|
+
if (pos2 !== pos) {
|
|
1218
|
+
throw new ParseError("whitespace not allowed between '.' and case identifier", pos);
|
|
1219
|
+
}
|
|
1220
|
+
const { value: caseName, position: casePos } = parseIdentifier(input, pos, ctx);
|
|
1221
|
+
const valueParser = caseParsers[caseName];
|
|
1222
|
+
if (valueParser === undefined) {
|
|
1223
|
+
throw new ParseError(`unknown variant case .${printIdentifier(caseName)}, expected one of: ${caseNames.map(c => `.${printIdentifier(c)}`).join(', ')}`, pos);
|
|
1224
|
+
}
|
|
1225
|
+
// Create variant with undefined placeholder
|
|
1226
|
+
const v = variant(caseName, undefined);
|
|
1227
|
+
// Parse the value with case name in path
|
|
1228
|
+
if (ctx)
|
|
1229
|
+
ctx.currentPath.push(`.${caseName}`);
|
|
1230
|
+
try {
|
|
1231
|
+
const { value: caseValue, position: valuePos } = valueParser(input, casePos, ctx);
|
|
1232
|
+
// Mutate the variant's value field
|
|
1233
|
+
v.value = caseValue;
|
|
1234
|
+
if (frozen) {
|
|
1235
|
+
Object.freeze(v);
|
|
1236
|
+
}
|
|
1237
|
+
return { value: v, position: valuePos };
|
|
1238
|
+
}
|
|
1239
|
+
catch (e) {
|
|
1240
|
+
if (e instanceof ParseError) {
|
|
1241
|
+
const newPath = `.${caseName}` + (e.path ? e.path : '');
|
|
1242
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1243
|
+
}
|
|
1244
|
+
throw e;
|
|
1245
|
+
}
|
|
1246
|
+
finally {
|
|
1247
|
+
if (ctx)
|
|
1248
|
+
ctx.currentPath.pop();
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
typeCtx.push(ret);
|
|
1252
|
+
for (const { name, type: caseType } of cases) {
|
|
1253
|
+
if (caseType.type === 'Null') {
|
|
1254
|
+
// Special case for null type - the "null" is actually optional
|
|
1255
|
+
caseParsers[name] = (input, pos, _ctx) => {
|
|
1256
|
+
pos = consumeWhitespace(input, pos);
|
|
1257
|
+
if (input.startsWith('null', pos) && isTokenTerminator(input, pos + 4)) {
|
|
1258
|
+
return { value: null, position: pos + 4 };
|
|
1259
|
+
}
|
|
1260
|
+
else if (isTokenTerminator(input, pos)) {
|
|
1261
|
+
// No "null" present, but we're at a valid terminator - that's ok for optional null
|
|
1262
|
+
return { value: null, position: pos };
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
const got = pos >= input.length ? 'end of input' : `'${input[pos]}'`;
|
|
1266
|
+
throw new ParseError(`expected null, got ${got}`, pos);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
caseParsers[name] = createParser(caseType, frozen, typeCtx);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
typeCtx.pop();
|
|
1275
|
+
return ret;
|
|
1276
|
+
};
|
|
1277
|
+
/**
|
|
1278
|
+
* Parse an East value and infer its type from the syntax.
|
|
1279
|
+
*
|
|
1280
|
+
* @param input - The string to parse
|
|
1281
|
+
* @param frozen - Whether to freeze mutable containers (default: false)
|
|
1282
|
+
* @returns A tuple of [inferred type, parsed value]
|
|
1283
|
+
* @throws {Error} When parsing fails or type conflicts are detected
|
|
1284
|
+
*
|
|
1285
|
+
* @remarks
|
|
1286
|
+
* Type inference rules:
|
|
1287
|
+
* - Primitive types are distinguished by syntax (floats have `.` or `e`, integers don't)
|
|
1288
|
+
* - Empty array `[]` has type `Array<Never>`
|
|
1289
|
+
* - Empty set `{}` has type `Set<Never>`
|
|
1290
|
+
* - Empty dict `{:}` has type `Dict<Never, Never>`
|
|
1291
|
+
* - Non-empty collections widen element/key/value types via `TypeWiden`
|
|
1292
|
+
* - Each variant literal `.case data` creates a single-case variant, multiple cases widen
|
|
1293
|
+
* - Structs infer each field independently
|
|
1294
|
+
* - Recursive types are never inferred (circular references cause errors)
|
|
1295
|
+
* - Functions cannot be parsed
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* ```typescript
|
|
1299
|
+
* // Integer
|
|
1300
|
+
* const [type1, value1] = parseInferred("42");
|
|
1301
|
+
* // type1 = IntegerType, value1 = 42n
|
|
1302
|
+
*
|
|
1303
|
+
* // Float
|
|
1304
|
+
* const [type2, value2] = parseInferred("3.14");
|
|
1305
|
+
* // type2 = FloatType, value2 = 3.14
|
|
1306
|
+
*
|
|
1307
|
+
* // Array with widening
|
|
1308
|
+
* const [type3, value3] = parseInferred("[1, 2, 3]");
|
|
1309
|
+
* // type3 = ArrayType(IntegerType), value3 = [1n, 2n, 3n]
|
|
1310
|
+
*
|
|
1311
|
+
* // Variant with widening
|
|
1312
|
+
* const [type4, value4] = parseInferred(".some 42");
|
|
1313
|
+
* // type4 = VariantType({ some: IntegerType }), value4 = variant("some", 42n)
|
|
1314
|
+
*
|
|
1315
|
+
* // Type mismatch error
|
|
1316
|
+
* parseInferred("[1, 2.5]"); // Error: cannot widen Integer to Float
|
|
1317
|
+
* ```
|
|
1318
|
+
*/
|
|
1319
|
+
export function parseInferred(input, frozen = false) {
|
|
1320
|
+
try {
|
|
1321
|
+
const result = parseInferredValue(input, 0, frozen);
|
|
1322
|
+
const pos = consumeWhitespace(input, result.position);
|
|
1323
|
+
if (pos < input.length) {
|
|
1324
|
+
const { line, column } = getLineAndColumn(input, pos);
|
|
1325
|
+
throw new Error(`Unexpected input after parsed value at line ${line}, col ${column}`);
|
|
1326
|
+
}
|
|
1327
|
+
return [result.type, result.value];
|
|
1328
|
+
}
|
|
1329
|
+
catch (e) {
|
|
1330
|
+
if (e instanceof ParseError) {
|
|
1331
|
+
const { line, column } = getLineAndColumn(input, e.position);
|
|
1332
|
+
const pathStr = e.path ? ` at ${e.path}` : '';
|
|
1333
|
+
throw new Error(`${e.message}${pathStr} (line ${line}, col ${column})`);
|
|
1334
|
+
}
|
|
1335
|
+
throw e;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Parse a value and infer its type.
|
|
1340
|
+
*/
|
|
1341
|
+
function parseInferredValue(input, startPos, frozen) {
|
|
1342
|
+
const pos = consumeWhitespace(input, startPos);
|
|
1343
|
+
if (pos >= input.length) {
|
|
1344
|
+
throw new ParseError("unexpected end of input", pos);
|
|
1345
|
+
}
|
|
1346
|
+
const ch = input[pos];
|
|
1347
|
+
// Check for circular references - not supported in inference
|
|
1348
|
+
if (ch === '#' || (ch >= '0' && ch <= '9' && input.indexOf('#', pos) !== -1 && input.indexOf('#', pos) < pos + 10)) {
|
|
1349
|
+
// Rough heuristic: if we see a digit followed by # nearby, it might be a reference
|
|
1350
|
+
const nextHash = input.indexOf('#', pos);
|
|
1351
|
+
if (nextHash !== -1 && nextHash <= pos + 10) {
|
|
1352
|
+
throw new ParseError("circular references not supported in type inference", pos);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
// Dispatch based on first character
|
|
1356
|
+
if (ch === '[') {
|
|
1357
|
+
return parseInferredArray(input, pos, frozen);
|
|
1358
|
+
}
|
|
1359
|
+
else if (ch === '{') {
|
|
1360
|
+
return parseInferredSetOrDict(input, pos, frozen);
|
|
1361
|
+
}
|
|
1362
|
+
else if (ch === '(') {
|
|
1363
|
+
return parseInferredStruct(input, pos, frozen);
|
|
1364
|
+
}
|
|
1365
|
+
else if (ch === '.') {
|
|
1366
|
+
return parseInferredVariant(input, pos, frozen);
|
|
1367
|
+
}
|
|
1368
|
+
else if (ch === '"') {
|
|
1369
|
+
const { value, position } = parseString(input, pos);
|
|
1370
|
+
return { type: StringType, value, position };
|
|
1371
|
+
}
|
|
1372
|
+
else if (ch === '&') {
|
|
1373
|
+
throw new ParseError("ref types not supported in type inference", pos);
|
|
1374
|
+
}
|
|
1375
|
+
else if (input.startsWith('null', pos) && isTokenTerminator(input, pos + 4)) {
|
|
1376
|
+
return { type: NullType, value: null, position: pos + 4 };
|
|
1377
|
+
}
|
|
1378
|
+
else if (input.startsWith('true', pos) && isTokenTerminator(input, pos + 4)) {
|
|
1379
|
+
return { type: BooleanType, value: true, position: pos + 4 };
|
|
1380
|
+
}
|
|
1381
|
+
else if (input.startsWith('false', pos) && isTokenTerminator(input, pos + 5)) {
|
|
1382
|
+
return { type: BooleanType, value: false, position: pos + 5 };
|
|
1383
|
+
}
|
|
1384
|
+
else if (input.startsWith('0x', pos)) {
|
|
1385
|
+
const { value, position } = parseBlob(frozen)(input, pos);
|
|
1386
|
+
return { type: BlobType, value, position };
|
|
1387
|
+
}
|
|
1388
|
+
else if (input.startsWith('NaN', pos)) {
|
|
1389
|
+
return { type: FloatType, value: NaN, position: pos + 3 };
|
|
1390
|
+
}
|
|
1391
|
+
else if (input.startsWith('Infinity', pos)) {
|
|
1392
|
+
return { type: FloatType, value: Infinity, position: pos + 8 };
|
|
1393
|
+
}
|
|
1394
|
+
else if (ch === '-' && input.startsWith('-Infinity', pos)) {
|
|
1395
|
+
return { type: FloatType, value: -Infinity, position: pos + 9 };
|
|
1396
|
+
}
|
|
1397
|
+
else if (ch === '-' || (ch >= '0' && ch <= '9')) {
|
|
1398
|
+
// Check for datetime before trying number
|
|
1399
|
+
const datetimeMatch = /^-?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?/.exec(input.slice(pos));
|
|
1400
|
+
if (datetimeMatch) {
|
|
1401
|
+
const { value, position } = parseDateTime(frozen)(input, pos);
|
|
1402
|
+
return { type: DateTimeType, value, position };
|
|
1403
|
+
}
|
|
1404
|
+
return parseInferredNumber(input, pos);
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
throw new ParseError(`unexpected character '${ch}'`, pos);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Parse a number and infer whether it's an integer or float.
|
|
1412
|
+
* Floats must contain '.' or 'e'/'E', otherwise it's an integer.
|
|
1413
|
+
*/
|
|
1414
|
+
function parseInferredNumber(input, pos) {
|
|
1415
|
+
// Try to parse as float first (lookahead for . or e)
|
|
1416
|
+
let hasDecimal = false;
|
|
1417
|
+
let hasExponent = false;
|
|
1418
|
+
let scanPos = pos;
|
|
1419
|
+
if (input[scanPos] === '-')
|
|
1420
|
+
scanPos++;
|
|
1421
|
+
while (scanPos < input.length && input[scanPos] >= '0' && input[scanPos] <= '9') {
|
|
1422
|
+
scanPos++;
|
|
1423
|
+
}
|
|
1424
|
+
if (scanPos < input.length && input[scanPos] === '.') {
|
|
1425
|
+
hasDecimal = true;
|
|
1426
|
+
}
|
|
1427
|
+
if (scanPos < input.length && (input[scanPos] === 'e' || input[scanPos] === 'E')) {
|
|
1428
|
+
hasExponent = true;
|
|
1429
|
+
}
|
|
1430
|
+
if (hasDecimal || hasExponent) {
|
|
1431
|
+
// It's a float
|
|
1432
|
+
const { value, position } = parseFloat(input, pos);
|
|
1433
|
+
return { type: FloatType, value, position };
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
// It's an integer
|
|
1437
|
+
const { value, position } = parseInteger(input, pos);
|
|
1438
|
+
return { type: IntegerType, value, position };
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Parse an array and infer the element type by widening.
|
|
1443
|
+
*/
|
|
1444
|
+
function parseInferredArray(input, startPos, frozen) {
|
|
1445
|
+
let pos = consumeWhitespace(input, startPos);
|
|
1446
|
+
if (input[pos] !== '[') {
|
|
1447
|
+
throw new ParseError("expected '[' to start array", pos);
|
|
1448
|
+
}
|
|
1449
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1450
|
+
const values = [];
|
|
1451
|
+
let elementType = NeverType;
|
|
1452
|
+
// Handle empty array
|
|
1453
|
+
if (input[pos] === ']') {
|
|
1454
|
+
const arr = frozen ? Object.freeze(values) : values;
|
|
1455
|
+
return { type: ArrayType(NeverType), value: arr, position: pos + 1 };
|
|
1456
|
+
}
|
|
1457
|
+
let elementIndex = 0;
|
|
1458
|
+
while (true) {
|
|
1459
|
+
try {
|
|
1460
|
+
const element = parseInferredValue(input, pos, frozen);
|
|
1461
|
+
values.push(element.value);
|
|
1462
|
+
// Widen element type
|
|
1463
|
+
try {
|
|
1464
|
+
elementType = TypeWiden(elementType, element.type);
|
|
1465
|
+
}
|
|
1466
|
+
catch (cause) {
|
|
1467
|
+
if (cause instanceof Error && cause.name === "TypeMismatchError") {
|
|
1468
|
+
throw new ParseError(`incompatible array element types: ${cause.message}`, pos, `[${elementIndex}]`);
|
|
1469
|
+
}
|
|
1470
|
+
throw cause;
|
|
1471
|
+
}
|
|
1472
|
+
pos = consumeWhitespace(input, element.position);
|
|
1473
|
+
}
|
|
1474
|
+
catch (e) {
|
|
1475
|
+
if (e instanceof ParseError) {
|
|
1476
|
+
const newPath = `[${elementIndex}]` + (e.path ? e.path : '');
|
|
1477
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1478
|
+
}
|
|
1479
|
+
throw e;
|
|
1480
|
+
}
|
|
1481
|
+
if (input[pos] === ']') {
|
|
1482
|
+
const arr = frozen ? Object.freeze(values) : values;
|
|
1483
|
+
return { type: ArrayType(elementType), value: arr, position: pos + 1 };
|
|
1484
|
+
}
|
|
1485
|
+
if (input[pos] !== ',') {
|
|
1486
|
+
throw new ParseError("expected ',' or ']' after array element", pos);
|
|
1487
|
+
}
|
|
1488
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1489
|
+
elementIndex++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Parse either a set or dict and infer types.
|
|
1494
|
+
* Distinguishes based on presence of ':' (dict) vs no ':' (set).
|
|
1495
|
+
*/
|
|
1496
|
+
function parseInferredSetOrDict(input, startPos, frozen) {
|
|
1497
|
+
let pos = consumeWhitespace(input, startPos);
|
|
1498
|
+
if (input[pos] !== '{') {
|
|
1499
|
+
throw new ParseError("expected '{' to start set or dict", pos);
|
|
1500
|
+
}
|
|
1501
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1502
|
+
// Check for empty dict syntax {:}
|
|
1503
|
+
if (input[pos] === ':') {
|
|
1504
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1505
|
+
if (input[pos] === '}') {
|
|
1506
|
+
const dict = new SortedMap(undefined, compareFor(NeverType));
|
|
1507
|
+
if (frozen)
|
|
1508
|
+
Object.freeze(dict);
|
|
1509
|
+
return { type: DictType(NeverType, NeverType), value: dict, position: pos + 1 };
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
throw new ParseError("expected '}' after ':' in empty dict", pos);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
// Check for empty set syntax {}
|
|
1516
|
+
if (input[pos] === '}') {
|
|
1517
|
+
const set = new SortedSet(undefined, compareFor(NeverType));
|
|
1518
|
+
if (frozen)
|
|
1519
|
+
Object.freeze(set);
|
|
1520
|
+
return { type: SetType(NeverType), value: set, position: pos + 1 };
|
|
1521
|
+
}
|
|
1522
|
+
// Parse first element to determine if set or dict
|
|
1523
|
+
const firstElementPos = pos;
|
|
1524
|
+
const firstElement = parseInferredValue(input, pos, frozen);
|
|
1525
|
+
pos = consumeWhitespace(input, firstElement.position);
|
|
1526
|
+
if (input[pos] === ':') {
|
|
1527
|
+
// It's a dict
|
|
1528
|
+
return parseInferredDictRest(input, startPos, frozen, firstElement, firstElementPos);
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
// It's a set
|
|
1532
|
+
return parseInferredSetRest(input, startPos, frozen, firstElement, firstElementPos);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Continue parsing a set after determining it's a set.
|
|
1537
|
+
*/
|
|
1538
|
+
function parseInferredSetRest(input, startPos, frozen, firstElement, firstElementPos) {
|
|
1539
|
+
let keyType = firstElement.type;
|
|
1540
|
+
const compare = compareFor(keyType);
|
|
1541
|
+
const values = new SortedSet(undefined, compare);
|
|
1542
|
+
try {
|
|
1543
|
+
values.add(firstElement.value);
|
|
1544
|
+
}
|
|
1545
|
+
catch (e) {
|
|
1546
|
+
throw new ParseError(`invalid set key: ${e instanceof Error ? e.message : String(e)}`, firstElementPos);
|
|
1547
|
+
}
|
|
1548
|
+
let pos = consumeWhitespace(input, firstElement.position);
|
|
1549
|
+
let elementIndex = 1;
|
|
1550
|
+
while (true) {
|
|
1551
|
+
if (input[pos] === '}') {
|
|
1552
|
+
if (frozen)
|
|
1553
|
+
Object.freeze(values);
|
|
1554
|
+
return { type: SetType(keyType), value: values, position: pos + 1 };
|
|
1555
|
+
}
|
|
1556
|
+
if (input[pos] !== ',') {
|
|
1557
|
+
throw new ParseError("expected ',' or '}' after set element", pos);
|
|
1558
|
+
}
|
|
1559
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1560
|
+
try {
|
|
1561
|
+
const element = parseInferredValue(input, pos, frozen);
|
|
1562
|
+
// Widen key type
|
|
1563
|
+
try {
|
|
1564
|
+
const newKeyType = TypeWiden(keyType, element.type);
|
|
1565
|
+
if (!isTypeEqual(keyType, newKeyType)) {
|
|
1566
|
+
// Type changed - need to rebuild set with new comparator
|
|
1567
|
+
const newCompare = compareFor(newKeyType);
|
|
1568
|
+
const newValues = new SortedSet(values, newCompare);
|
|
1569
|
+
values.clear();
|
|
1570
|
+
for (const v of newValues) {
|
|
1571
|
+
values.add(v);
|
|
1572
|
+
}
|
|
1573
|
+
keyType = newKeyType;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
catch (cause) {
|
|
1577
|
+
if (cause instanceof Error && cause.name === "TypeMismatchError") {
|
|
1578
|
+
throw new ParseError(`incompatible set element types: ${cause.message}`, pos, `[${elementIndex}]`);
|
|
1579
|
+
}
|
|
1580
|
+
throw cause;
|
|
1581
|
+
}
|
|
1582
|
+
values.add(element.value);
|
|
1583
|
+
pos = consumeWhitespace(input, element.position);
|
|
1584
|
+
}
|
|
1585
|
+
catch (e) {
|
|
1586
|
+
if (e instanceof ParseError) {
|
|
1587
|
+
const newPath = `[${elementIndex}]` + (e.path ? e.path : '');
|
|
1588
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1589
|
+
}
|
|
1590
|
+
throw e;
|
|
1591
|
+
}
|
|
1592
|
+
elementIndex++;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Continue parsing a dict after determining it's a dict.
|
|
1597
|
+
*/
|
|
1598
|
+
function parseInferredDictRest(input, _startPos, frozen, firstKey, _firstKeyPos) {
|
|
1599
|
+
let keyType = firstKey.type;
|
|
1600
|
+
let valueType = NeverType;
|
|
1601
|
+
const compare = compareFor(keyType);
|
|
1602
|
+
const values = new SortedMap(undefined, compare);
|
|
1603
|
+
let pos = consumeWhitespace(input, firstKey.position);
|
|
1604
|
+
if (input[pos] !== ':') {
|
|
1605
|
+
throw new ParseError("expected ':' after dict key", pos);
|
|
1606
|
+
}
|
|
1607
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1608
|
+
try {
|
|
1609
|
+
const firstValue = parseInferredValue(input, pos, frozen);
|
|
1610
|
+
valueType = firstValue.type;
|
|
1611
|
+
values.set(firstKey.value, firstValue.value);
|
|
1612
|
+
pos = consumeWhitespace(input, firstValue.position);
|
|
1613
|
+
}
|
|
1614
|
+
catch (e) {
|
|
1615
|
+
if (e instanceof ParseError) {
|
|
1616
|
+
const keyPrinter = printFor(keyType);
|
|
1617
|
+
const keyStr = keyPrinter(firstKey.value);
|
|
1618
|
+
const newPath = `[${keyStr}]` + (e.path ? e.path : '');
|
|
1619
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1620
|
+
}
|
|
1621
|
+
throw e;
|
|
1622
|
+
}
|
|
1623
|
+
let entryIndex = 1;
|
|
1624
|
+
while (true) {
|
|
1625
|
+
if (input[pos] === '}') {
|
|
1626
|
+
if (frozen)
|
|
1627
|
+
Object.freeze(values);
|
|
1628
|
+
return { type: DictType(keyType, valueType), value: values, position: pos + 1 };
|
|
1629
|
+
}
|
|
1630
|
+
if (input[pos] !== ',') {
|
|
1631
|
+
throw new ParseError("expected ',' or '}' after dict entry", pos);
|
|
1632
|
+
}
|
|
1633
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1634
|
+
let dictKey;
|
|
1635
|
+
let dictKeyType;
|
|
1636
|
+
let keyPos;
|
|
1637
|
+
try {
|
|
1638
|
+
const keyElement = parseInferredValue(input, pos, frozen);
|
|
1639
|
+
dictKey = keyElement.value;
|
|
1640
|
+
dictKeyType = keyElement.type;
|
|
1641
|
+
keyPos = pos;
|
|
1642
|
+
pos = consumeWhitespace(input, keyElement.position);
|
|
1643
|
+
}
|
|
1644
|
+
catch (e) {
|
|
1645
|
+
if (e instanceof ParseError) {
|
|
1646
|
+
const newPath = `[${entryIndex}](key)` + (e.path ? e.path : '');
|
|
1647
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1648
|
+
}
|
|
1649
|
+
throw e;
|
|
1650
|
+
}
|
|
1651
|
+
if (input[pos] !== ':') {
|
|
1652
|
+
throw new ParseError("expected ':' after dict key", pos);
|
|
1653
|
+
}
|
|
1654
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1655
|
+
try {
|
|
1656
|
+
const valueElement = parseInferredValue(input, pos, frozen);
|
|
1657
|
+
// Widen key and value types
|
|
1658
|
+
try {
|
|
1659
|
+
const newKeyType = TypeWiden(keyType, dictKeyType);
|
|
1660
|
+
const newValueType = TypeWiden(valueType, valueElement.type);
|
|
1661
|
+
if (!isTypeEqual(keyType, newKeyType)) {
|
|
1662
|
+
// Key type changed - need to rebuild dict with new comparator
|
|
1663
|
+
const newCompare = compareFor(newKeyType);
|
|
1664
|
+
const newValues = new SortedMap(values, newCompare);
|
|
1665
|
+
values.clear();
|
|
1666
|
+
for (const [k, v] of newValues) {
|
|
1667
|
+
values.set(k, v);
|
|
1668
|
+
}
|
|
1669
|
+
keyType = newKeyType;
|
|
1670
|
+
}
|
|
1671
|
+
valueType = newValueType;
|
|
1672
|
+
}
|
|
1673
|
+
catch (cause) {
|
|
1674
|
+
if (cause instanceof Error && cause.name === "TypeMismatchError") {
|
|
1675
|
+
throw new ParseError(`incompatible dict types: ${cause.message}`, keyPos, `[${entryIndex}]`);
|
|
1676
|
+
}
|
|
1677
|
+
throw cause;
|
|
1678
|
+
}
|
|
1679
|
+
values.set(dictKey, valueElement.value);
|
|
1680
|
+
pos = consumeWhitespace(input, valueElement.position);
|
|
1681
|
+
}
|
|
1682
|
+
catch (e) {
|
|
1683
|
+
if (e instanceof ParseError) {
|
|
1684
|
+
const keyPrinter = printFor(keyType);
|
|
1685
|
+
const keyStr = keyPrinter(dictKey);
|
|
1686
|
+
const newPath = `[${keyStr}]` + (e.path ? e.path : '');
|
|
1687
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1688
|
+
}
|
|
1689
|
+
throw e;
|
|
1690
|
+
}
|
|
1691
|
+
entryIndex++;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Parse a struct and infer field types.
|
|
1696
|
+
*/
|
|
1697
|
+
function parseInferredStruct(input, startPos, frozen) {
|
|
1698
|
+
let pos = consumeWhitespace(input, startPos);
|
|
1699
|
+
if (input[pos] !== '(') {
|
|
1700
|
+
throw new ParseError("expected '(' to start struct", pos);
|
|
1701
|
+
}
|
|
1702
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1703
|
+
const fields = {};
|
|
1704
|
+
const values = {};
|
|
1705
|
+
const fieldOrder = [];
|
|
1706
|
+
// Handle empty struct
|
|
1707
|
+
if (input[pos] === ')') {
|
|
1708
|
+
const struct = frozen ? Object.freeze(values) : values;
|
|
1709
|
+
return { type: StructType({}), value: struct, position: pos + 1 };
|
|
1710
|
+
}
|
|
1711
|
+
while (true) {
|
|
1712
|
+
// Parse field name
|
|
1713
|
+
const { value: fieldName, position: namePos } = parseIdentifier(input, pos);
|
|
1714
|
+
pos = consumeWhitespace(input, namePos);
|
|
1715
|
+
if (input[pos] !== '=') {
|
|
1716
|
+
throw new ParseError(`expected '=' after field name '${fieldName}'`, pos);
|
|
1717
|
+
}
|
|
1718
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1719
|
+
// Parse field value and infer type
|
|
1720
|
+
const fieldPath = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName) ? `.${fieldName}` : `[${JSON.stringify(fieldName)}]`;
|
|
1721
|
+
try {
|
|
1722
|
+
const fieldValue = parseInferredValue(input, pos, frozen);
|
|
1723
|
+
fields[fieldName] = fieldValue.type;
|
|
1724
|
+
values[fieldName] = fieldValue.value;
|
|
1725
|
+
fieldOrder.push(fieldName);
|
|
1726
|
+
pos = consumeWhitespace(input, fieldValue.position);
|
|
1727
|
+
}
|
|
1728
|
+
catch (e) {
|
|
1729
|
+
if (e instanceof ParseError) {
|
|
1730
|
+
const newPath = fieldPath + (e.path ? e.path : '');
|
|
1731
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1732
|
+
}
|
|
1733
|
+
throw e;
|
|
1734
|
+
}
|
|
1735
|
+
if (input[pos] === ')') {
|
|
1736
|
+
// Reorder fields to match insertion order
|
|
1737
|
+
const orderedFields = {};
|
|
1738
|
+
for (const name of fieldOrder) {
|
|
1739
|
+
orderedFields[name] = fields[name];
|
|
1740
|
+
}
|
|
1741
|
+
const struct = frozen ? Object.freeze(values) : values;
|
|
1742
|
+
return { type: StructType(orderedFields), value: struct, position: pos + 1 };
|
|
1743
|
+
}
|
|
1744
|
+
if (input[pos] !== ',') {
|
|
1745
|
+
throw new ParseError("expected ',' or ')' after struct field", pos);
|
|
1746
|
+
}
|
|
1747
|
+
pos = consumeWhitespace(input, pos + 1);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Parse a variant and infer the case type.
|
|
1752
|
+
*/
|
|
1753
|
+
function parseInferredVariant(input, startPos, frozen) {
|
|
1754
|
+
let pos = consumeWhitespace(input, startPos);
|
|
1755
|
+
if (input[pos] !== '.') {
|
|
1756
|
+
throw new ParseError("expected '.' to start variant case", pos);
|
|
1757
|
+
}
|
|
1758
|
+
pos += 1;
|
|
1759
|
+
// Check for whitespace after '.'
|
|
1760
|
+
const pos2 = consumeWhitespace(input, pos);
|
|
1761
|
+
if (pos2 !== pos) {
|
|
1762
|
+
throw new ParseError("whitespace not allowed between '.' and case identifier", pos);
|
|
1763
|
+
}
|
|
1764
|
+
// Parse case name
|
|
1765
|
+
const { value: caseName, position: casePos } = parseIdentifier(input, pos);
|
|
1766
|
+
pos = consumeWhitespace(input, casePos);
|
|
1767
|
+
// Check if there's data following
|
|
1768
|
+
let caseType;
|
|
1769
|
+
let caseValue;
|
|
1770
|
+
let finalPos;
|
|
1771
|
+
if (isTokenTerminator(input, pos)) {
|
|
1772
|
+
// No data - this is a null variant
|
|
1773
|
+
caseType = NullType;
|
|
1774
|
+
caseValue = null;
|
|
1775
|
+
finalPos = pos;
|
|
1776
|
+
}
|
|
1777
|
+
else {
|
|
1778
|
+
// Parse the associated data
|
|
1779
|
+
try {
|
|
1780
|
+
const dataResult = parseInferredValue(input, pos, frozen);
|
|
1781
|
+
caseType = dataResult.type;
|
|
1782
|
+
caseValue = dataResult.value;
|
|
1783
|
+
finalPos = dataResult.position;
|
|
1784
|
+
}
|
|
1785
|
+
catch (e) {
|
|
1786
|
+
if (e instanceof ParseError) {
|
|
1787
|
+
const newPath = `.${caseName}` + (e.path ? e.path : '');
|
|
1788
|
+
throw new ParseError(e.message, e.position, newPath);
|
|
1789
|
+
}
|
|
1790
|
+
throw e;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
const v = variant(caseName, caseValue);
|
|
1794
|
+
if (frozen)
|
|
1795
|
+
Object.freeze(v);
|
|
1796
|
+
return {
|
|
1797
|
+
type: VariantType({ [caseName]: caseType }),
|
|
1798
|
+
value: v,
|
|
1799
|
+
position: finalPos
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
//# sourceMappingURL=east.js.map
|