@hymarkx/compiler 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/analyze/index.d.ts +75 -0
  5. package/dist/analyze/index.d.ts.map +1 -0
  6. package/dist/analyze/index.js +455 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/components/authored.d.ts +30 -0
  9. package/dist/components/authored.d.ts.map +1 -0
  10. package/dist/components/authored.js +293 -0
  11. package/dist/components/authored.js.map +1 -0
  12. package/dist/components/builtins.d.ts +6 -0
  13. package/dist/components/builtins.d.ts.map +1 -0
  14. package/dist/components/builtins.js +205 -0
  15. package/dist/components/builtins.js.map +1 -0
  16. package/dist/components/styles.d.ts +3 -0
  17. package/dist/components/styles.d.ts.map +1 -0
  18. package/dist/components/styles.js +99 -0
  19. package/dist/components/styles.js.map +1 -0
  20. package/dist/components/types.d.ts +77 -0
  21. package/dist/components/types.d.ts.map +1 -0
  22. package/dist/components/types.js +2 -0
  23. package/dist/components/types.js.map +1 -0
  24. package/dist/components/validate.d.ts +14 -0
  25. package/dist/components/validate.d.ts.map +1 -0
  26. package/dist/components/validate.js +347 -0
  27. package/dist/components/validate.js.map +1 -0
  28. package/dist/diagnostic-origin.d.ts +11 -0
  29. package/dist/diagnostic-origin.d.ts.map +1 -0
  30. package/dist/diagnostic-origin.js +12 -0
  31. package/dist/diagnostic-origin.js.map +1 -0
  32. package/dist/diagnostics/render.d.ts +13 -0
  33. package/dist/diagnostics/render.d.ts.map +1 -0
  34. package/dist/diagnostics/render.js +92 -0
  35. package/dist/diagnostics/render.js.map +1 -0
  36. package/dist/emit/backend.d.ts +13 -0
  37. package/dist/emit/backend.d.ts.map +1 -0
  38. package/dist/emit/backend.js +2 -0
  39. package/dist/emit/backend.js.map +1 -0
  40. package/dist/emit/escape.d.ts +5 -0
  41. package/dist/emit/escape.d.ts.map +1 -0
  42. package/dist/emit/escape.js +52 -0
  43. package/dist/emit/escape.js.map +1 -0
  44. package/dist/emit/html.d.ts +16 -0
  45. package/dist/emit/html.d.ts.map +1 -0
  46. package/dist/emit/html.js +528 -0
  47. package/dist/emit/html.js.map +1 -0
  48. package/dist/emit/sanitize.d.ts +19 -0
  49. package/dist/emit/sanitize.d.ts.map +1 -0
  50. package/dist/emit/sanitize.js +383 -0
  51. package/dist/emit/sanitize.js.map +1 -0
  52. package/dist/events.d.ts +7 -0
  53. package/dist/events.d.ts.map +1 -0
  54. package/dist/events.js +16 -0
  55. package/dist/events.js.map +1 -0
  56. package/dist/expression.d.ts +133 -0
  57. package/dist/expression.d.ts.map +1 -0
  58. package/dist/expression.js +1248 -0
  59. package/dist/expression.js.map +1 -0
  60. package/dist/frontmatter.d.ts +15 -0
  61. package/dist/frontmatter.d.ts.map +1 -0
  62. package/dist/frontmatter.js +196 -0
  63. package/dist/frontmatter.js.map +1 -0
  64. package/dist/index.d.ts +18 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +92 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/islands.d.ts +23 -0
  69. package/dist/islands.d.ts.map +1 -0
  70. package/dist/islands.js +76 -0
  71. package/dist/islands.js.map +1 -0
  72. package/dist/runtime.d.ts +14 -0
  73. package/dist/runtime.d.ts.map +1 -0
  74. package/dist/runtime.js +487 -0
  75. package/dist/runtime.js.map +1 -0
  76. package/dist/styles.d.ts +33 -0
  77. package/dist/styles.d.ts.map +1 -0
  78. package/dist/styles.js +370 -0
  79. package/dist/styles.js.map +1 -0
  80. package/dist/suggestions.d.ts +3 -0
  81. package/dist/suggestions.d.ts.map +1 -0
  82. package/dist/suggestions.js +29 -0
  83. package/dist/suggestions.js.map +1 -0
  84. package/dist/types.d.ts +59 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +2 -0
  87. package/dist/types.js.map +1 -0
  88. package/package.json +39 -0
@@ -0,0 +1,1248 @@
1
+ import { createDiagnostic, isForbiddenAttributeName } from '@hymarkx/ast';
2
+ import { nearestSuggestion } from './suggestions.js';
3
+ const MAX_EXPRESSION_DEPTH = 128;
4
+ const OPTIONAL_MISSING = Symbol('optional-missing');
5
+ const BINARY_PRECEDENCE = new Map([
6
+ ['??', 1],
7
+ ['||', 1],
8
+ ['&&', 2],
9
+ ['==', 3],
10
+ ['!=', 3],
11
+ ['<', 4],
12
+ ['<=', 4],
13
+ ['>', 4],
14
+ ['>=', 4],
15
+ ['+', 5],
16
+ ['-', 5],
17
+ ['*', 6],
18
+ ['/', 6],
19
+ ['%', 6],
20
+ ]);
21
+ const ASSIGNMENT_OPERATORS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&&=', '||=', '??=']);
22
+ const KEYWORDS = new Set(['new', 'function', 'this', 'import', 'await', 'yield']);
23
+ const THREE_CHARACTER_OPERATORS = new Set(['===', '!==', '&&=', '||=', '??=', '...']);
24
+ const TWO_CHARACTER_OPERATORS = new Set([
25
+ '?.',
26
+ '??',
27
+ '&&',
28
+ '||',
29
+ '==',
30
+ '!=',
31
+ '<=',
32
+ '>=',
33
+ '=>',
34
+ '++',
35
+ '--',
36
+ '+=',
37
+ '-=',
38
+ '*=',
39
+ '/=',
40
+ '%=',
41
+ '**',
42
+ ]);
43
+ const SINGLE_OPERATORS = new Set(['+', '-', '*', '/', '%', '<', '>', '!', '=']);
44
+ const PUNCTUATION = new Set(['(', ')', '[', ']', '{', '}', '?', ':', '.', ',', '`']);
45
+ class ExpressionFailure extends Error {
46
+ issue;
47
+ constructor(issue) {
48
+ super(issue.message);
49
+ this.issue = issue;
50
+ }
51
+ }
52
+ function failure(code, message, start, end, replacement) {
53
+ throw new ExpressionFailure({
54
+ code,
55
+ message,
56
+ start,
57
+ end: Math.max(start, end),
58
+ ...(replacement === undefined ? {} : { replacement }),
59
+ });
60
+ }
61
+ function isDigit(character) {
62
+ return character !== undefined && character >= '0' && character <= '9';
63
+ }
64
+ function isHexDigit(character) {
65
+ return (isDigit(character) ||
66
+ (character !== undefined && character.toLowerCase() >= 'a' && character.toLowerCase() <= 'f'));
67
+ }
68
+ function isIdentifierStart(character) {
69
+ return (character !== undefined &&
70
+ ((character >= 'A' && character <= 'Z') ||
71
+ (character >= 'a' && character <= 'z') ||
72
+ character === '_' ||
73
+ character === '$'));
74
+ }
75
+ function isIdentifierContinue(character) {
76
+ return isIdentifierStart(character) || isDigit(character);
77
+ }
78
+ function readFixedHex(source, start, length) {
79
+ const end = start + length;
80
+ if (end > source.length) {
81
+ failure('HMX1022', 'Unicode escape is incomplete.', start, source.length);
82
+ }
83
+ for (let index = start; index < end; index += 1) {
84
+ if (!isHexDigit(source[index])) {
85
+ failure('HMX1022', 'Unicode escape contains a non-hexadecimal digit.', index, index + 1);
86
+ }
87
+ }
88
+ return Number.parseInt(source.slice(start, end), 16);
89
+ }
90
+ function readString(source, start) {
91
+ const quote = source[start];
92
+ let cursor = start + 1;
93
+ let value = '';
94
+ while (cursor < source.length) {
95
+ const character = source[cursor];
96
+ if (character === quote) {
97
+ return {
98
+ token: {
99
+ kind: 'literal',
100
+ value: source.slice(start, cursor + 1),
101
+ literal: value,
102
+ start,
103
+ end: cursor + 1,
104
+ },
105
+ end: cursor + 1,
106
+ };
107
+ }
108
+ if (character === '\n' ||
109
+ character === '\r' ||
110
+ character === '\u2028' ||
111
+ character === '\u2029') {
112
+ failure('HMX1022', 'String literals cannot contain a line break.', cursor, cursor + 1);
113
+ }
114
+ if (character !== '\\') {
115
+ value += character;
116
+ cursor += 1;
117
+ continue;
118
+ }
119
+ const escapeStart = cursor;
120
+ const escaped = source[cursor + 1];
121
+ if (escaped === undefined) {
122
+ failure('HMX1022', 'String escape is not terminated.', escapeStart, cursor + 1);
123
+ }
124
+ const simpleEscapes = {
125
+ b: '\b',
126
+ f: '\f',
127
+ n: '\n',
128
+ r: '\r',
129
+ t: '\t',
130
+ v: '\v',
131
+ '0': '\0',
132
+ '\\': '\\',
133
+ "'": "'",
134
+ '"': '"',
135
+ };
136
+ const decodedEscape = simpleEscapes[escaped];
137
+ if (decodedEscape !== undefined) {
138
+ if (escaped === '0' && isDigit(source[cursor + 2])) {
139
+ failure('HMX1022', 'Legacy octal string escapes are not supported.', escapeStart, cursor + 2);
140
+ }
141
+ value += decodedEscape;
142
+ cursor += 2;
143
+ continue;
144
+ }
145
+ if (escaped === 'x') {
146
+ value += String.fromCharCode(readFixedHex(source, cursor + 2, 2));
147
+ cursor += 4;
148
+ continue;
149
+ }
150
+ if (escaped === 'u') {
151
+ if (source[cursor + 2] === '{') {
152
+ const closing = source.indexOf('}', cursor + 3);
153
+ if (closing < 0) {
154
+ failure('HMX1022', 'Unicode code-point escape is not terminated.', escapeStart, source.length);
155
+ }
156
+ const digits = source.slice(cursor + 3, closing);
157
+ if (digits.length === 0 ||
158
+ digits.length > 6 ||
159
+ [...digits].some((digit) => !isHexDigit(digit))) {
160
+ failure('HMX1022', 'Unicode code-point escape is invalid.', escapeStart, closing + 1);
161
+ }
162
+ const codePoint = Number.parseInt(digits, 16);
163
+ if (codePoint > 0x10ffff) {
164
+ failure('HMX1022', 'Unicode code point is out of range.', escapeStart, closing + 1);
165
+ }
166
+ value += String.fromCodePoint(codePoint);
167
+ cursor = closing + 1;
168
+ continue;
169
+ }
170
+ value += String.fromCharCode(readFixedHex(source, cursor + 2, 4));
171
+ cursor += 6;
172
+ continue;
173
+ }
174
+ failure('HMX1022', `Unsupported string escape \\${escaped}.`, escapeStart, cursor + 2);
175
+ }
176
+ failure('HMX1022', 'String literal is not terminated.', start, source.length);
177
+ }
178
+ function readNumber(source, start) {
179
+ let cursor = start;
180
+ if (source[cursor] === '.') {
181
+ cursor += 1;
182
+ }
183
+ while (isDigit(source[cursor])) {
184
+ cursor += 1;
185
+ }
186
+ if (source[cursor] === '.' && source[start] !== '.') {
187
+ cursor += 1;
188
+ while (isDigit(source[cursor])) {
189
+ cursor += 1;
190
+ }
191
+ }
192
+ if (source[cursor] === 'e' || source[cursor] === 'E') {
193
+ const exponentStart = cursor;
194
+ cursor += 1;
195
+ if (source[cursor] === '+' || source[cursor] === '-') {
196
+ cursor += 1;
197
+ }
198
+ const digitsStart = cursor;
199
+ while (isDigit(source[cursor])) {
200
+ cursor += 1;
201
+ }
202
+ if (cursor === digitsStart) {
203
+ failure('HMX1022', 'Numeric exponent requires at least one digit.', exponentStart, cursor);
204
+ }
205
+ }
206
+ if (isIdentifierStart(source[cursor])) {
207
+ failure('HMX1022', 'Numeric literal has an invalid suffix.', cursor, cursor + 1);
208
+ }
209
+ const raw = source.slice(start, cursor);
210
+ const value = Number(raw);
211
+ if (!Number.isFinite(value)) {
212
+ failure('HMX2042', 'Numeric result must be finite.', start, cursor);
213
+ }
214
+ return {
215
+ token: { kind: 'literal', value: raw, literal: value, start, end: cursor },
216
+ end: cursor,
217
+ };
218
+ }
219
+ function tokenize(source) {
220
+ const tokens = [];
221
+ let cursor = 0;
222
+ while (cursor < source.length) {
223
+ const character = source[cursor];
224
+ if (character === ' ' || character === '\t' || character === '\n' || character === '\r') {
225
+ cursor += 1;
226
+ continue;
227
+ }
228
+ if (character === '"' || character === "'") {
229
+ const result = readString(source, cursor);
230
+ tokens.push(result.token);
231
+ cursor = result.end;
232
+ continue;
233
+ }
234
+ if (isDigit(character) || (character === '.' && isDigit(source[cursor + 1]))) {
235
+ const result = readNumber(source, cursor);
236
+ tokens.push(result.token);
237
+ cursor = result.end;
238
+ continue;
239
+ }
240
+ if (isIdentifierStart(character)) {
241
+ const start = cursor;
242
+ cursor += 1;
243
+ while (isIdentifierContinue(source[cursor])) {
244
+ cursor += 1;
245
+ }
246
+ const value = source.slice(start, cursor);
247
+ if (value === 'true' || value === 'false' || value === 'null') {
248
+ tokens.push({
249
+ kind: 'literal',
250
+ value,
251
+ literal: value === 'true' ? true : value === 'false' ? false : null,
252
+ start,
253
+ end: cursor,
254
+ });
255
+ }
256
+ else {
257
+ tokens.push({
258
+ kind: KEYWORDS.has(value) ? 'keyword' : 'identifier',
259
+ value,
260
+ start,
261
+ end: cursor,
262
+ });
263
+ }
264
+ continue;
265
+ }
266
+ const three = source.slice(cursor, cursor + 3);
267
+ if (THREE_CHARACTER_OPERATORS.has(three)) {
268
+ tokens.push({ kind: 'operator', value: three, start: cursor, end: cursor + 3 });
269
+ cursor += 3;
270
+ continue;
271
+ }
272
+ const two = source.slice(cursor, cursor + 2);
273
+ if (TWO_CHARACTER_OPERATORS.has(two)) {
274
+ tokens.push({ kind: 'operator', value: two, start: cursor, end: cursor + 2 });
275
+ cursor += 2;
276
+ continue;
277
+ }
278
+ if (character !== undefined && SINGLE_OPERATORS.has(character)) {
279
+ tokens.push({ kind: 'operator', value: character, start: cursor, end: cursor + 1 });
280
+ cursor += 1;
281
+ continue;
282
+ }
283
+ if (character !== undefined && PUNCTUATION.has(character)) {
284
+ tokens.push({ kind: 'punctuation', value: character, start: cursor, end: cursor + 1 });
285
+ cursor += 1;
286
+ continue;
287
+ }
288
+ failure('HMX1022', `Unexpected character ${JSON.stringify(character)} in expression.`, cursor, cursor + 1);
289
+ }
290
+ tokens.push({ kind: 'eof', value: '', start: source.length, end: source.length });
291
+ return tokens;
292
+ }
293
+ function binaryOperator(token) {
294
+ return BINARY_PRECEDENCE.has(token.value)
295
+ ? token.value
296
+ : undefined;
297
+ }
298
+ function logicalFamily(expression) {
299
+ if (expression.type !== 'binary') {
300
+ return undefined;
301
+ }
302
+ if (expression.operator === '??') {
303
+ return 'nullish';
304
+ }
305
+ return expression.operator === '&&' || expression.operator === '||' ? 'logical' : undefined;
306
+ }
307
+ class Parser {
308
+ #tokens;
309
+ #allowAssignment;
310
+ #cursor = 0;
311
+ #nesting = 0;
312
+ constructor(tokens, allowAssignment = false) {
313
+ this.#tokens = tokens;
314
+ this.#allowAssignment = allowAssignment;
315
+ }
316
+ current() {
317
+ return this.#tokens[this.#cursor] ?? this.#tokens[this.#tokens.length - 1];
318
+ }
319
+ peek(distance = 1) {
320
+ return this.#tokens[this.#cursor + distance] ?? this.#tokens[this.#tokens.length - 1];
321
+ }
322
+ consume() {
323
+ const token = this.current();
324
+ this.#cursor += 1;
325
+ return token;
326
+ }
327
+ matches(value) {
328
+ return this.current().value === value;
329
+ }
330
+ expect(value, message) {
331
+ const token = this.current();
332
+ if (token.value !== value) {
333
+ if (!this.#allowAssignment && ASSIGNMENT_OPERATORS.has(token.value)) {
334
+ failure('HMX2061', 'Assignment is only allowed inside an event handler.', token.start, token.end);
335
+ }
336
+ failure('HMX1022', message, token.start, token.end);
337
+ }
338
+ return this.consume();
339
+ }
340
+ nested(token, parse) {
341
+ this.#nesting += 1;
342
+ if (this.#nesting > MAX_EXPRESSION_DEPTH) {
343
+ failure('HMX1021', 'Expression nesting is too deep.', token.start, token.end);
344
+ }
345
+ try {
346
+ return parse();
347
+ }
348
+ finally {
349
+ this.#nesting -= 1;
350
+ }
351
+ }
352
+ parse() {
353
+ const expression = this.parseNestedExpression();
354
+ const trailing = this.current();
355
+ if (trailing.kind !== 'eof') {
356
+ this.rejectTrailing(trailing);
357
+ }
358
+ this.assertDepth(expression);
359
+ return expression;
360
+ }
361
+ rejectTrailing(token) {
362
+ if (ASSIGNMENT_OPERATORS.has(token.value)) {
363
+ failure('HMX2061', 'Assignment is only allowed inside an event handler.', token.start, token.end);
364
+ }
365
+ if (token.value === '++' || token.value === '--') {
366
+ failure('HMX2044', 'Increment and decrement are not allowed.', token.start, token.end);
367
+ }
368
+ if (token.value === '=>') {
369
+ failure('HMX2044', 'Arrow function literals are not allowed.', token.start, token.end);
370
+ }
371
+ if (token.value === ',') {
372
+ failure('HMX2044', 'The comma operator is not allowed.', token.start, token.end);
373
+ }
374
+ if (token.value === '(' || token.value === '?.') {
375
+ failure('HMX2044', 'Function calls are not allowed.', token.start, token.end);
376
+ }
377
+ if (token.value === '`') {
378
+ failure('HMX2044', 'Tagged templates are not allowed.', token.start, token.end);
379
+ }
380
+ failure('HMX1022', `Unexpected token ${JSON.stringify(token.value)}.`, token.start, token.end);
381
+ }
382
+ parseNestedExpression() {
383
+ return this.parseAssignment();
384
+ }
385
+ parseAssignment() {
386
+ const target = this.parseConditional();
387
+ const token = this.current();
388
+ if (!ASSIGNMENT_OPERATORS.has(token.value)) {
389
+ return target;
390
+ }
391
+ if (!this.#allowAssignment) {
392
+ failure('HMX2061', 'Assignment is only allowed inside an event handler.', token.start, token.end);
393
+ }
394
+ if (target.type !== 'identifier') {
395
+ failure('HMX2061', 'An event handler may only assign to a declared state name.', target.start, target.end);
396
+ }
397
+ this.consume();
398
+ const value = this.nested(token, () => this.parseAssignment());
399
+ return {
400
+ type: 'assignment',
401
+ name: target.name,
402
+ operator: token.value,
403
+ value,
404
+ start: target.start,
405
+ end: value.end,
406
+ };
407
+ }
408
+ parseConditional() {
409
+ const test = this.parseBinary(1);
410
+ if (!this.matches('?')) {
411
+ return test;
412
+ }
413
+ const question = this.consume();
414
+ return this.nested(question, () => {
415
+ const consequent = this.parseNestedExpression();
416
+ this.expect(':', 'Ternary expression requires a colon.');
417
+ const alternate = this.parseNestedExpression();
418
+ return {
419
+ type: 'conditional',
420
+ test,
421
+ consequent,
422
+ alternate,
423
+ start: test.start,
424
+ end: alternate.end,
425
+ };
426
+ });
427
+ }
428
+ parseBinary(minimumPrecedence) {
429
+ let left = this.parseUnary();
430
+ while (true) {
431
+ const token = this.current();
432
+ const operator = binaryOperator(token);
433
+ const precedence = operator === undefined ? undefined : BINARY_PRECEDENCE.get(operator);
434
+ if (operator === undefined || precedence === undefined || precedence < minimumPrecedence) {
435
+ return left;
436
+ }
437
+ this.consume();
438
+ const right = this.parseBinary(precedence + 1);
439
+ const family = operator === '??'
440
+ ? 'nullish'
441
+ : operator === '&&' || operator === '||'
442
+ ? 'logical'
443
+ : undefined;
444
+ if (family !== undefined &&
445
+ [logicalFamily(left), logicalFamily(right)].some((childFamily) => childFamily !== undefined && childFamily !== family)) {
446
+ failure('HMX1022', 'Nullish coalescing cannot be mixed with && or || without parentheses.', token.start, token.end);
447
+ }
448
+ left = {
449
+ type: 'binary',
450
+ operator,
451
+ left,
452
+ right,
453
+ operatorStart: token.start,
454
+ start: left.start,
455
+ end: right.end,
456
+ };
457
+ }
458
+ }
459
+ parseUnary() {
460
+ const token = this.current();
461
+ if (token.value === '!' || token.value === '-' || token.value === '+') {
462
+ this.consume();
463
+ return this.nested(token, () => {
464
+ const argument = this.parseUnary();
465
+ return {
466
+ type: 'unary',
467
+ operator: token.value,
468
+ argument,
469
+ start: token.start,
470
+ end: argument.end,
471
+ };
472
+ });
473
+ }
474
+ return this.parsePostfix(this.parsePrimary());
475
+ }
476
+ parsePostfix(initial) {
477
+ let expression = initial;
478
+ while (true) {
479
+ const token = this.current();
480
+ if (token.value === '(' || (token.value === '?.' && this.peek().value === '(')) {
481
+ failure('HMX2044', 'Function calls are not allowed.', token.start, token.end);
482
+ }
483
+ if (token.value === '`') {
484
+ failure('HMX2044', 'Tagged templates are not allowed.', token.start, token.end);
485
+ }
486
+ if (token.value === '.' || token.value === '?.') {
487
+ const optional = token.value === '?.';
488
+ this.consume();
489
+ if (this.matches('[')) {
490
+ this.consume();
491
+ const property = this.nested(token, () => this.parseNestedExpression());
492
+ const closing = this.expect(']', 'Computed member access requires a closing bracket.');
493
+ expression = {
494
+ type: 'member',
495
+ object: expression,
496
+ property,
497
+ computed: true,
498
+ optional,
499
+ propertyStart: property.start,
500
+ propertyEnd: property.end,
501
+ start: expression.start,
502
+ end: closing.end,
503
+ };
504
+ continue;
505
+ }
506
+ const property = this.current();
507
+ if (property.kind !== 'identifier' &&
508
+ property.kind !== 'keyword' &&
509
+ property.kind !== 'literal') {
510
+ failure('HMX1022', 'Member access requires a property name.', property.start, property.end);
511
+ }
512
+ if (property.kind === 'literal' &&
513
+ typeof property.literal !== 'boolean' &&
514
+ property.literal !== null) {
515
+ failure('HMX1022', 'Member access requires an identifier-style property name.', property.start, property.end);
516
+ }
517
+ this.consume();
518
+ expression = {
519
+ type: 'member',
520
+ object: expression,
521
+ property: property.value,
522
+ computed: false,
523
+ optional,
524
+ propertyStart: property.start,
525
+ propertyEnd: property.end,
526
+ start: expression.start,
527
+ end: property.end,
528
+ };
529
+ continue;
530
+ }
531
+ if (token.value === '[') {
532
+ this.consume();
533
+ const property = this.nested(token, () => this.parseNestedExpression());
534
+ const closing = this.expect(']', 'Computed member access requires a closing bracket.');
535
+ expression = {
536
+ type: 'member',
537
+ object: expression,
538
+ property,
539
+ computed: true,
540
+ optional: false,
541
+ propertyStart: property.start,
542
+ propertyEnd: property.end,
543
+ start: expression.start,
544
+ end: closing.end,
545
+ };
546
+ continue;
547
+ }
548
+ return expression;
549
+ }
550
+ }
551
+ parsePrimary() {
552
+ const token = this.consume();
553
+ if (token.kind === 'literal') {
554
+ return { type: 'literal', value: token.literal, start: token.start, end: token.end };
555
+ }
556
+ if (token.kind === 'identifier') {
557
+ return { type: 'identifier', name: token.value, start: token.start, end: token.end };
558
+ }
559
+ if (token.kind === 'keyword') {
560
+ const messages = {
561
+ new: 'new expressions are not allowed.',
562
+ function: this.current().value === '*'
563
+ ? 'Generator literals are not allowed.'
564
+ : 'Function literals are not allowed.',
565
+ this: 'this is not available in expressions.',
566
+ import: 'import expressions are not allowed.',
567
+ await: 'await expressions are not allowed.',
568
+ yield: 'Generators are not allowed.',
569
+ };
570
+ failure('HMX2044', messages[token.value] ?? 'This construct is not allowed.', token.start, token.end);
571
+ }
572
+ if (token.value === '`') {
573
+ failure('HMX2044', 'Template literals are not allowed.', token.start, token.end);
574
+ }
575
+ if (token.value === '/') {
576
+ failure('HMX2044', 'Regular expression literals are not allowed.', token.start, token.end);
577
+ }
578
+ if (token.value === '++' || token.value === '--') {
579
+ failure('HMX2044', 'Increment and decrement are not allowed.', token.start, token.end);
580
+ }
581
+ if (token.value === '(') {
582
+ if (this.matches(')') && this.peek().value === '=>') {
583
+ const arrow = this.peek();
584
+ failure('HMX2044', 'Arrow function literals are not allowed.', arrow.start, arrow.end);
585
+ }
586
+ return this.nested(token, () => {
587
+ const expression = this.parseNestedExpression();
588
+ if (this.matches(',')) {
589
+ const comma = this.current();
590
+ failure('HMX2044', 'The comma operator is not allowed.', comma.start, comma.end);
591
+ }
592
+ const closing = this.expect(')', 'Parenthesized expression requires a closing parenthesis.');
593
+ return { type: 'group', expression, start: token.start, end: closing.end };
594
+ });
595
+ }
596
+ if (token.value === '[') {
597
+ return this.parseArray(token);
598
+ }
599
+ if (token.value === '{') {
600
+ return this.parseObject(token);
601
+ }
602
+ if (token.kind === 'eof') {
603
+ failure('HMX1022', 'Expected an expression.', token.start, token.end);
604
+ }
605
+ this.rejectTrailing(token);
606
+ }
607
+ parseArray(opening) {
608
+ return this.nested(opening, () => {
609
+ const elements = [];
610
+ if (this.matches(']')) {
611
+ const closing = this.consume();
612
+ return { type: 'array', elements, start: opening.start, end: closing.end };
613
+ }
614
+ while (true) {
615
+ if (this.matches(',')) {
616
+ const comma = this.current();
617
+ failure('HMX1022', 'Array holes are not supported.', comma.start, comma.end);
618
+ }
619
+ elements.push(this.parseNestedExpression());
620
+ if (!this.matches(',')) {
621
+ const closing = this.expect(']', 'Array literal requires a closing bracket.');
622
+ return { type: 'array', elements, start: opening.start, end: closing.end };
623
+ }
624
+ this.consume();
625
+ if (this.matches(']')) {
626
+ const closing = this.consume();
627
+ return { type: 'array', elements, start: opening.start, end: closing.end };
628
+ }
629
+ }
630
+ });
631
+ }
632
+ parseObject(opening) {
633
+ return this.nested(opening, () => {
634
+ const properties = [];
635
+ if (this.matches('}')) {
636
+ const closing = this.consume();
637
+ return { type: 'object', properties, start: opening.start, end: closing.end };
638
+ }
639
+ while (true) {
640
+ const keyToken = this.consume();
641
+ let key;
642
+ if (keyToken.kind === 'identifier' || keyToken.kind === 'keyword') {
643
+ key = keyToken.value;
644
+ }
645
+ else if (keyToken.kind === 'literal' &&
646
+ (typeof keyToken.literal === 'string' || typeof keyToken.literal === 'number')) {
647
+ key = String(keyToken.literal);
648
+ }
649
+ else {
650
+ failure('HMX1022', 'Object literal requires a static property name.', keyToken.start, keyToken.end);
651
+ }
652
+ let value;
653
+ if (this.matches(':')) {
654
+ this.consume();
655
+ value = this.parseNestedExpression();
656
+ }
657
+ else if (keyToken.kind === 'identifier') {
658
+ value = { type: 'identifier', name: key, start: keyToken.start, end: keyToken.end };
659
+ }
660
+ else {
661
+ failure('HMX1022', 'Object property requires a colon and value.', keyToken.start, keyToken.end);
662
+ }
663
+ properties.push({ key, value });
664
+ if (!this.matches(',')) {
665
+ const closing = this.expect('}', 'Object literal requires a closing brace.');
666
+ return { type: 'object', properties, start: opening.start, end: closing.end };
667
+ }
668
+ this.consume();
669
+ if (this.matches('}')) {
670
+ const closing = this.consume();
671
+ return { type: 'object', properties, start: opening.start, end: closing.end };
672
+ }
673
+ }
674
+ });
675
+ }
676
+ assertDepth(expression) {
677
+ const stack = [
678
+ { expression, depth: 1 },
679
+ ];
680
+ while (stack.length > 0) {
681
+ const frame = stack.pop();
682
+ if (frame === undefined) {
683
+ continue;
684
+ }
685
+ if (frame.depth > MAX_EXPRESSION_DEPTH) {
686
+ failure('HMX1021', 'Expression nesting is too deep.', frame.expression.start, Math.min(frame.expression.start + 1, frame.expression.end));
687
+ }
688
+ const nextDepth = frame.depth + 1;
689
+ const node = frame.expression;
690
+ if (node.type === 'unary') {
691
+ stack.push({ expression: node.argument, depth: nextDepth });
692
+ }
693
+ else if (node.type === 'binary') {
694
+ stack.push({ expression: node.left, depth: nextDepth }, { expression: node.right, depth: nextDepth });
695
+ }
696
+ else if (node.type === 'conditional') {
697
+ stack.push({ expression: node.test, depth: nextDepth }, { expression: node.consequent, depth: nextDepth }, { expression: node.alternate, depth: nextDepth });
698
+ }
699
+ else if (node.type === 'member') {
700
+ stack.push({ expression: node.object, depth: nextDepth });
701
+ if (node.computed) {
702
+ stack.push({ expression: node.property, depth: nextDepth });
703
+ }
704
+ }
705
+ else if (node.type === 'array') {
706
+ for (const element of node.elements) {
707
+ stack.push({ expression: element, depth: nextDepth });
708
+ }
709
+ }
710
+ else if (node.type === 'object') {
711
+ for (const property of node.properties) {
712
+ stack.push({ expression: property.value, depth: nextDepth });
713
+ }
714
+ }
715
+ else if (node.type === 'group') {
716
+ stack.push({ expression: node.expression, depth: nextDepth });
717
+ }
718
+ else if (node.type === 'assignment') {
719
+ stack.push({ expression: node.value, depth: nextDepth });
720
+ }
721
+ }
722
+ }
723
+ }
724
+ function instructionFor(expression) {
725
+ switch (expression.type) {
726
+ case 'literal':
727
+ return ['l', expression.value];
728
+ case 'identifier':
729
+ return ['i', expression.name];
730
+ case 'unary':
731
+ return ['u', expression.operator, instructionFor(expression.argument)];
732
+ case 'binary':
733
+ return [
734
+ 'b',
735
+ expression.operator,
736
+ instructionFor(expression.left),
737
+ instructionFor(expression.right),
738
+ ];
739
+ case 'conditional':
740
+ return [
741
+ 'c',
742
+ instructionFor(expression.test),
743
+ instructionFor(expression.consequent),
744
+ instructionFor(expression.alternate),
745
+ ];
746
+ case 'member':
747
+ return [
748
+ 'm',
749
+ instructionFor(expression.object),
750
+ expression.computed
751
+ ? instructionFor(expression.property)
752
+ : expression.property,
753
+ expression.computed ? 1 : 0,
754
+ expression.optional ? 1 : 0,
755
+ ];
756
+ case 'array':
757
+ return ['r', expression.elements.map(instructionFor)];
758
+ case 'object':
759
+ return ['o', expression.properties.map(({ key, value }) => [key, instructionFor(value)])];
760
+ case 'group':
761
+ return instructionFor(expression.expression);
762
+ case 'assignment':
763
+ return ['a', expression.name, expression.operator, instructionFor(expression.value)];
764
+ }
765
+ }
766
+ function expressionNames(expression) {
767
+ const identifiers = new Set();
768
+ const assignments = new Set();
769
+ const stack = [expression];
770
+ while (stack.length > 0) {
771
+ const node = stack.pop();
772
+ if (node === undefined)
773
+ continue;
774
+ if (node.type === 'identifier') {
775
+ identifiers.add(node.name);
776
+ }
777
+ else if (node.type === 'unary') {
778
+ stack.push(node.argument);
779
+ }
780
+ else if (node.type === 'binary') {
781
+ stack.push(node.left, node.right);
782
+ }
783
+ else if (node.type === 'conditional') {
784
+ stack.push(node.test, node.consequent, node.alternate);
785
+ }
786
+ else if (node.type === 'member') {
787
+ stack.push(node.object);
788
+ if (node.computed)
789
+ stack.push(node.property);
790
+ }
791
+ else if (node.type === 'array') {
792
+ stack.push(...node.elements);
793
+ }
794
+ else if (node.type === 'object') {
795
+ stack.push(...node.properties.map(({ value }) => value));
796
+ }
797
+ else if (node.type === 'group') {
798
+ stack.push(node.expression);
799
+ }
800
+ else if (node.type === 'assignment') {
801
+ assignments.add(node.name);
802
+ if (node.operator !== '=')
803
+ identifiers.add(node.name);
804
+ stack.push(node.value);
805
+ }
806
+ }
807
+ return { identifiers: [...identifiers], assignments: [...assignments] };
808
+ }
809
+ function assertStaticScope(expression, scope) {
810
+ const names = expressionNames(expression);
811
+ for (const name of names.identifiers) {
812
+ if (Object.hasOwn(scope, name))
813
+ continue;
814
+ const identifier = findIdentifier(expression, name);
815
+ failure('HMX2040', `Unknown identifier "${name}".`, identifier?.start ?? 0, identifier?.end ?? Math.min(1, name.length));
816
+ }
817
+ }
818
+ function literalPropertyName(expression) {
819
+ if (expression.type === 'group')
820
+ return literalPropertyName(expression.expression);
821
+ if (expression.type !== 'literal')
822
+ return undefined;
823
+ return expression.value === null ? 'null' : String(expression.value);
824
+ }
825
+ function assertStaticMemberSafety(expression) {
826
+ const member = findExpression(expression, (node) => {
827
+ if (node.type !== 'member')
828
+ return false;
829
+ const name = node.computed
830
+ ? literalPropertyName(node.property)
831
+ : node.property;
832
+ return name !== undefined && isForbiddenAttributeName(name);
833
+ });
834
+ if (member === undefined)
835
+ return;
836
+ const name = member.computed
837
+ ? literalPropertyName(member.property)
838
+ : member.property;
839
+ failure('HMX2044', `Property "${name ?? ''}" is forbidden.`, member.propertyStart, member.propertyEnd);
840
+ }
841
+ function isStructured(value) {
842
+ return typeof value === 'object' && value !== null;
843
+ }
844
+ function scalarString(value) {
845
+ if (value === null) {
846
+ return 'null';
847
+ }
848
+ return String(value);
849
+ }
850
+ class Evaluator {
851
+ #scope;
852
+ #identifierNames;
853
+ constructor(scope, identifierNames) {
854
+ this.#scope = scope;
855
+ this.#identifierNames = identifierNames;
856
+ }
857
+ evaluate(expression) {
858
+ const value = this.evaluateNode(expression);
859
+ return value === OPTIONAL_MISSING ? null : value;
860
+ }
861
+ safeValue(value, expression) {
862
+ if (value === null ||
863
+ typeof value === 'string' ||
864
+ typeof value === 'boolean' ||
865
+ typeof value === 'number') {
866
+ if (typeof value === 'number' && !Number.isFinite(value)) {
867
+ failure('HMX2042', 'Numeric result must be finite.', expression.start, expression.end);
868
+ }
869
+ return value;
870
+ }
871
+ if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
872
+ return value;
873
+ }
874
+ failure('HMX2044', typeof value === 'function'
875
+ ? 'Function values are not allowed.'
876
+ : 'This value is not available to expressions.', expression.start, expression.end);
877
+ }
878
+ value(expression) {
879
+ const value = this.evaluateNode(expression);
880
+ return value === OPTIONAL_MISSING ? null : value;
881
+ }
882
+ evaluateNode(expression) {
883
+ switch (expression.type) {
884
+ case 'literal':
885
+ return expression.value;
886
+ case 'identifier': {
887
+ if (!Object.hasOwn(this.#scope, expression.name)) {
888
+ const replacement = nearestSuggestion(expression.name, this.#identifierNames.filter((name) => name !== expression.name));
889
+ failure('HMX2040', replacement === undefined
890
+ ? `Unknown identifier "${expression.name}".`
891
+ : `Unknown identifier "${expression.name}"; did you mean "${replacement}"?`, expression.start, expression.end, replacement);
892
+ }
893
+ return this.safeValue(this.#scope[expression.name], expression);
894
+ }
895
+ case 'group':
896
+ return this.value(expression.expression);
897
+ case 'unary':
898
+ return this.evaluateUnary(expression);
899
+ case 'binary':
900
+ return this.evaluateBinary(expression);
901
+ case 'conditional':
902
+ return this.isTruthy(this.value(expression.test))
903
+ ? this.value(expression.consequent)
904
+ : this.value(expression.alternate);
905
+ case 'member':
906
+ return this.evaluateMember(expression);
907
+ case 'array':
908
+ return expression.elements.map((element) => this.value(element));
909
+ case 'object': {
910
+ const output = Object.create(null);
911
+ for (const property of expression.properties) {
912
+ output[property.key] = this.value(property.value);
913
+ }
914
+ return output;
915
+ }
916
+ case 'assignment':
917
+ failure('HMX2061', 'Assignment is only allowed inside an event handler.', expression.start, expression.end);
918
+ }
919
+ }
920
+ isTruthy(value) {
921
+ if (value === null || value === false || value === '' || value === 0) {
922
+ return false;
923
+ }
924
+ return !(typeof value === 'number' && Number.isNaN(value));
925
+ }
926
+ finite(value, expression) {
927
+ if (!Number.isFinite(value)) {
928
+ failure('HMX2042', 'Numeric result must be finite.', expression.start, expression.end);
929
+ }
930
+ return value;
931
+ }
932
+ number(value, expression) {
933
+ if (isStructured(value)) {
934
+ failure('HMX2043', 'Objects and arrays cannot be coerced to numbers.', expression.start, expression.end);
935
+ }
936
+ return this.finite(Number(value), expression);
937
+ }
938
+ evaluateUnary(expression) {
939
+ const value = this.value(expression.argument);
940
+ if (expression.operator === '!') {
941
+ return !this.isTruthy(value);
942
+ }
943
+ const numeric = this.number(value, expression);
944
+ return expression.operator === '-' ? this.finite(-numeric, expression) : numeric;
945
+ }
946
+ evaluateBinary(expression) {
947
+ const operator = expression.operator;
948
+ if (operator === '&&') {
949
+ const left = this.value(expression.left);
950
+ return this.isTruthy(left) ? this.value(expression.right) : left;
951
+ }
952
+ if (operator === '||') {
953
+ const left = this.value(expression.left);
954
+ return this.isTruthy(left) ? left : this.value(expression.right);
955
+ }
956
+ if (operator === '??') {
957
+ const left = this.value(expression.left);
958
+ return left === null ? this.value(expression.right) : left;
959
+ }
960
+ const left = this.value(expression.left);
961
+ const right = this.value(expression.right);
962
+ if (operator === '==') {
963
+ return this.equal(left, right, expression);
964
+ }
965
+ if (operator === '!=') {
966
+ return !this.equal(left, right, expression);
967
+ }
968
+ if (operator === '+') {
969
+ if (isStructured(left) || isStructured(right)) {
970
+ failure('HMX2043', 'Objects and arrays cannot be used with +.', expression.operatorStart, expression.operatorStart + 1);
971
+ }
972
+ return typeof left === 'string' || typeof right === 'string'
973
+ ? scalarString(left) + scalarString(right)
974
+ : this.finite(this.number(left, expression) + this.number(right, expression), expression);
975
+ }
976
+ if (operator === '-') {
977
+ return this.finite(this.number(left, expression) - this.number(right, expression), expression);
978
+ }
979
+ if (operator === '*') {
980
+ return this.finite(this.number(left, expression) * this.number(right, expression), expression);
981
+ }
982
+ if (operator === '/') {
983
+ return this.finite(this.number(left, expression) / this.number(right, expression), expression);
984
+ }
985
+ if (operator === '%') {
986
+ return this.finite(this.number(left, expression) % this.number(right, expression), expression);
987
+ }
988
+ return this.compare(operator, left, right, expression);
989
+ }
990
+ equal(left, right, expression) {
991
+ if (isStructured(left) || isStructured(right)) {
992
+ if (isStructured(left) && isStructured(right)) {
993
+ return left === right;
994
+ }
995
+ failure('HMX2043', 'Objects and arrays cannot be compared with scalar values.', expression.operatorStart, expression.operatorStart + expression.operator.length);
996
+ }
997
+ if (left === null || right === null) {
998
+ return left === right;
999
+ }
1000
+ if (typeof left === typeof right) {
1001
+ return left === right;
1002
+ }
1003
+ if (typeof left === 'boolean') {
1004
+ return this.number(left, expression) === this.number(right, expression);
1005
+ }
1006
+ if (typeof right === 'boolean') {
1007
+ return this.number(left, expression) === this.number(right, expression);
1008
+ }
1009
+ if ((typeof left === 'number' && typeof right === 'string') ||
1010
+ (typeof left === 'string' && typeof right === 'number')) {
1011
+ return this.number(left, expression) === this.number(right, expression);
1012
+ }
1013
+ return false;
1014
+ }
1015
+ compare(operator, left, right, expression) {
1016
+ if (isStructured(left) || isStructured(right)) {
1017
+ failure('HMX2043', 'Objects and arrays cannot be ordered.', expression.operatorStart, expression.operatorStart + operator.length);
1018
+ }
1019
+ const comparableLeft = typeof left === 'string' && typeof right === 'string' ? left : this.number(left, expression);
1020
+ const comparableRight = typeof left === 'string' && typeof right === 'string' ? right : this.number(right, expression);
1021
+ if (operator === '<')
1022
+ return comparableLeft < comparableRight;
1023
+ if (operator === '<=')
1024
+ return comparableLeft <= comparableRight;
1025
+ if (operator === '>')
1026
+ return comparableLeft > comparableRight;
1027
+ return comparableLeft >= comparableRight;
1028
+ }
1029
+ propertyKey(value, expression) {
1030
+ if (isStructured(value)) {
1031
+ failure('HMX2041', 'Computed property name must be a scalar value.', expression.propertyStart, expression.propertyEnd);
1032
+ }
1033
+ return scalarString(value);
1034
+ }
1035
+ ownProperty(value, key) {
1036
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
1037
+ return { found: false };
1038
+ }
1039
+ if (typeof value === 'string') {
1040
+ if (key === 'length') {
1041
+ return { found: true, value: value.length };
1042
+ }
1043
+ const index = Number(key);
1044
+ return Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key
1045
+ ? { found: true, value: value[index] }
1046
+ : { found: false };
1047
+ }
1048
+ if (!Object.hasOwn(value, key)) {
1049
+ return { found: false };
1050
+ }
1051
+ const record = value;
1052
+ return { found: true, value: record[key] };
1053
+ }
1054
+ evaluateMember(expression) {
1055
+ const object = this.evaluateNode(expression.object);
1056
+ if (object === OPTIONAL_MISSING) {
1057
+ return OPTIONAL_MISSING;
1058
+ }
1059
+ if (object === null) {
1060
+ if (expression.optional) {
1061
+ return OPTIONAL_MISSING;
1062
+ }
1063
+ failure('HMX2041', 'Property does not exist on null.', expression.propertyStart, expression.propertyEnd);
1064
+ }
1065
+ const key = expression.computed
1066
+ ? this.propertyKey(this.value(expression.property), expression)
1067
+ : expression.property;
1068
+ if (isForbiddenAttributeName(key)) {
1069
+ failure('HMX2044', `Property "${key}" is forbidden.`, expression.propertyStart, expression.propertyEnd);
1070
+ }
1071
+ const property = this.ownProperty(object, key);
1072
+ if (!property.found) {
1073
+ if (expression.optional) {
1074
+ return OPTIONAL_MISSING;
1075
+ }
1076
+ failure('HMX2041', `Property "${key}" does not exist on this value.`, expression.propertyStart, expression.propertyEnd);
1077
+ }
1078
+ return this.safeValue(property.value, expression);
1079
+ }
1080
+ }
1081
+ function pointAt(source, offset) {
1082
+ const clamped = Math.min(Math.max(offset, 0), source.length);
1083
+ let line = 1;
1084
+ let lineStart = 0;
1085
+ for (let index = 0; index < clamped; index += 1) {
1086
+ if (source.charCodeAt(index) === 10) {
1087
+ line += 1;
1088
+ lineStart = index + 1;
1089
+ }
1090
+ }
1091
+ return { line, column: clamped - lineStart + 1, offset: clamped };
1092
+ }
1093
+ function issueSpan(issue, context) {
1094
+ const start = context.startOffset + issue.start;
1095
+ const end = context.startOffset + issue.end;
1096
+ return {
1097
+ start: pointAt(context.documentSource, start),
1098
+ end: pointAt(context.documentSource, end),
1099
+ };
1100
+ }
1101
+ function issueDiagnostic(issue, context) {
1102
+ const span = issueSpan(issue, context);
1103
+ return createDiagnostic({
1104
+ code: issue.code,
1105
+ severity: 'error',
1106
+ message: issue.message,
1107
+ span,
1108
+ ...(issue.replacement === undefined
1109
+ ? {}
1110
+ : {
1111
+ suggestion: {
1112
+ message: `Replace with "${issue.replacement}".`,
1113
+ replacement: issue.replacement,
1114
+ span,
1115
+ },
1116
+ }),
1117
+ });
1118
+ }
1119
+ /**
1120
+ * Parses and evaluates one expression without host evaluation. Every input returns either
1121
+ * a value or one source-located diagnostic; no expression failure escapes as an exception.
1122
+ */
1123
+ export function evaluateExpression(source, scope, context = { documentSource: source, startOffset: 0 }) {
1124
+ try {
1125
+ const expression = new Parser(tokenize(source)).parse();
1126
+ assertStaticMemberSafety(expression);
1127
+ if (context.strictScope === true)
1128
+ assertStaticScope(expression, scope);
1129
+ const value = new Evaluator(scope, context.identifierNames ?? Object.keys(scope)).evaluate(expression);
1130
+ const names = expressionNames(expression);
1131
+ return {
1132
+ ok: true,
1133
+ value,
1134
+ instruction: instructionFor(expression),
1135
+ identifiers: names.identifiers,
1136
+ diagnostics: [],
1137
+ };
1138
+ }
1139
+ catch (error) {
1140
+ const issue = error instanceof ExpressionFailure
1141
+ ? error.issue
1142
+ : {
1143
+ code: 'HMX1022',
1144
+ message: 'Expression could not be evaluated safely.',
1145
+ start: 0,
1146
+ end: Math.min(1, source.length),
1147
+ };
1148
+ return { ok: false, diagnostics: [issueDiagnostic(issue, context)] };
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Parses one event handler through the shared expression parser and restricts every read and
1153
+ * assignment to the component-local state names supplied by the compiler.
1154
+ */
1155
+ export function compileHandlerExpression(source, stateNames, context = { documentSource: source, startOffset: 0 }) {
1156
+ try {
1157
+ const expression = new Parser(tokenize(source), true).parse();
1158
+ assertStaticMemberSafety(expression);
1159
+ const names = expressionNames(expression);
1160
+ for (const name of names.assignments) {
1161
+ if (!stateNames.has(name)) {
1162
+ const assignment = findAssignment(expression, name);
1163
+ failure('HMX2061', `An event handler may only assign to declared state; "${name}" is not declared.`, assignment?.start ?? 0, assignment === undefined ? Math.min(1, source.length) : assignment.start + name.length);
1164
+ }
1165
+ }
1166
+ for (const name of names.identifiers) {
1167
+ if (!stateNames.has(name)) {
1168
+ const identifier = findIdentifier(expression, name);
1169
+ const replacement = nearestSuggestion(name, [...stateNames]);
1170
+ failure('HMX2040', replacement === undefined
1171
+ ? `Unknown identifier "${name}".`
1172
+ : `Unknown identifier "${name}"; did you mean "${replacement}"?`, identifier?.start ?? 0, identifier?.end ?? Math.min(1, source.length), replacement);
1173
+ }
1174
+ }
1175
+ return {
1176
+ ok: true,
1177
+ instruction: instructionFor(expression),
1178
+ identifiers: names.identifiers,
1179
+ assignments: names.assignments,
1180
+ diagnostics: [],
1181
+ };
1182
+ }
1183
+ catch (error) {
1184
+ const issue = error instanceof ExpressionFailure
1185
+ ? error.issue
1186
+ : {
1187
+ code: 'HMX1022',
1188
+ message: 'Expression could not be compiled safely.',
1189
+ start: 0,
1190
+ end: Math.min(1, source.length),
1191
+ };
1192
+ return { ok: false, diagnostics: [issueDiagnostic(issue, context)] };
1193
+ }
1194
+ }
1195
+ function findAssignment(expression, name) {
1196
+ return findExpression(expression, (node) => node.type === 'assignment' && node.name === name);
1197
+ }
1198
+ function findIdentifier(expression, name) {
1199
+ return findExpression(expression, (node) => node.type === 'identifier' && node.name === name);
1200
+ }
1201
+ function findExpression(expression, matches) {
1202
+ const stack = [expression];
1203
+ while (stack.length > 0) {
1204
+ const node = stack.pop();
1205
+ if (node === undefined)
1206
+ continue;
1207
+ if (matches(node))
1208
+ return node;
1209
+ if (node.type === 'unary')
1210
+ stack.push(node.argument);
1211
+ else if (node.type === 'binary')
1212
+ stack.push(node.left, node.right);
1213
+ else if (node.type === 'conditional')
1214
+ stack.push(node.test, node.consequent, node.alternate);
1215
+ else if (node.type === 'member') {
1216
+ stack.push(node.object);
1217
+ if (node.computed)
1218
+ stack.push(node.property);
1219
+ }
1220
+ else if (node.type === 'array')
1221
+ stack.push(...node.elements);
1222
+ else if (node.type === 'object')
1223
+ stack.push(...node.properties.map(({ value }) => value));
1224
+ else if (node.type === 'group')
1225
+ stack.push(node.expression);
1226
+ else if (node.type === 'assignment')
1227
+ stack.push(node.value);
1228
+ }
1229
+ return undefined;
1230
+ }
1231
+ /** Returns whether a declaration name can be referenced by the expression grammar. */
1232
+ export function isExpressionIdentifier(name) {
1233
+ if (name.length === 0 ||
1234
+ !isIdentifierStart(name[0]) ||
1235
+ KEYWORDS.has(name) ||
1236
+ name === 'true' ||
1237
+ name === 'false' ||
1238
+ name === 'null' ||
1239
+ isForbiddenAttributeName(name)) {
1240
+ return false;
1241
+ }
1242
+ for (let index = 1; index < name.length; index += 1) {
1243
+ if (!isIdentifierContinue(name[index]))
1244
+ return false;
1245
+ }
1246
+ return true;
1247
+ }
1248
+ //# sourceMappingURL=expression.js.map