@altopelago/aeon-parser 0.9.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/README.md +26 -0
- package/dist/ast.d.ts +260 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +121 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1282 -0
- package/dist/parser.js.map +1 -0
- package/dist/path-resolver.d.ts +10 -0
- package/dist/path-resolver.d.ts.map +1 -0
- package/dist/path-resolver.js +11 -0
- package/dist/path-resolver.js.map +1 -0
- package/dist/trimticks.d.ts +7 -0
- package/dist/trimticks.d.ts.map +1 -0
- package/dist/trimticks.js +61 -0
- package/dist/trimticks.js.map +1 -0
- package/package.json +29 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
import { TokenType, createSpan } from '@altopelago/aeon-lexer';
|
|
2
|
+
import { ParserError, SyntaxError, DuplicateKeyError, InvalidSeparatorCharError, SeparatorDepthExceededError, GenericDepthExceededError, AttributeDepthExceededError, NestingDepthExceededError, } from './errors.js';
|
|
3
|
+
import { applyTrimticks } from './trimticks.js';
|
|
4
|
+
/**
|
|
5
|
+
* Recursive-descent parser for AEON documents
|
|
6
|
+
*/
|
|
7
|
+
class Parser {
|
|
8
|
+
tokens;
|
|
9
|
+
maxAttributeDepth;
|
|
10
|
+
maxSeparatorDepth;
|
|
11
|
+
maxGenericDepth;
|
|
12
|
+
maxNestingDepth;
|
|
13
|
+
currentNestingDepth = 0;
|
|
14
|
+
current = 0;
|
|
15
|
+
errors = [];
|
|
16
|
+
constructor(tokens, options = {}) {
|
|
17
|
+
this.tokens = tokens;
|
|
18
|
+
this.maxAttributeDepth = options.maxAttributeDepth ?? 1;
|
|
19
|
+
this.maxSeparatorDepth = options.maxSeparatorDepth ?? 1;
|
|
20
|
+
this.maxGenericDepth = options.maxGenericDepth ?? 1;
|
|
21
|
+
this.maxNestingDepth = options.maxNestingDepth ?? 256;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse the document
|
|
25
|
+
*/
|
|
26
|
+
parse() {
|
|
27
|
+
try {
|
|
28
|
+
const document = this.parseDocument();
|
|
29
|
+
return {
|
|
30
|
+
document,
|
|
31
|
+
errors: this.errors,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (e instanceof ParserError) {
|
|
36
|
+
this.errors.push(e);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
document: null,
|
|
40
|
+
errors: this.errors,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ============================================
|
|
45
|
+
// Document parsing
|
|
46
|
+
// ============================================
|
|
47
|
+
parseDocument() {
|
|
48
|
+
const start = this.peek().span.start;
|
|
49
|
+
let header = null;
|
|
50
|
+
const bindings = [];
|
|
51
|
+
const keys = new Set();
|
|
52
|
+
// Check for header forms
|
|
53
|
+
if (this.isHeaderStart()) {
|
|
54
|
+
header = this.parseHeader();
|
|
55
|
+
}
|
|
56
|
+
// Parse body bindings
|
|
57
|
+
while (!this.isAtEnd()) {
|
|
58
|
+
try {
|
|
59
|
+
if (bindings.length > 0 && this.isStructuredHeaderStart()) {
|
|
60
|
+
const headerStart = this.peek();
|
|
61
|
+
this.errors.push(new SyntaxError('Structured headers must precede body bindings', headerStart.span, 'top-level binding', 'aeon:header'));
|
|
62
|
+
this.parseHeader();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const binding = this.parseBinding();
|
|
66
|
+
if (binding) {
|
|
67
|
+
if (keys.has(binding.key)) {
|
|
68
|
+
this.errors.push(new DuplicateKeyError(binding.key, binding.span, this.rootKeyPath(binding.key)));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
keys.add(binding.key);
|
|
72
|
+
}
|
|
73
|
+
bindings.push(binding);
|
|
74
|
+
this.consumeSeparatorOrLineBreak(TokenType.EOF, 'Expected \',\' or newline between top-level bindings');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e instanceof ParserError) {
|
|
79
|
+
this.errors.push(e);
|
|
80
|
+
this.synchronize();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const end = this.previous().span.end;
|
|
88
|
+
return {
|
|
89
|
+
type: 'Document',
|
|
90
|
+
header,
|
|
91
|
+
bindings,
|
|
92
|
+
envelope: null,
|
|
93
|
+
span: createSpan(start, end),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
rootKeyPath(key) {
|
|
97
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
98
|
+
return `$.${key}`;
|
|
99
|
+
}
|
|
100
|
+
return `$.[${JSON.stringify(key)}]`;
|
|
101
|
+
}
|
|
102
|
+
isHeaderStart() {
|
|
103
|
+
if (!this.check(TokenType.Identifier))
|
|
104
|
+
return false;
|
|
105
|
+
const token = this.peek();
|
|
106
|
+
if (token.value !== 'aeon')
|
|
107
|
+
return false;
|
|
108
|
+
// Look ahead for colon
|
|
109
|
+
if (this.current + 1 < this.tokens.length) {
|
|
110
|
+
const next = this.tokens[this.current + 1];
|
|
111
|
+
if (next.type !== TokenType.Colon)
|
|
112
|
+
return false;
|
|
113
|
+
const nextNext = this.tokens[this.current + 2];
|
|
114
|
+
const nextNextNext = this.tokens[this.current + 3];
|
|
115
|
+
if (nextNext?.type === TokenType.Identifier &&
|
|
116
|
+
nextNext.value === 'envelope' &&
|
|
117
|
+
nextNextNext?.type === TokenType.Equals) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
isStructuredHeaderStart() {
|
|
125
|
+
if (!this.isHeaderStart())
|
|
126
|
+
return false;
|
|
127
|
+
const fieldToken = this.tokens[this.current + 2];
|
|
128
|
+
const equalsToken = this.tokens[this.current + 3];
|
|
129
|
+
return fieldToken?.type === TokenType.Identifier
|
|
130
|
+
&& fieldToken.value === 'header'
|
|
131
|
+
&& equalsToken?.type === TokenType.Equals;
|
|
132
|
+
}
|
|
133
|
+
parseHeader() {
|
|
134
|
+
const start = this.peek().span.start;
|
|
135
|
+
const fields = new Map();
|
|
136
|
+
const bindings = [];
|
|
137
|
+
let hasStructured = false;
|
|
138
|
+
let hasShorthand = false;
|
|
139
|
+
const seenShorthandFields = new Set();
|
|
140
|
+
// Parse header lines (aeon:xxx = ...)
|
|
141
|
+
while (this.isHeaderStart()) {
|
|
142
|
+
this.advance(); // consume 'aeon'
|
|
143
|
+
this.consume(TokenType.Colon, "Expected ':' after 'aeon'");
|
|
144
|
+
const fieldToken = this.consume(TokenType.Identifier, "Expected header field name");
|
|
145
|
+
const fieldName = fieldToken.value;
|
|
146
|
+
this.consume(TokenType.Equals, "Expected '=' in header");
|
|
147
|
+
if (fieldName === 'header') {
|
|
148
|
+
hasStructured = true;
|
|
149
|
+
const value = this.parseValue();
|
|
150
|
+
// Extract fields from structured header
|
|
151
|
+
if (value.type === 'ObjectNode') {
|
|
152
|
+
for (const binding of value.bindings) {
|
|
153
|
+
bindings.push(binding);
|
|
154
|
+
fields.set(binding.key, binding.value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
hasShorthand = true;
|
|
160
|
+
const value = this.parseValue();
|
|
161
|
+
const bindingSpan = createSpan(fieldToken.span.start, value.span.end);
|
|
162
|
+
bindings.push({
|
|
163
|
+
type: 'Binding',
|
|
164
|
+
key: fieldName,
|
|
165
|
+
value,
|
|
166
|
+
datatype: null,
|
|
167
|
+
attributes: [],
|
|
168
|
+
span: bindingSpan,
|
|
169
|
+
});
|
|
170
|
+
if (seenShorthandFields.has(fieldName)) {
|
|
171
|
+
this.errors.push(new DuplicateKeyError(`aeon:${fieldName}`, fieldToken.span));
|
|
172
|
+
}
|
|
173
|
+
seenShorthandFields.add(fieldName);
|
|
174
|
+
fields.set(fieldName, value);
|
|
175
|
+
}
|
|
176
|
+
this.consumeSeparatorOrLineBreak(TokenType.EOF, 'Expected \',\' or newline between header bindings');
|
|
177
|
+
}
|
|
178
|
+
const end = this.previous().span.end;
|
|
179
|
+
const form = hasStructured ? 'structured' : 'shorthand';
|
|
180
|
+
return {
|
|
181
|
+
type: 'Header',
|
|
182
|
+
form,
|
|
183
|
+
hasStructured,
|
|
184
|
+
hasShorthand,
|
|
185
|
+
bindings,
|
|
186
|
+
fields,
|
|
187
|
+
span: createSpan(start, end),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// ============================================
|
|
191
|
+
// Binding parsing
|
|
192
|
+
// ============================================
|
|
193
|
+
parseBinding() {
|
|
194
|
+
// Skip any stray newlines at the start
|
|
195
|
+
// (handled by lexer not including newlines by default)
|
|
196
|
+
if (this.isAtEnd())
|
|
197
|
+
return null;
|
|
198
|
+
const start = this.peek().span.start;
|
|
199
|
+
// Parse key
|
|
200
|
+
if (!this.check(TokenType.Identifier) && !this.check(TokenType.String)) {
|
|
201
|
+
if (this.isAtEnd())
|
|
202
|
+
return null;
|
|
203
|
+
throw new SyntaxError(`Expected key, found '${this.peek().value}'`, this.peek().span, 'key', this.peek().value);
|
|
204
|
+
}
|
|
205
|
+
const keyToken = this.advance();
|
|
206
|
+
const key = this.keyFromToken(keyToken);
|
|
207
|
+
// Parse optional attributes @{...}
|
|
208
|
+
const attributes = [];
|
|
209
|
+
if (this.check(TokenType.At)) {
|
|
210
|
+
attributes.push(this.parseAttribute(1));
|
|
211
|
+
if (this.check(TokenType.At)) {
|
|
212
|
+
throw new SyntaxError('Only one attribute block is allowed before a binding datatype', this.peek().span, ': or =', this.peek().value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Parse optional datatype :type
|
|
216
|
+
let datatype = null;
|
|
217
|
+
if (this.check(TokenType.Colon)) {
|
|
218
|
+
this.advance(); // consume :
|
|
219
|
+
datatype = this.parseTypeAnnotation();
|
|
220
|
+
}
|
|
221
|
+
// Expect =
|
|
222
|
+
if (!this.check(TokenType.Equals)) {
|
|
223
|
+
throw new SyntaxError(`Expected '=' after key '${key}'`, this.peek().span, '=', this.peek().value);
|
|
224
|
+
}
|
|
225
|
+
this.advance(); // consume =
|
|
226
|
+
// Parse value
|
|
227
|
+
const value = this.parseValue();
|
|
228
|
+
const end = this.previous().span.end;
|
|
229
|
+
return {
|
|
230
|
+
type: 'Binding',
|
|
231
|
+
key,
|
|
232
|
+
value,
|
|
233
|
+
datatype,
|
|
234
|
+
attributes,
|
|
235
|
+
span: createSpan(start, end),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
parseAttribute(depth) {
|
|
239
|
+
if (depth > this.maxAttributeDepth) {
|
|
240
|
+
throw new AttributeDepthExceededError(depth, this.maxAttributeDepth, this.peek().span);
|
|
241
|
+
}
|
|
242
|
+
const start = this.peek().span.start;
|
|
243
|
+
this.advance(); // consume @
|
|
244
|
+
this.consume(TokenType.LeftBrace, "Expected '{' after '@'");
|
|
245
|
+
const entries = new Map();
|
|
246
|
+
while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
|
|
247
|
+
const attrKeyToken = this.consumeOneOf([TokenType.Identifier, TokenType.String], "Expected attribute key");
|
|
248
|
+
const attrKey = this.keyFromToken(attrKeyToken);
|
|
249
|
+
if (RESERVED_ATTRIBUTE_KEYS.has(attrKey)) {
|
|
250
|
+
throw new SyntaxError(`Reserved attribute key: ${attrKey}`, attrKeyToken.span, 'non-reserved attribute key', attrKeyToken.value);
|
|
251
|
+
}
|
|
252
|
+
const attributes = [];
|
|
253
|
+
if (this.check(TokenType.At)) {
|
|
254
|
+
attributes.push(this.parseAttribute(depth + 1));
|
|
255
|
+
if (this.check(TokenType.At)) {
|
|
256
|
+
throw new SyntaxError('Only one attribute block is allowed before an attribute entry datatype', this.peek().span, ': or =', this.peek().value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Optional datatype
|
|
260
|
+
let attrDatatype = null;
|
|
261
|
+
if (this.check(TokenType.Colon)) {
|
|
262
|
+
this.advance();
|
|
263
|
+
attrDatatype = this.parseTypeAnnotation();
|
|
264
|
+
}
|
|
265
|
+
this.consume(TokenType.Equals, "Expected '=' in attribute");
|
|
266
|
+
const attrValue = this.parseValue();
|
|
267
|
+
if (entries.has(attrKey)) {
|
|
268
|
+
this.errors.push(new DuplicateKeyError(attrKey, attrKeyToken.span));
|
|
269
|
+
}
|
|
270
|
+
entries.set(attrKey, { value: attrValue, datatype: attrDatatype, attributes });
|
|
271
|
+
if (!this.check(TokenType.RightBrace)) {
|
|
272
|
+
this.consumeSeparatorOrLineBreak(TokenType.RightBrace, 'Expected \',\' or newline between attribute entries');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.consume(TokenType.RightBrace, "Expected '}' to close attribute");
|
|
276
|
+
const end = this.previous().span.end;
|
|
277
|
+
return {
|
|
278
|
+
type: 'Attribute',
|
|
279
|
+
entries,
|
|
280
|
+
span: createSpan(start, end),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
parseTypeAnnotation(genericDepth = 0) {
|
|
284
|
+
if (genericDepth > this.maxGenericDepth) {
|
|
285
|
+
throw new GenericDepthExceededError(genericDepth, this.maxGenericDepth, this.peek().span);
|
|
286
|
+
}
|
|
287
|
+
const start = this.peek().span.start;
|
|
288
|
+
const name = this.consume(TokenType.Identifier, "Expected type name").value;
|
|
289
|
+
const genericArgs = [];
|
|
290
|
+
let radixBase = null;
|
|
291
|
+
const separators = [];
|
|
292
|
+
// Parse optional generic args: TypeName<arg1, arg2>
|
|
293
|
+
if (this.check(TokenType.LeftAngle)) {
|
|
294
|
+
if (name === 'radix') {
|
|
295
|
+
throw new SyntaxError("Radix datatype bases must use bracket syntax like 'radix[10]'", this.peek().span, 'radix[10]', this.peek().value);
|
|
296
|
+
}
|
|
297
|
+
this.advance(); // consume <
|
|
298
|
+
genericArgs.push(this.parseGenericArgument(genericDepth));
|
|
299
|
+
while (this.check(TokenType.Comma)) {
|
|
300
|
+
this.advance();
|
|
301
|
+
genericArgs.push(this.parseGenericArgument(genericDepth));
|
|
302
|
+
}
|
|
303
|
+
this.consume(TokenType.RightAngle, "Expected '>' to close generic arguments");
|
|
304
|
+
}
|
|
305
|
+
// Parse repeated separator specifiers: [x][,][;]
|
|
306
|
+
while (this.check(TokenType.LeftBracket)) {
|
|
307
|
+
this.advance(); // consume [
|
|
308
|
+
if (RESERVED_V1_DATATYPES.has(name) && !BRACKETED_V1_DATATYPES.has(name)) {
|
|
309
|
+
throw new SyntaxError(`Datatype '${name}' does not support bracket specifiers in v1`, this.peek().span, null, name);
|
|
310
|
+
}
|
|
311
|
+
if (name === 'radix' && radixBase === null) {
|
|
312
|
+
radixBase = this.parseRadixBaseSpecifier();
|
|
313
|
+
this.consume(TokenType.RightBracket, "Expected ']' to close radix base spec");
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (name === 'radix') {
|
|
317
|
+
throw new SyntaxError("Radix datatype allows exactly one base bracket like 'radix[10]'", this.peek().span, 'radix[10]', this.peek().value);
|
|
318
|
+
}
|
|
319
|
+
const spec = RESERVED_V1_DATATYPES.has(name)
|
|
320
|
+
? this.parseSeparatorCharacter()
|
|
321
|
+
: this.parseCustomBracketSpecifier();
|
|
322
|
+
separators.push(spec);
|
|
323
|
+
this.consume(TokenType.RightBracket, "Expected ']' to close separator spec");
|
|
324
|
+
if (separators.length > this.maxSeparatorDepth) {
|
|
325
|
+
throw new SeparatorDepthExceededError(separators.length, this.maxSeparatorDepth, this.previous().span);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.validateReservedDatatypeAdornments(name, genericArgs, radixBase, separators);
|
|
329
|
+
const end = this.previous().span.end;
|
|
330
|
+
return {
|
|
331
|
+
type: 'TypeAnnotation',
|
|
332
|
+
name,
|
|
333
|
+
genericArgs,
|
|
334
|
+
radixBase,
|
|
335
|
+
separators,
|
|
336
|
+
span: createSpan(start, end),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
validateReservedDatatypeAdornments(name, genericArgs, radixBase, separators) {
|
|
340
|
+
if (!RESERVED_V1_DATATYPES.has(name))
|
|
341
|
+
return;
|
|
342
|
+
if (genericArgs.length > 0 && !GENERIC_V1_DATATYPES.has(name)) {
|
|
343
|
+
throw new SyntaxError(`Datatype '${name}' does not support generic arguments in v1`, this.previous().span, null, name);
|
|
344
|
+
}
|
|
345
|
+
if ((radixBase !== null || separators.length > 0) && !BRACKETED_V1_DATATYPES.has(name)) {
|
|
346
|
+
throw new SyntaxError(`Datatype '${name}' does not support bracket specifiers in v1`, this.previous().span, null, name);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
parseGenericArgument(genericDepth) {
|
|
350
|
+
const token = this.peek();
|
|
351
|
+
if (token.type !== TokenType.Identifier && token.type !== TokenType.Number) {
|
|
352
|
+
throw new SyntaxError('Expected generic argument', token.span, 'generic argument', token.value);
|
|
353
|
+
}
|
|
354
|
+
if (token.type === TokenType.Number) {
|
|
355
|
+
this.advance();
|
|
356
|
+
return token.value;
|
|
357
|
+
}
|
|
358
|
+
const type = this.parseTypeAnnotation(genericDepth + 1);
|
|
359
|
+
return this.formatTypeAnnotation(type);
|
|
360
|
+
}
|
|
361
|
+
formatTypeAnnotation(type) {
|
|
362
|
+
const generics = type.genericArgs.length > 0 ? `<${type.genericArgs.join(', ')}>` : '';
|
|
363
|
+
const radixBase = type.radixBase != null ? `[${type.radixBase}]` : '';
|
|
364
|
+
const separators = type.separators.map((separator) => `[${separator}]`).join('');
|
|
365
|
+
return `${type.name}${generics}${radixBase}${separators}`;
|
|
366
|
+
}
|
|
367
|
+
parseRadixBaseSpecifier() {
|
|
368
|
+
if (this.check(TokenType.RightBracket)) {
|
|
369
|
+
throw new SyntaxError('Radix base must be an integer from 2 to 64', this.peek().span, 'integer from 2 to 64', this.peek().value);
|
|
370
|
+
}
|
|
371
|
+
const token = this.consume(TokenType.Number, 'Expected radix base');
|
|
372
|
+
const raw = token.value.replace(/_/g, '');
|
|
373
|
+
if (!/^(0|[1-9]\d*)$/.test(raw) || raw !== token.value) {
|
|
374
|
+
throw new SyntaxError('Radix base must be a base-10 integer without leading zeroes', token.span, 'integer from 2 to 64', token.value);
|
|
375
|
+
}
|
|
376
|
+
const base = Number(raw);
|
|
377
|
+
if (!Number.isInteger(base) || base < 2 || base > 64) {
|
|
378
|
+
throw new SyntaxError('Radix base must be an integer from 2 to 64', token.span, 'integer from 2 to 64', token.value);
|
|
379
|
+
}
|
|
380
|
+
return base;
|
|
381
|
+
}
|
|
382
|
+
// ============================================
|
|
383
|
+
// Value parsing
|
|
384
|
+
// ============================================
|
|
385
|
+
parseValue() {
|
|
386
|
+
const countsTowardNesting = this.check(TokenType.LeftAngle)
|
|
387
|
+
|| this.check(TokenType.LeftBrace)
|
|
388
|
+
|| this.check(TokenType.LeftBracket)
|
|
389
|
+
|| this.check(TokenType.LeftParen);
|
|
390
|
+
if (countsTowardNesting) {
|
|
391
|
+
this.currentNestingDepth++;
|
|
392
|
+
const projectedDepth = this.projectedOpeningContainerDepth();
|
|
393
|
+
if (projectedDepth !== null) {
|
|
394
|
+
this.currentNestingDepth--;
|
|
395
|
+
throw new NestingDepthExceededError(projectedDepth, this.maxNestingDepth, this.peek().span);
|
|
396
|
+
}
|
|
397
|
+
if (this.currentNestingDepth > this.maxNestingDepth) {
|
|
398
|
+
const observedDepth = this.currentNestingDepth;
|
|
399
|
+
this.currentNestingDepth--;
|
|
400
|
+
throw new NestingDepthExceededError(observedDepth, this.maxNestingDepth, this.peek().span);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
return this.doParseValue();
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
if (countsTowardNesting) {
|
|
408
|
+
this.currentNestingDepth--;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
parseContainerValue() {
|
|
413
|
+
if (!this.check(TokenType.Colon) && !this.check(TokenType.At)) {
|
|
414
|
+
return this.parseValue();
|
|
415
|
+
}
|
|
416
|
+
const start = this.peek().span.start;
|
|
417
|
+
const attributes = [];
|
|
418
|
+
if (this.check(TokenType.At)) {
|
|
419
|
+
attributes.push(this.parseAttribute(1));
|
|
420
|
+
if (this.check(TokenType.At)) {
|
|
421
|
+
throw new SyntaxError('Only one attribute block is allowed before an anonymous value datatype', this.peek().span, ': or =', this.peek().value);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let datatype = null;
|
|
425
|
+
if (this.check(TokenType.Colon)) {
|
|
426
|
+
this.advance(); // consume :
|
|
427
|
+
datatype = this.parseTypeAnnotation();
|
|
428
|
+
}
|
|
429
|
+
this.consume(TokenType.Equals, "Expected '=' after anonymous value head");
|
|
430
|
+
const value = this.parseValue();
|
|
431
|
+
return {
|
|
432
|
+
type: 'TypedValue',
|
|
433
|
+
datatype,
|
|
434
|
+
attributes,
|
|
435
|
+
value,
|
|
436
|
+
span: createSpan(start, value.span.end),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
projectedOpeningContainerDepth() {
|
|
440
|
+
let extraDepth = 0;
|
|
441
|
+
for (let index = this.current; index < this.tokens.length; index++) {
|
|
442
|
+
switch (this.tokens[index]?.type) {
|
|
443
|
+
case TokenType.LeftBracket:
|
|
444
|
+
case TokenType.LeftParen:
|
|
445
|
+
case TokenType.LeftBrace:
|
|
446
|
+
case TokenType.LeftAngle:
|
|
447
|
+
extraDepth++;
|
|
448
|
+
break;
|
|
449
|
+
default:
|
|
450
|
+
return this.toProjectedOpeningContainerDepth(extraDepth) > this.maxNestingDepth
|
|
451
|
+
? this.toProjectedOpeningContainerDepth(extraDepth)
|
|
452
|
+
: null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const projectedDepth = this.toProjectedOpeningContainerDepth(extraDepth);
|
|
456
|
+
return projectedDepth > this.maxNestingDepth
|
|
457
|
+
? projectedDepth
|
|
458
|
+
: null;
|
|
459
|
+
}
|
|
460
|
+
toProjectedOpeningContainerDepth(extraDepth) {
|
|
461
|
+
return this.currentNestingDepth + Math.max(extraDepth - 1, 0);
|
|
462
|
+
}
|
|
463
|
+
doParseValue() {
|
|
464
|
+
// Node introducer syntax
|
|
465
|
+
if (this.check(TokenType.LeftAngle)) {
|
|
466
|
+
return this.parseNode();
|
|
467
|
+
}
|
|
468
|
+
// Node values must begin with the '<' introducer.
|
|
469
|
+
if (this.check(TokenType.Identifier) && this.peekNext()?.type === TokenType.LeftAngle) {
|
|
470
|
+
throw new SyntaxError("Node values must use the '<tag>' or '<tag(...)>' forms", this.peek().span, '<tag>', this.peek().value);
|
|
471
|
+
}
|
|
472
|
+
// Object
|
|
473
|
+
if (this.check(TokenType.LeftBrace)) {
|
|
474
|
+
return this.parseObject();
|
|
475
|
+
}
|
|
476
|
+
// List
|
|
477
|
+
if (this.check(TokenType.LeftBracket)) {
|
|
478
|
+
return this.parseList();
|
|
479
|
+
}
|
|
480
|
+
// Tuple
|
|
481
|
+
if (this.check(TokenType.LeftParen)) {
|
|
482
|
+
return this.parseTuple();
|
|
483
|
+
}
|
|
484
|
+
// Clone reference
|
|
485
|
+
if (this.check(TokenType.Tilde)) {
|
|
486
|
+
return this.parseCloneReference();
|
|
487
|
+
}
|
|
488
|
+
// Pointer reference
|
|
489
|
+
if (this.check(TokenType.TildeArrow)) {
|
|
490
|
+
return this.parsePointerReference();
|
|
491
|
+
}
|
|
492
|
+
// Literals
|
|
493
|
+
return this.parseLiteral();
|
|
494
|
+
}
|
|
495
|
+
parseNode() {
|
|
496
|
+
const start = this.peek().span.start;
|
|
497
|
+
this.consume(TokenType.LeftAngle, "Expected '<' to start node literal");
|
|
498
|
+
const tag = this.parseNodeTag();
|
|
499
|
+
const attributes = [];
|
|
500
|
+
if (this.check(TokenType.At)) {
|
|
501
|
+
attributes.push(this.parseAttribute(1));
|
|
502
|
+
if (this.check(TokenType.At)) {
|
|
503
|
+
throw new SyntaxError('Only one attribute block is allowed before a node datatype', this.peek().span, ':, (, or >', this.peek().value);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
let datatype = null;
|
|
507
|
+
if (this.check(TokenType.Colon)) {
|
|
508
|
+
this.advance(); // consume :
|
|
509
|
+
datatype = this.parseTypeAnnotation();
|
|
510
|
+
if (datatype.genericArgs.length > 0 || datatype.radixBase !== null || datatype.separators.length > 0) {
|
|
511
|
+
throw new SyntaxError('Node head datatypes must be simple labels without generics or separator specs', datatype.span, 'simple node head datatype', this.formatTypeAnnotation(datatype));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const children = [];
|
|
515
|
+
if (this.check(TokenType.RightAngle)) {
|
|
516
|
+
this.advance();
|
|
517
|
+
const end = this.previous().span.end;
|
|
518
|
+
return {
|
|
519
|
+
type: 'NodeLiteral',
|
|
520
|
+
tag,
|
|
521
|
+
attributes,
|
|
522
|
+
datatype,
|
|
523
|
+
children,
|
|
524
|
+
span: createSpan(start, end),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
this.consume(TokenType.LeftParen, "Expected '(' or '>' after node tag");
|
|
528
|
+
while (!this.check(TokenType.RightParen) && !this.isAtEnd()) {
|
|
529
|
+
children.push(this.parseContainerValue());
|
|
530
|
+
if (!this.check(TokenType.RightParen)) {
|
|
531
|
+
this.consumeSeparatorOrLineBreak(TokenType.RightParen, 'Expected \',\' or newline between node children');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
this.consume(TokenType.RightParen, "Expected ')' to close node children");
|
|
535
|
+
this.consume(TokenType.RightAngle, "Expected '>' after node children");
|
|
536
|
+
const end = this.previous().span.end;
|
|
537
|
+
return {
|
|
538
|
+
type: 'NodeLiteral',
|
|
539
|
+
tag,
|
|
540
|
+
attributes,
|
|
541
|
+
datatype,
|
|
542
|
+
children,
|
|
543
|
+
span: createSpan(start, end),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
parseNodeTag() {
|
|
547
|
+
const token = this.consumeOneOf([TokenType.Identifier, TokenType.String], "Expected node tag after '<'");
|
|
548
|
+
if (token.type === TokenType.String) {
|
|
549
|
+
if (token.quote === '`') {
|
|
550
|
+
throw new SyntaxError('Backtick-quoted node tags are not supported', token.span, 'single or double quoted node tag', token.value);
|
|
551
|
+
}
|
|
552
|
+
if (token.value.length === 0) {
|
|
553
|
+
throw new SyntaxError('Quoted node tags must not be empty', token.span, 'quoted node tag', token.value);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return token.value;
|
|
557
|
+
}
|
|
558
|
+
parseObject() {
|
|
559
|
+
const start = this.peek().span.start;
|
|
560
|
+
this.advance(); // consume {
|
|
561
|
+
const bindings = [];
|
|
562
|
+
const keys = new Set();
|
|
563
|
+
const attributes = [];
|
|
564
|
+
while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
|
|
565
|
+
if (this.check(TokenType.At)) {
|
|
566
|
+
throw new SyntaxError('Object attributes must be attached to the object binding or an object member binding', this.peek().span, 'object member key', this.peek().value);
|
|
567
|
+
}
|
|
568
|
+
if (this.check(TokenType.RightBrace))
|
|
569
|
+
break;
|
|
570
|
+
const binding = this.parseBinding();
|
|
571
|
+
if (binding) {
|
|
572
|
+
// Check for duplicate key
|
|
573
|
+
if (keys.has(binding.key)) {
|
|
574
|
+
this.errors.push(new DuplicateKeyError(binding.key, binding.span));
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
keys.add(binding.key);
|
|
578
|
+
}
|
|
579
|
+
bindings.push(binding);
|
|
580
|
+
}
|
|
581
|
+
if (!this.check(TokenType.RightBrace)) {
|
|
582
|
+
this.consumeSeparatorOrLineBreak(TokenType.RightBrace, 'Expected \',\' or newline between object bindings');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!this.check(TokenType.RightBrace)) {
|
|
586
|
+
throw new SyntaxError("Expected '}' to close object", this.peek().span, '}', this.peek().value);
|
|
587
|
+
}
|
|
588
|
+
this.advance(); // consume }
|
|
589
|
+
const end = this.previous().span.end;
|
|
590
|
+
return {
|
|
591
|
+
type: 'ObjectNode',
|
|
592
|
+
bindings,
|
|
593
|
+
attributes,
|
|
594
|
+
span: createSpan(start, end),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
parseList() {
|
|
598
|
+
const start = this.peek().span.start;
|
|
599
|
+
this.advance(); // consume [
|
|
600
|
+
const elements = [];
|
|
601
|
+
const attributes = [];
|
|
602
|
+
while (!this.check(TokenType.RightBracket) && !this.isAtEnd()) {
|
|
603
|
+
const element = this.parseContainerValue();
|
|
604
|
+
elements.push(element);
|
|
605
|
+
if (!this.check(TokenType.RightBracket)) {
|
|
606
|
+
this.consumeSeparatorOrLineBreak(TokenType.RightBracket, 'Expected \',\' or newline between list elements');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!this.check(TokenType.RightBracket)) {
|
|
610
|
+
throw new SyntaxError("Expected ']' to close list", this.peek().span, ']', this.peek().value);
|
|
611
|
+
}
|
|
612
|
+
this.advance(); // consume ]
|
|
613
|
+
const end = this.previous().span.end;
|
|
614
|
+
return {
|
|
615
|
+
type: 'ListNode',
|
|
616
|
+
elements,
|
|
617
|
+
attributes,
|
|
618
|
+
span: createSpan(start, end),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
parseTuple() {
|
|
622
|
+
const start = this.peek().span.start;
|
|
623
|
+
this.advance(); // consume (
|
|
624
|
+
const elements = [];
|
|
625
|
+
const attributes = [];
|
|
626
|
+
while (!this.check(TokenType.RightParen) && !this.isAtEnd()) {
|
|
627
|
+
const element = this.parseContainerValue();
|
|
628
|
+
elements.push(element);
|
|
629
|
+
if (this.check(TokenType.Comma)) {
|
|
630
|
+
this.advance();
|
|
631
|
+
while (this.check(TokenType.Newline)) {
|
|
632
|
+
this.advance();
|
|
633
|
+
}
|
|
634
|
+
if (this.check(TokenType.RightParen)) {
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
if (this.check(TokenType.Comma)) {
|
|
638
|
+
throw new SyntaxError("Expected ',' or newline between tuple elements", this.peek().span, "',' or newline", this.peek().value);
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!this.check(TokenType.RightParen)) {
|
|
643
|
+
this.consumeSeparatorOrLineBreak(TokenType.RightParen, 'Expected \',\' or newline between tuple elements');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (!this.check(TokenType.RightParen)) {
|
|
647
|
+
throw new SyntaxError("Expected ')' to close tuple", this.peek().span, ')', this.peek().value);
|
|
648
|
+
}
|
|
649
|
+
this.advance(); // consume )
|
|
650
|
+
const end = this.previous().span.end;
|
|
651
|
+
return {
|
|
652
|
+
type: 'TupleLiteral',
|
|
653
|
+
elements,
|
|
654
|
+
attributes,
|
|
655
|
+
raw: '',
|
|
656
|
+
span: createSpan(start, end),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
parseCloneReference() {
|
|
660
|
+
const start = this.peek().span.start;
|
|
661
|
+
this.advance(); // consume ~
|
|
662
|
+
const path = this.parsePath();
|
|
663
|
+
const end = this.previous().span.end;
|
|
664
|
+
return {
|
|
665
|
+
type: 'CloneReference',
|
|
666
|
+
path,
|
|
667
|
+
span: createSpan(start, end),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
parsePointerReference() {
|
|
671
|
+
const start = this.peek().span.start;
|
|
672
|
+
this.advance(); // consume ~>
|
|
673
|
+
const path = this.parsePath();
|
|
674
|
+
const end = this.previous().span.end;
|
|
675
|
+
return {
|
|
676
|
+
type: 'PointerReference',
|
|
677
|
+
path,
|
|
678
|
+
span: createSpan(start, end),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
parsePath() {
|
|
682
|
+
const path = [];
|
|
683
|
+
let sawRootDot = false;
|
|
684
|
+
let sawExplicitRoot = false;
|
|
685
|
+
if (this.check(TokenType.Dollar)) {
|
|
686
|
+
this.advance(); // consume $
|
|
687
|
+
sawExplicitRoot = true;
|
|
688
|
+
if (this.check(TokenType.Dot)) {
|
|
689
|
+
this.advance(); // consume explicit dot after $
|
|
690
|
+
sawRootDot = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
this.parsePathInitialSegment(path, sawRootDot, sawExplicitRoot);
|
|
694
|
+
while (this.check(TokenType.Dot) || this.check(TokenType.LeftBracket) || this.check(TokenType.At)) {
|
|
695
|
+
if (this.check(TokenType.Dot)) {
|
|
696
|
+
this.advance(); // consume .
|
|
697
|
+
if (this.check(TokenType.LeftBracket)) {
|
|
698
|
+
path.push(this.parseQuotedBracketMemberSegment());
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
path.push(this.parseMemberSegment("Expected member path segment after '.'"));
|
|
702
|
+
}
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (this.check(TokenType.At)) {
|
|
706
|
+
this.advance(); // consume @
|
|
707
|
+
path.push(this.parseAttributePathSegment());
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
path.push(this.parseBracketPathSegment());
|
|
711
|
+
}
|
|
712
|
+
return path;
|
|
713
|
+
}
|
|
714
|
+
parseLiteral() {
|
|
715
|
+
const token = this.peek();
|
|
716
|
+
switch (token.type) {
|
|
717
|
+
case TokenType.RightAngle:
|
|
718
|
+
return this.parseTrimtickString();
|
|
719
|
+
case TokenType.String:
|
|
720
|
+
this.advance();
|
|
721
|
+
return this.createStringLiteral(token);
|
|
722
|
+
case TokenType.Number:
|
|
723
|
+
this.advance();
|
|
724
|
+
return this.createNumberLiteral(token);
|
|
725
|
+
case TokenType.Identifier:
|
|
726
|
+
if (token.value === 'Infinity') {
|
|
727
|
+
this.advance();
|
|
728
|
+
return this.createInfinityLiteral(token.value);
|
|
729
|
+
}
|
|
730
|
+
if (token.value === 'NaN') {
|
|
731
|
+
this.advance();
|
|
732
|
+
return this.createNaNLiteral(token.value);
|
|
733
|
+
}
|
|
734
|
+
throw new SyntaxError(`Unexpected token '${token.value}'`, token.span, 'value', token.value);
|
|
735
|
+
case TokenType.Symbol:
|
|
736
|
+
if (token.value === '-' && this.peekNext()?.type === TokenType.Identifier && this.peekNext()?.value === 'Infinity') {
|
|
737
|
+
const minus = this.advance();
|
|
738
|
+
const infinity = this.advance();
|
|
739
|
+
return this.createInfinityLiteral('-Infinity', createSpan(minus.span.start, infinity.span.end));
|
|
740
|
+
}
|
|
741
|
+
if (token.value === '-' && this.peekNext()?.type === TokenType.Identifier && this.peekNext()?.value === 'NaN') {
|
|
742
|
+
const minus = this.advance();
|
|
743
|
+
const nan = this.advance();
|
|
744
|
+
return this.createNaNLiteral('-NaN', createSpan(minus.span.start, nan.span.end));
|
|
745
|
+
}
|
|
746
|
+
if (token.value === '!') {
|
|
747
|
+
return this.parseNullLiteral();
|
|
748
|
+
}
|
|
749
|
+
throw new SyntaxError(`Unexpected token '${token.value}'`, token.span, 'value', token.value);
|
|
750
|
+
case TokenType.True:
|
|
751
|
+
case TokenType.False:
|
|
752
|
+
this.advance();
|
|
753
|
+
return this.createBooleanLiteral(token);
|
|
754
|
+
case TokenType.Yes:
|
|
755
|
+
case TokenType.No:
|
|
756
|
+
case TokenType.On:
|
|
757
|
+
case TokenType.Off:
|
|
758
|
+
this.advance();
|
|
759
|
+
return this.createToggleLiteral(token);
|
|
760
|
+
case TokenType.HexLiteral:
|
|
761
|
+
this.advance();
|
|
762
|
+
return this.createHexLiteral(token);
|
|
763
|
+
case TokenType.Date:
|
|
764
|
+
this.advance();
|
|
765
|
+
return this.createDateLiteral(token);
|
|
766
|
+
case TokenType.DateTime:
|
|
767
|
+
this.advance();
|
|
768
|
+
return this.createDateTimeLiteral(token);
|
|
769
|
+
case TokenType.Time:
|
|
770
|
+
this.advance();
|
|
771
|
+
return this.createTimeLiteral(token);
|
|
772
|
+
case TokenType.SeparatorLiteral:
|
|
773
|
+
this.advance();
|
|
774
|
+
return this.createSeparatorLiteral(token);
|
|
775
|
+
case TokenType.Caret:
|
|
776
|
+
throw new SyntaxError('Separator literals must contain a payload', token.span, 'separator literal payload', token.value);
|
|
777
|
+
case TokenType.RadixLiteral:
|
|
778
|
+
this.advance();
|
|
779
|
+
return {
|
|
780
|
+
type: 'RadixLiteral',
|
|
781
|
+
value: token.value.substring(1), // remove %
|
|
782
|
+
raw: token.value,
|
|
783
|
+
span: token.span,
|
|
784
|
+
};
|
|
785
|
+
case TokenType.EncodingLiteral:
|
|
786
|
+
this.advance();
|
|
787
|
+
return {
|
|
788
|
+
type: 'EncodingLiteral',
|
|
789
|
+
value: token.value.substring(1), // remove $
|
|
790
|
+
raw: token.value,
|
|
791
|
+
span: token.span,
|
|
792
|
+
};
|
|
793
|
+
default:
|
|
794
|
+
throw new SyntaxError(`Unexpected token '${token.value}'`, token.span, 'value', token.value);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
createStringLiteral(token) {
|
|
798
|
+
return {
|
|
799
|
+
type: 'StringLiteral',
|
|
800
|
+
value: token.value,
|
|
801
|
+
raw: token.value, // Could store original with quotes if needed
|
|
802
|
+
delimiter: token.quote ?? '"',
|
|
803
|
+
span: token.span,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
parseTrimtickString() {
|
|
807
|
+
const startToken = this.peek();
|
|
808
|
+
let markerWidth = 0;
|
|
809
|
+
let previousAngle = null;
|
|
810
|
+
while (this.check(TokenType.RightAngle)) {
|
|
811
|
+
const angle = this.peek();
|
|
812
|
+
if (previousAngle && previousAngle.span.end.offset !== angle.span.start.offset) {
|
|
813
|
+
throw new SyntaxError('Trimtick marker must be contiguous', angle.span, 'trimticks', angle.value);
|
|
814
|
+
}
|
|
815
|
+
markerWidth += 1;
|
|
816
|
+
if (markerWidth > 4) {
|
|
817
|
+
throw new SyntaxError('Trimtick marker may contain at most four ">" characters', angle.span, 'trimticks', angle.value);
|
|
818
|
+
}
|
|
819
|
+
previousAngle = this.advance();
|
|
820
|
+
}
|
|
821
|
+
if (!this.check(TokenType.String) || this.peek().quote !== '`') {
|
|
822
|
+
throw new SyntaxError('Trimtick marker must be followed by a backtick string', this.peek().span, 'trimticks', this.peek().value);
|
|
823
|
+
}
|
|
824
|
+
const token = this.advance();
|
|
825
|
+
const rawValue = token.value;
|
|
826
|
+
return {
|
|
827
|
+
type: 'StringLiteral',
|
|
828
|
+
value: applyTrimticks(rawValue, markerWidth),
|
|
829
|
+
raw: rawValue,
|
|
830
|
+
delimiter: '`',
|
|
831
|
+
trimticks: {
|
|
832
|
+
markerWidth: markerWidth,
|
|
833
|
+
rawValue,
|
|
834
|
+
},
|
|
835
|
+
span: createSpan(startToken.span.start, token.span.end),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
createNumberLiteral(token) {
|
|
839
|
+
return {
|
|
840
|
+
type: 'NumberLiteral',
|
|
841
|
+
value: token.value.replace(/_/g, ''),
|
|
842
|
+
raw: token.value,
|
|
843
|
+
span: token.span,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
createInfinityLiteral(raw, span) {
|
|
847
|
+
return {
|
|
848
|
+
type: 'InfinityLiteral',
|
|
849
|
+
value: raw,
|
|
850
|
+
raw,
|
|
851
|
+
span: span ?? this.previous().span,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
createNaNLiteral(raw, span) {
|
|
855
|
+
return {
|
|
856
|
+
type: 'NaNLiteral',
|
|
857
|
+
value: raw,
|
|
858
|
+
raw,
|
|
859
|
+
span: span ?? this.previous().span,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
parseNullLiteral() {
|
|
863
|
+
const bang = this.advance();
|
|
864
|
+
const next = this.peek();
|
|
865
|
+
if (next.type === TokenType.Identifier) {
|
|
866
|
+
if (!RESERVED_NULL_SENTINELS.has(next.value)) {
|
|
867
|
+
throw new ParserError(`Invalid null sentinel '${next.value}'`, createSpan(bang.span.start, next.span.end), 'INVALID_NULL_SENTINEL');
|
|
868
|
+
}
|
|
869
|
+
const ident = this.advance();
|
|
870
|
+
return {
|
|
871
|
+
type: 'NullLiteral',
|
|
872
|
+
mode: 'reserved',
|
|
873
|
+
value: ident.value,
|
|
874
|
+
raw: `!${ident.value}`,
|
|
875
|
+
span: createSpan(bang.span.start, ident.span.end),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
if (next.type === TokenType.String) {
|
|
879
|
+
const string = this.advance();
|
|
880
|
+
const span = createSpan(bang.span.start, string.span.end);
|
|
881
|
+
if (string.value.length === 0) {
|
|
882
|
+
throw new ParserError('Null reason must not be empty', span, 'INVALID_NULL_REASON_EMPTY');
|
|
883
|
+
}
|
|
884
|
+
if (isAsciiWhitespaceOnly(string.value)) {
|
|
885
|
+
throw new ParserError('Null reason must not be ASCII-whitespace-only', span, 'INVALID_NULL_REASON_WHITESPACE');
|
|
886
|
+
}
|
|
887
|
+
if (RESERVED_NULL_SENTINELS.has(string.value)) {
|
|
888
|
+
throw new ParserError(`Null reason collides with reserved sentinel '${string.value}'`, span, 'INVALID_NULL_REASON_COLLISION');
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
type: 'NullLiteral',
|
|
892
|
+
mode: 'reason',
|
|
893
|
+
value: string.value,
|
|
894
|
+
raw: `!${JSON.stringify(string.value)}`,
|
|
895
|
+
span,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
throw new ParserError('Null literal must be followed by a reserved sentinel or quoted reason', bang.span, 'INVALID_NULL_LITERAL');
|
|
899
|
+
}
|
|
900
|
+
createBooleanLiteral(token) {
|
|
901
|
+
return {
|
|
902
|
+
type: 'BooleanLiteral',
|
|
903
|
+
value: token.value.toLowerCase() === 'true',
|
|
904
|
+
raw: token.value,
|
|
905
|
+
span: token.span,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
createToggleLiteral(token) {
|
|
909
|
+
const normalized = token.value.toLowerCase();
|
|
910
|
+
if (normalized === 'yes' || normalized === 'no' || normalized === 'on' || normalized === 'off') {
|
|
911
|
+
return {
|
|
912
|
+
type: 'ToggleLiteral',
|
|
913
|
+
value: normalized,
|
|
914
|
+
raw: token.value,
|
|
915
|
+
span: token.span,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
throw new SyntaxError(`Unexpected toggle literal '${token.value}'`, token.span, 'toggle literal', token.value);
|
|
919
|
+
}
|
|
920
|
+
createHexLiteral(token) {
|
|
921
|
+
return {
|
|
922
|
+
type: 'HexLiteral',
|
|
923
|
+
value: token.value.substring(1), // remove #
|
|
924
|
+
raw: token.value,
|
|
925
|
+
span: token.span,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
createDateLiteral(token) {
|
|
929
|
+
return {
|
|
930
|
+
type: 'DateLiteral',
|
|
931
|
+
value: token.value,
|
|
932
|
+
raw: token.value,
|
|
933
|
+
span: token.span,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
createDateTimeLiteral(token) {
|
|
937
|
+
return {
|
|
938
|
+
type: 'DateTimeLiteral',
|
|
939
|
+
value: token.value,
|
|
940
|
+
raw: token.value,
|
|
941
|
+
span: token.span,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
createTimeLiteral(token) {
|
|
945
|
+
return {
|
|
946
|
+
type: 'TimeLiteral',
|
|
947
|
+
value: token.value,
|
|
948
|
+
raw: token.value,
|
|
949
|
+
span: token.span,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
createSeparatorLiteral(token) {
|
|
953
|
+
return {
|
|
954
|
+
type: 'SeparatorLiteral',
|
|
955
|
+
value: token.value.substring(1), // remove ^
|
|
956
|
+
raw: token.value,
|
|
957
|
+
span: token.span,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
// ============================================
|
|
961
|
+
// Utility methods
|
|
962
|
+
// ============================================
|
|
963
|
+
isAtEnd() {
|
|
964
|
+
return this.peek().type === TokenType.EOF;
|
|
965
|
+
}
|
|
966
|
+
peek() {
|
|
967
|
+
return this.tokens[this.current];
|
|
968
|
+
}
|
|
969
|
+
peekNext() {
|
|
970
|
+
if (this.current + 1 >= this.tokens.length)
|
|
971
|
+
return undefined;
|
|
972
|
+
return this.tokens[this.current + 1];
|
|
973
|
+
}
|
|
974
|
+
previous() {
|
|
975
|
+
return this.tokens[this.current - 1] ?? this.tokens[0];
|
|
976
|
+
}
|
|
977
|
+
advance() {
|
|
978
|
+
if (!this.isAtEnd())
|
|
979
|
+
this.current++;
|
|
980
|
+
return this.previous();
|
|
981
|
+
}
|
|
982
|
+
check(type) {
|
|
983
|
+
if (this.isAtEnd())
|
|
984
|
+
return false;
|
|
985
|
+
return this.peek().type === type;
|
|
986
|
+
}
|
|
987
|
+
consume(type, message) {
|
|
988
|
+
if (this.check(type))
|
|
989
|
+
return this.advance();
|
|
990
|
+
throw new SyntaxError(message, this.peek().span, type, this.peek().value);
|
|
991
|
+
}
|
|
992
|
+
consumeOneOf(types, message) {
|
|
993
|
+
for (const type of types) {
|
|
994
|
+
if (this.check(type)) {
|
|
995
|
+
return this.advance();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
throw new SyntaxError(message, this.peek().span, types.join(' | '), this.peek().value);
|
|
999
|
+
}
|
|
1000
|
+
keyFromToken(token) {
|
|
1001
|
+
if (token.type === TokenType.String && token.quote === '`') {
|
|
1002
|
+
throw new SyntaxError('Backtick-quoted keys are not supported', token.span, 'single or double quoted key', token.value);
|
|
1003
|
+
}
|
|
1004
|
+
return this.assertNonEmptyKey(token.value, token.span, 'Keys must not be empty');
|
|
1005
|
+
}
|
|
1006
|
+
assertNonEmptyKey(key, span, message) {
|
|
1007
|
+
if (key.length === 0) {
|
|
1008
|
+
throw new SyntaxError(message, span, 'non-empty key', key);
|
|
1009
|
+
}
|
|
1010
|
+
return key;
|
|
1011
|
+
}
|
|
1012
|
+
parsePathInitialSegment(path, sawRootDot = false, sawExplicitRoot = false) {
|
|
1013
|
+
if (this.check(TokenType.Identifier) || this.check(TokenType.String)) {
|
|
1014
|
+
path.push(this.parseMemberSegment('Expected path segment'));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (this.check(TokenType.LeftBracket)) {
|
|
1018
|
+
if (sawExplicitRoot && !sawRootDot && this.peekNext()?.type === TokenType.String) {
|
|
1019
|
+
throw new SyntaxError("Expected '.' after '$' before quoted root-member segment", this.peek().span, 'reference path', this.peek().value);
|
|
1020
|
+
}
|
|
1021
|
+
path.push(this.parseBracketPathSegment());
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
throw new SyntaxError("Expected path segment", this.peek().span, 'identifier, string key, or bracket segment', this.peek().value);
|
|
1025
|
+
}
|
|
1026
|
+
parseMemberSegment(message) {
|
|
1027
|
+
const token = this.consumeOneOf([TokenType.Identifier, TokenType.String], message);
|
|
1028
|
+
if (token.type === TokenType.String && token.quote === '`') {
|
|
1029
|
+
throw new SyntaxError('Backtick-quoted keys are not supported in paths', token.span, 'single or double quoted key', token.value);
|
|
1030
|
+
}
|
|
1031
|
+
return this.assertNonEmptyKey(token.value, token.span, 'Quoted path keys must not be empty');
|
|
1032
|
+
}
|
|
1033
|
+
parseAttributePathSegment() {
|
|
1034
|
+
if (this.check(TokenType.LeftBracket)) {
|
|
1035
|
+
this.advance(); // consume [
|
|
1036
|
+
const keyToken = this.consume(TokenType.String, "Expected quoted attribute key after '@['");
|
|
1037
|
+
if (keyToken.quote === '`') {
|
|
1038
|
+
throw new SyntaxError('Backtick-quoted keys are not supported in attribute segments', keyToken.span, 'single or double quoted key', keyToken.value);
|
|
1039
|
+
}
|
|
1040
|
+
this.consume(TokenType.RightBracket, "Expected ']' after quoted attribute key");
|
|
1041
|
+
return { type: 'attr', key: this.assertNonEmptyKey(keyToken.value, keyToken.span, 'Quoted attribute keys must not be empty') };
|
|
1042
|
+
}
|
|
1043
|
+
const keyToken = this.consumeOneOf([TokenType.Identifier, TokenType.String], "Expected attribute path segment");
|
|
1044
|
+
if (keyToken.type === TokenType.String && keyToken.quote === '`') {
|
|
1045
|
+
throw new SyntaxError('Backtick-quoted keys are not supported in attribute segments', keyToken.span, 'single or double quoted key', keyToken.value);
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
type: 'attr',
|
|
1049
|
+
key: this.assertNonEmptyKey(keyToken.value, keyToken.span, 'Quoted attribute keys must not be empty'),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
parseBracketPathSegment() {
|
|
1053
|
+
this.advance(); // consume [
|
|
1054
|
+
if (this.check(TokenType.String)) {
|
|
1055
|
+
const keyToken = this.advance();
|
|
1056
|
+
if (keyToken.quote === '`') {
|
|
1057
|
+
throw new SyntaxError('Backtick-quoted keys are not supported in paths', keyToken.span, 'single or double quoted key', keyToken.value);
|
|
1058
|
+
}
|
|
1059
|
+
this.consume(TokenType.RightBracket, "Expected ']' after quoted path segment");
|
|
1060
|
+
return this.assertNonEmptyKey(keyToken.value, keyToken.span, 'Quoted path keys must not be empty');
|
|
1061
|
+
}
|
|
1062
|
+
const indexToken = this.consume(TokenType.Number, "Expected numeric index or quoted key segment");
|
|
1063
|
+
this.consume(TokenType.RightBracket, "Expected ']' after index segment");
|
|
1064
|
+
const numericText = indexToken.value.replace(/_/g, '');
|
|
1065
|
+
const parsedIndex = Number.parseInt(numericText, 10);
|
|
1066
|
+
if (!Number.isInteger(parsedIndex) || parsedIndex < 0) {
|
|
1067
|
+
throw new SyntaxError(`Invalid index segment '${indexToken.value}'`, indexToken.span, 'non-negative integer', indexToken.value);
|
|
1068
|
+
}
|
|
1069
|
+
return parsedIndex;
|
|
1070
|
+
}
|
|
1071
|
+
parseQuotedBracketMemberSegment() {
|
|
1072
|
+
this.consume(TokenType.LeftBracket, "Expected '[' after '.'");
|
|
1073
|
+
const keyToken = this.consume(TokenType.String, "Expected quoted member path segment after '.['");
|
|
1074
|
+
if (keyToken.quote === '`') {
|
|
1075
|
+
throw new SyntaxError('Backtick-quoted keys are not supported in paths', keyToken.span, 'single or double quoted key', keyToken.value);
|
|
1076
|
+
}
|
|
1077
|
+
this.consume(TokenType.RightBracket, "Expected ']' after quoted member path segment");
|
|
1078
|
+
return this.assertNonEmptyKey(keyToken.value, keyToken.span, 'Quoted path keys must not be empty');
|
|
1079
|
+
}
|
|
1080
|
+
parseSeparatorCharacter() {
|
|
1081
|
+
const token = this.peek();
|
|
1082
|
+
if (token.type === TokenType.Identifier
|
|
1083
|
+
|| token.type === TokenType.Number
|
|
1084
|
+
|| token.type === TokenType.String
|
|
1085
|
+
|| token.type === TokenType.Symbol) {
|
|
1086
|
+
this.advance();
|
|
1087
|
+
if (token.value.length !== 1) {
|
|
1088
|
+
throw new InvalidSeparatorCharError(token.value, token.span);
|
|
1089
|
+
}
|
|
1090
|
+
const char = token.value;
|
|
1091
|
+
if (!isAllowedSeparatorSpecChar(char)) {
|
|
1092
|
+
throw new InvalidSeparatorCharError(char, token.span);
|
|
1093
|
+
}
|
|
1094
|
+
return char;
|
|
1095
|
+
}
|
|
1096
|
+
let char;
|
|
1097
|
+
switch (token.type) {
|
|
1098
|
+
case TokenType.Comma:
|
|
1099
|
+
char = ',';
|
|
1100
|
+
break;
|
|
1101
|
+
case TokenType.Semicolon:
|
|
1102
|
+
char = ';';
|
|
1103
|
+
break;
|
|
1104
|
+
case TokenType.Colon:
|
|
1105
|
+
char = ':';
|
|
1106
|
+
break;
|
|
1107
|
+
case TokenType.Dot:
|
|
1108
|
+
char = '.';
|
|
1109
|
+
break;
|
|
1110
|
+
case TokenType.At:
|
|
1111
|
+
char = '@';
|
|
1112
|
+
break;
|
|
1113
|
+
case TokenType.Hash:
|
|
1114
|
+
char = '#';
|
|
1115
|
+
break;
|
|
1116
|
+
case TokenType.Dollar:
|
|
1117
|
+
char = '$';
|
|
1118
|
+
break;
|
|
1119
|
+
case TokenType.Percent:
|
|
1120
|
+
char = '%';
|
|
1121
|
+
break;
|
|
1122
|
+
case TokenType.Ampersand:
|
|
1123
|
+
char = '&';
|
|
1124
|
+
break;
|
|
1125
|
+
case TokenType.Caret:
|
|
1126
|
+
char = '^';
|
|
1127
|
+
break;
|
|
1128
|
+
case TokenType.Equals:
|
|
1129
|
+
char = '=';
|
|
1130
|
+
break;
|
|
1131
|
+
case TokenType.LeftAngle:
|
|
1132
|
+
char = '<';
|
|
1133
|
+
break;
|
|
1134
|
+
case TokenType.RightAngle:
|
|
1135
|
+
char = '>';
|
|
1136
|
+
break;
|
|
1137
|
+
case TokenType.Tilde:
|
|
1138
|
+
char = '~';
|
|
1139
|
+
break;
|
|
1140
|
+
case TokenType.LeftBracket:
|
|
1141
|
+
char = '[';
|
|
1142
|
+
break;
|
|
1143
|
+
case TokenType.RightBracket:
|
|
1144
|
+
char = ']';
|
|
1145
|
+
break;
|
|
1146
|
+
default:
|
|
1147
|
+
throw new SyntaxError('Expected separator character', token.span, 'single separator character', token.value);
|
|
1148
|
+
}
|
|
1149
|
+
this.advance();
|
|
1150
|
+
if (char.length !== 1) {
|
|
1151
|
+
throw new SyntaxError('Separator datatype bracket specs must contain exactly one character', token.span, 'single separator character', token.value);
|
|
1152
|
+
}
|
|
1153
|
+
if (!isAllowedSeparatorSpecChar(char)) {
|
|
1154
|
+
throw new InvalidSeparatorCharError(char, token.span);
|
|
1155
|
+
}
|
|
1156
|
+
return char;
|
|
1157
|
+
}
|
|
1158
|
+
parseCustomBracketSpecifier() {
|
|
1159
|
+
const token = this.peek();
|
|
1160
|
+
let value;
|
|
1161
|
+
switch (token.type) {
|
|
1162
|
+
case TokenType.Identifier:
|
|
1163
|
+
case TokenType.Number:
|
|
1164
|
+
case TokenType.String:
|
|
1165
|
+
case TokenType.Symbol:
|
|
1166
|
+
value = token.value;
|
|
1167
|
+
break;
|
|
1168
|
+
case TokenType.Comma:
|
|
1169
|
+
value = ',';
|
|
1170
|
+
break;
|
|
1171
|
+
case TokenType.Semicolon:
|
|
1172
|
+
value = ';';
|
|
1173
|
+
break;
|
|
1174
|
+
case TokenType.Colon:
|
|
1175
|
+
value = ':';
|
|
1176
|
+
break;
|
|
1177
|
+
case TokenType.Dot:
|
|
1178
|
+
value = '.';
|
|
1179
|
+
break;
|
|
1180
|
+
case TokenType.At:
|
|
1181
|
+
value = '@';
|
|
1182
|
+
break;
|
|
1183
|
+
case TokenType.Hash:
|
|
1184
|
+
value = '#';
|
|
1185
|
+
break;
|
|
1186
|
+
case TokenType.Dollar:
|
|
1187
|
+
value = '$';
|
|
1188
|
+
break;
|
|
1189
|
+
case TokenType.Percent:
|
|
1190
|
+
value = '%';
|
|
1191
|
+
break;
|
|
1192
|
+
case TokenType.Ampersand:
|
|
1193
|
+
value = '&';
|
|
1194
|
+
break;
|
|
1195
|
+
case TokenType.Caret:
|
|
1196
|
+
value = '^';
|
|
1197
|
+
break;
|
|
1198
|
+
case TokenType.Equals:
|
|
1199
|
+
value = '=';
|
|
1200
|
+
break;
|
|
1201
|
+
case TokenType.LeftAngle:
|
|
1202
|
+
value = '<';
|
|
1203
|
+
break;
|
|
1204
|
+
case TokenType.RightAngle:
|
|
1205
|
+
value = '>';
|
|
1206
|
+
break;
|
|
1207
|
+
case TokenType.Tilde:
|
|
1208
|
+
value = '~';
|
|
1209
|
+
break;
|
|
1210
|
+
case TokenType.LeftBracket:
|
|
1211
|
+
value = '[';
|
|
1212
|
+
break;
|
|
1213
|
+
case TokenType.RightBracket:
|
|
1214
|
+
throw new SyntaxError('Expected separator character', token.span, 'separator or radix bracket spec', token.value);
|
|
1215
|
+
default:
|
|
1216
|
+
throw new SyntaxError('Expected separator character', token.span, 'separator or radix bracket spec', token.value);
|
|
1217
|
+
}
|
|
1218
|
+
this.advance();
|
|
1219
|
+
if (value === '[') {
|
|
1220
|
+
throw new InvalidSeparatorCharError(value, token.span);
|
|
1221
|
+
}
|
|
1222
|
+
return value;
|
|
1223
|
+
}
|
|
1224
|
+
synchronize() {
|
|
1225
|
+
this.advance();
|
|
1226
|
+
while (!this.isAtEnd()) {
|
|
1227
|
+
// If we see what looks like the start of a new binding, stop synchronizing
|
|
1228
|
+
if (this.check(TokenType.Identifier)) {
|
|
1229
|
+
// Peek ahead to see if this is a binding (identifier followed by = or :)
|
|
1230
|
+
const next = this.peekNext();
|
|
1231
|
+
if (next && (next.type === TokenType.Equals || next.type === TokenType.Colon || next.type === TokenType.At)) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
this.advance();
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
consumeSeparatorOrLineBreak(closeType, message) {
|
|
1239
|
+
const next = this.peek();
|
|
1240
|
+
if (next.type === closeType || next.type === TokenType.EOF) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (this.check(TokenType.Comma)) {
|
|
1244
|
+
this.advance();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const prev = this.previous();
|
|
1248
|
+
if (next.span.start.line > prev.span.end.line) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
throw new SyntaxError(message, next.span, "',' or newline", next.value);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function isAllowedSeparatorSpecChar(char) {
|
|
1255
|
+
return /^[A-Za-z0-9!#$%&*+\-.:;=?@^_|~<>]$/.test(char);
|
|
1256
|
+
}
|
|
1257
|
+
const GENERIC_V1_DATATYPES = new Set(['list', 'tuple']);
|
|
1258
|
+
const BRACKETED_V1_DATATYPES = new Set(['sep', 'set', 'radix']);
|
|
1259
|
+
const RESERVED_NULL_SENTINELS = new Set(['none', 'notSet', 'notApplicable', 'tombstone']);
|
|
1260
|
+
const RESERVED_ATTRIBUTE_KEYS = new Set(['@', '@items', '__proto__', 'constructor', 'prototype']);
|
|
1261
|
+
const RESERVED_V1_DATATYPES = new Set([
|
|
1262
|
+
'n', 'number', 'int', 'int8', 'int16', 'int32', 'int64',
|
|
1263
|
+
'uint', 'uint8', 'uint16', 'uint32', 'uint64',
|
|
1264
|
+
'float', 'float32', 'float64',
|
|
1265
|
+
'string', 'trimtick', 'prose', 'boolean', 'bool', 'toggle', 'infinity', 'nan',
|
|
1266
|
+
'hex', 'date', 'time', 'datetime', 'zrut',
|
|
1267
|
+
'encoding', 'base64', 'embed', 'inline',
|
|
1268
|
+
'radix', 'radix2', 'radix6', 'radix8', 'radix12',
|
|
1269
|
+
'sep', 'set',
|
|
1270
|
+
'tuple', 'list', 'object', 'obj', 'envelope', 'o', 'node', 'null',
|
|
1271
|
+
]);
|
|
1272
|
+
/**
|
|
1273
|
+
* Parse AEON tokens into an AST
|
|
1274
|
+
*/
|
|
1275
|
+
export function parse(tokens, options) {
|
|
1276
|
+
const parser = new Parser(tokens, options);
|
|
1277
|
+
return parser.parse();
|
|
1278
|
+
}
|
|
1279
|
+
function isAsciiWhitespaceOnly(value) {
|
|
1280
|
+
return /^[ \t\r\n]+$/.test(value);
|
|
1281
|
+
}
|
|
1282
|
+
//# sourceMappingURL=parser.js.map
|