@paradoc/expr 0.3.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/dist/index.js ADDED
@@ -0,0 +1,1659 @@
1
+ // src/types.ts
2
+ var T = {
3
+ string: { kind: "string" },
4
+ number: { kind: "number" },
5
+ boolean: { kind: "boolean" },
6
+ date: { kind: "date" },
7
+ datetime: { kind: "datetime" },
8
+ time: { kind: "time" },
9
+ duration: { kind: "duration" },
10
+ money: { kind: "money" },
11
+ object: { kind: "object" },
12
+ null: { kind: "null" },
13
+ unknown: { kind: "unknown" },
14
+ array: (element) => ({ kind: "array", element })
15
+ };
16
+ function formatType(t) {
17
+ return t.kind === "array" ? `array<${formatType(t.element)}>` : t.kind;
18
+ }
19
+ function typesEqual(a, b) {
20
+ if (a.kind === "array" && b.kind === "array") {
21
+ return typesEqual(a.element, b.element);
22
+ }
23
+ return a.kind === b.kind;
24
+ }
25
+
26
+ // src/grammar/grammar.ts
27
+ var KEYWORDS = ["and", "or", "not", "in", "true", "false", "null"];
28
+ var BINARY_OPERATORS = [
29
+ { token: "or", precedence: 1, associativity: "left" },
30
+ { token: "and", precedence: 2, associativity: "left" },
31
+ { token: "==", precedence: 3, associativity: "left" },
32
+ { token: "!=", precedence: 3, associativity: "left" },
33
+ { token: "<", precedence: 4, associativity: "left" },
34
+ { token: "<=", precedence: 4, associativity: "left" },
35
+ { token: ">", precedence: 4, associativity: "left" },
36
+ { token: ">=", precedence: 4, associativity: "left" },
37
+ { token: "in", precedence: 5, associativity: "left" },
38
+ { token: "+", precedence: 6, associativity: "left" },
39
+ { token: "-", precedence: 6, associativity: "left" },
40
+ { token: "*", precedence: 7, associativity: "left" },
41
+ { token: "/", precedence: 7, associativity: "left" },
42
+ { token: "%", precedence: 7, associativity: "left" }
43
+ ];
44
+ var UNARY_OPERATORS = ["not", "!", "-"];
45
+ var FORBIDDEN_OPERATORS = {
46
+ "=": "Use '==' for equality; '=' (assignment) is not allowed.",
47
+ "||": "Use 'or' for logic and '+' to concatenate strings; '||' is not supported.",
48
+ "&&": "Use 'and'; '&&' is not supported."
49
+ };
50
+ var KEYWORD_SET = new Set(KEYWORDS);
51
+ var BINARY_OPERATOR_TOKENS = new Set(
52
+ BINARY_OPERATORS.map((o) => o.token)
53
+ );
54
+
55
+ // src/registry/registry.ts
56
+ var fixed = (type) => ({ kind: "fixed", type });
57
+ var DEFAULT_SIGNATURES = [
58
+ // --- string ---
59
+ {
60
+ name: "contains",
61
+ category: "string",
62
+ params: [
63
+ { name: "haystack", type: T.unknown },
64
+ { name: "needle", type: T.unknown }
65
+ ],
66
+ returns: fixed(T.boolean),
67
+ deterministic: true
68
+ },
69
+ {
70
+ name: "startsWith",
71
+ category: "string",
72
+ params: [
73
+ { name: "value", type: T.string },
74
+ { name: "prefix", type: T.string }
75
+ ],
76
+ returns: fixed(T.boolean),
77
+ deterministic: true
78
+ },
79
+ {
80
+ name: "endsWith",
81
+ category: "string",
82
+ params: [
83
+ { name: "value", type: T.string },
84
+ { name: "suffix", type: T.string }
85
+ ],
86
+ returns: fixed(T.boolean),
87
+ deterministic: true
88
+ },
89
+ {
90
+ name: "trim",
91
+ category: "string",
92
+ params: [{ name: "value", type: T.string }],
93
+ returns: fixed(T.string),
94
+ deterministic: true
95
+ },
96
+ {
97
+ name: "lower",
98
+ category: "string",
99
+ params: [{ name: "value", type: T.string }],
100
+ returns: fixed(T.string),
101
+ deterministic: true
102
+ },
103
+ {
104
+ name: "upper",
105
+ category: "string",
106
+ params: [{ name: "value", type: T.string }],
107
+ returns: fixed(T.string),
108
+ deterministic: true
109
+ },
110
+ {
111
+ name: "matches",
112
+ category: "string",
113
+ params: [
114
+ { name: "value", type: T.string },
115
+ { name: "pattern", type: T.string }
116
+ ],
117
+ returns: fixed(T.boolean),
118
+ deterministic: true
119
+ },
120
+ {
121
+ name: "isEmpty",
122
+ category: "string",
123
+ params: [{ name: "value", type: T.unknown }],
124
+ returns: fixed(T.boolean),
125
+ deterministic: true
126
+ },
127
+ {
128
+ name: "isNotEmpty",
129
+ category: "string",
130
+ params: [{ name: "value", type: T.unknown }],
131
+ returns: fixed(T.boolean),
132
+ deterministic: true
133
+ },
134
+ {
135
+ name: "length",
136
+ category: "string",
137
+ params: [{ name: "value", type: T.unknown }],
138
+ returns: fixed(T.number),
139
+ deterministic: true
140
+ },
141
+ {
142
+ name: "coalesce",
143
+ category: "logical",
144
+ params: [{ name: "value", type: T.unknown }],
145
+ variadic: true,
146
+ returns: { kind: "commonOfArgs" },
147
+ deterministic: true
148
+ },
149
+ // --- number (decimal-aware) ---
150
+ {
151
+ name: "round",
152
+ category: "number",
153
+ params: [
154
+ { name: "value", type: T.number },
155
+ { name: "digits", type: T.number, optional: true }
156
+ ],
157
+ returns: fixed(T.number),
158
+ deterministic: true
159
+ },
160
+ {
161
+ name: "floor",
162
+ category: "number",
163
+ params: [{ name: "value", type: T.number }],
164
+ returns: fixed(T.number),
165
+ deterministic: true
166
+ },
167
+ {
168
+ name: "ceil",
169
+ category: "number",
170
+ params: [{ name: "value", type: T.number }],
171
+ returns: fixed(T.number),
172
+ deterministic: true
173
+ },
174
+ {
175
+ name: "abs",
176
+ category: "number",
177
+ params: [{ name: "value", type: T.number }],
178
+ returns: fixed(T.number),
179
+ deterministic: true
180
+ },
181
+ {
182
+ name: "min",
183
+ category: "number",
184
+ params: [{ name: "value", type: T.number }],
185
+ variadic: true,
186
+ returns: fixed(T.number),
187
+ deterministic: true
188
+ },
189
+ {
190
+ name: "max",
191
+ category: "number",
192
+ params: [{ name: "value", type: T.number }],
193
+ variadic: true,
194
+ returns: fixed(T.number),
195
+ deterministic: true
196
+ },
197
+ // --- date (host-injected as-of clock) ---
198
+ {
199
+ name: "today",
200
+ category: "date",
201
+ params: [],
202
+ returns: fixed(T.date),
203
+ hostInjected: true,
204
+ deterministic: true
205
+ },
206
+ {
207
+ name: "now",
208
+ category: "date",
209
+ params: [],
210
+ returns: fixed(T.datetime),
211
+ hostInjected: true,
212
+ deterministic: true
213
+ },
214
+ {
215
+ name: "yearsBetween",
216
+ category: "date",
217
+ params: [
218
+ { name: "from", type: T.date },
219
+ { name: "to", type: T.date }
220
+ ],
221
+ returns: fixed(T.number),
222
+ deterministic: true
223
+ },
224
+ {
225
+ name: "dateDiff",
226
+ category: "date",
227
+ params: [
228
+ { name: "from", type: T.date },
229
+ { name: "to", type: T.date },
230
+ { name: "unit", type: T.string, optional: true }
231
+ ],
232
+ returns: fixed(T.number),
233
+ deterministic: true
234
+ },
235
+ {
236
+ name: "addDuration",
237
+ category: "date",
238
+ params: [
239
+ { name: "date", type: T.date },
240
+ { name: "duration", type: T.duration }
241
+ ],
242
+ returns: fixed(T.date),
243
+ deterministic: true
244
+ },
245
+ {
246
+ name: "addDays",
247
+ category: "date",
248
+ params: [
249
+ { name: "date", type: T.date },
250
+ { name: "days", type: T.number }
251
+ ],
252
+ returns: fixed(T.date),
253
+ deterministic: true
254
+ },
255
+ // --- domain: party / witness (host-injected party context) ---
256
+ {
257
+ name: "partyCount",
258
+ category: "domain",
259
+ params: [{ name: "roleId", type: T.string }],
260
+ returns: fixed(T.number),
261
+ hostInjected: true,
262
+ deterministic: true
263
+ },
264
+ {
265
+ name: "signedCount",
266
+ category: "domain",
267
+ params: [{ name: "roleId", type: T.string }],
268
+ returns: fixed(T.number),
269
+ hostInjected: true,
270
+ deterministic: true
271
+ },
272
+ {
273
+ name: "allSigned",
274
+ category: "domain",
275
+ params: [{ name: "roleId", type: T.string }],
276
+ returns: fixed(T.boolean),
277
+ hostInjected: true,
278
+ deterministic: true
279
+ },
280
+ {
281
+ name: "anySigned",
282
+ category: "domain",
283
+ params: [{ name: "roleId", type: T.string }],
284
+ returns: fixed(T.boolean),
285
+ hostInjected: true,
286
+ deterministic: true
287
+ },
288
+ {
289
+ name: "partyType",
290
+ category: "domain",
291
+ params: [{ name: "roleId", type: T.string }],
292
+ returns: fixed(T.string),
293
+ hostInjected: true,
294
+ deterministic: true
295
+ },
296
+ {
297
+ name: "witnessCount",
298
+ category: "domain",
299
+ params: [],
300
+ returns: fixed(T.number),
301
+ hostInjected: true,
302
+ deterministic: true
303
+ },
304
+ {
305
+ name: "allWitnessesSigned",
306
+ category: "domain",
307
+ params: [],
308
+ returns: fixed(T.boolean),
309
+ hostInjected: true,
310
+ deterministic: true
311
+ },
312
+ {
313
+ name: "anyWitnessSigned",
314
+ category: "domain",
315
+ params: [],
316
+ returns: fixed(T.boolean),
317
+ hostInjected: true,
318
+ deterministic: true
319
+ }
320
+ ];
321
+ function buildRegistry(extra = []) {
322
+ const map = /* @__PURE__ */ new Map();
323
+ for (const sig of [...DEFAULT_SIGNATURES, ...extra]) {
324
+ map.set(sig.name, sig);
325
+ }
326
+ return {
327
+ signatures: map,
328
+ has: (name) => map.has(name),
329
+ get: (name) => map.get(name),
330
+ names: () => [...map.keys()]
331
+ };
332
+ }
333
+
334
+ // src/parser/lexer.ts
335
+ var LexError = class extends Error {
336
+ constructor(message, position) {
337
+ super(message);
338
+ this.position = position;
339
+ this.name = "LexError";
340
+ }
341
+ position;
342
+ };
343
+ var TWO_CHAR_OPERATORS = /* @__PURE__ */ new Set(["==", "!=", "<=", ">=", "||", "&&"]);
344
+ function isDigit(ch) {
345
+ return ch !== void 0 && ch >= "0" && ch <= "9";
346
+ }
347
+ function isIdentStart(ch) {
348
+ return ch !== void 0 && (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_");
349
+ }
350
+ function isIdentPart(ch) {
351
+ return isIdentStart(ch) || isDigit(ch);
352
+ }
353
+ function tokenize(source) {
354
+ const tokens = [];
355
+ let offset = 0;
356
+ let line = 1;
357
+ let column = 1;
358
+ const pos = () => ({ offset, line, column });
359
+ const advance = (n = 1) => {
360
+ for (let i = 0; i < n; i++) {
361
+ if (source[offset] === "\n") {
362
+ line++;
363
+ column = 1;
364
+ } else {
365
+ column++;
366
+ }
367
+ offset++;
368
+ }
369
+ };
370
+ const push = (type, value, start) => {
371
+ tokens.push({ type, value, span: { start, end: pos() } });
372
+ };
373
+ while (offset < source.length) {
374
+ const ch = source[offset];
375
+ if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
376
+ advance();
377
+ continue;
378
+ }
379
+ const start = pos();
380
+ if (isDigit(ch)) {
381
+ let value = "";
382
+ while (offset < source.length && isDigit(source[offset])) {
383
+ value += source[offset];
384
+ advance();
385
+ }
386
+ if (source[offset] === "." && isDigit(source[offset + 1])) {
387
+ value += ".";
388
+ advance();
389
+ while (offset < source.length && isDigit(source[offset])) {
390
+ value += source[offset];
391
+ advance();
392
+ }
393
+ }
394
+ push("number", value, start);
395
+ continue;
396
+ }
397
+ if (ch === '"' || ch === "'") {
398
+ const quote = ch;
399
+ advance();
400
+ let value = "";
401
+ let closed = false;
402
+ while (offset < source.length) {
403
+ const c = source[offset];
404
+ if (c === "\\") {
405
+ const next = source[offset + 1];
406
+ value += next === void 0 ? "\\" : unescape(next);
407
+ advance(2);
408
+ continue;
409
+ }
410
+ if (c === quote) {
411
+ advance();
412
+ closed = true;
413
+ break;
414
+ }
415
+ if (c === "\n") break;
416
+ value += c;
417
+ advance();
418
+ }
419
+ if (!closed) throw new LexError("Unterminated string literal", start);
420
+ push("string", value, start);
421
+ continue;
422
+ }
423
+ if (isIdentStart(ch)) {
424
+ let value = "";
425
+ while (offset < source.length && isIdentPart(source[offset])) {
426
+ value += source[offset];
427
+ advance();
428
+ }
429
+ push(KEYWORD_SET.has(value) ? "keyword" : "identifier", value, start);
430
+ continue;
431
+ }
432
+ const single = {
433
+ "(": "lparen",
434
+ ")": "rparen",
435
+ "[": "lbracket",
436
+ "]": "rbracket",
437
+ ",": "comma",
438
+ ".": "dot",
439
+ "?": "question",
440
+ ":": "colon"
441
+ };
442
+ const punct = single[ch];
443
+ if (punct) {
444
+ advance();
445
+ push(punct, ch, start);
446
+ continue;
447
+ }
448
+ const two = source.slice(offset, offset + 2);
449
+ if (TWO_CHAR_OPERATORS.has(two)) {
450
+ advance(2);
451
+ push("operator", two, start);
452
+ continue;
453
+ }
454
+ if ("=<>+-*/%!".includes(ch)) {
455
+ advance();
456
+ push("operator", ch, start);
457
+ continue;
458
+ }
459
+ throw new LexError(`Unexpected character '${ch}'`, start);
460
+ }
461
+ tokens.push({ type: "eof", value: "", span: { start: pos(), end: pos() } });
462
+ return tokens;
463
+ }
464
+ function unescape(ch) {
465
+ switch (ch) {
466
+ case "n":
467
+ return "\n";
468
+ case "t":
469
+ return " ";
470
+ case "r":
471
+ return "\r";
472
+ default:
473
+ return ch;
474
+ }
475
+ }
476
+
477
+ // src/parser/parser.ts
478
+ var ParseError = class extends Error {
479
+ constructor(diagnostic) {
480
+ super(diagnostic.message);
481
+ this.diagnostic = diagnostic;
482
+ this.name = "ParseError";
483
+ }
484
+ diagnostic;
485
+ };
486
+ var OPERATOR_INFO = new Map(
487
+ BINARY_OPERATORS.map((o) => [o.token, o])
488
+ );
489
+ var ARITHMETIC = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%"]);
490
+ var COMPARISON = /* @__PURE__ */ new Set(["==", "!=", "<", "<=", ">", ">="]);
491
+ function spanBetween(a, b) {
492
+ return { start: a.start, end: b.end };
493
+ }
494
+ var Parser = class {
495
+ constructor(tokens) {
496
+ this.tokens = tokens;
497
+ }
498
+ tokens;
499
+ pos = 0;
500
+ peek(ahead = 0) {
501
+ return this.tokens[Math.min(this.pos + ahead, this.tokens.length - 1)];
502
+ }
503
+ next() {
504
+ const t = this.tokens[this.pos];
505
+ if (this.pos < this.tokens.length - 1) this.pos++;
506
+ return t;
507
+ }
508
+ fail(message, span, code = "syntax") {
509
+ throw new ParseError({ severity: "error", code, message, span });
510
+ }
511
+ parse() {
512
+ if (this.peek().type === "eof") {
513
+ this.fail("Empty expression", this.peek().span);
514
+ }
515
+ const expr = this.parseExpression();
516
+ const tok = this.peek();
517
+ if (tok.type !== "eof") {
518
+ const forbidden = FORBIDDEN_OPERATORS[tok.value];
519
+ if (forbidden) this.fail(forbidden, tok.span, "forbidden-operator");
520
+ this.fail(`Unexpected '${tok.value || "end of input"}'`, tok.span);
521
+ }
522
+ return expr;
523
+ }
524
+ parseExpression() {
525
+ return this.parseTernary();
526
+ }
527
+ parseTernary() {
528
+ const test = this.parseBinary(0);
529
+ if (this.peek().type === "question") {
530
+ this.next();
531
+ const consequent = this.parseExpression();
532
+ if (this.peek().type !== "colon") {
533
+ this.fail("Expected ':' in ternary expression", this.peek().span);
534
+ }
535
+ this.next();
536
+ const alternate = this.parseExpression();
537
+ return {
538
+ kind: "Conditional",
539
+ test,
540
+ consequent,
541
+ alternate,
542
+ span: spanBetween(test.span, alternate.span)
543
+ };
544
+ }
545
+ return test;
546
+ }
547
+ /** Precedence climbing, with `not in` and forbidden-operator handling. */
548
+ parseBinary(minPrec) {
549
+ let left = this.parseUnary();
550
+ for (; ; ) {
551
+ const tok = this.peek();
552
+ if (tok.type === "keyword" && tok.value === "not" && this.peek(1).value === "in") {
553
+ const info2 = OPERATOR_INFO.get("in");
554
+ if (info2.precedence < minPrec) break;
555
+ this.next();
556
+ this.next();
557
+ const right2 = this.parseBinary(info2.precedence + 1);
558
+ left = {
559
+ kind: "Membership",
560
+ negated: true,
561
+ element: left,
562
+ collection: right2,
563
+ span: spanBetween(left.span, right2.span)
564
+ };
565
+ continue;
566
+ }
567
+ const forbidden = FORBIDDEN_OPERATORS[tok.value];
568
+ if (forbidden && (tok.type === "operator" || tok.type === "keyword")) {
569
+ this.fail(forbidden, tok.span, "forbidden-operator");
570
+ }
571
+ const info = OPERATOR_INFO.get(tok.value);
572
+ if (!info || tok.type !== "operator" && tok.type !== "keyword" || info.precedence < minPrec) {
573
+ break;
574
+ }
575
+ this.next();
576
+ const nextMin = info.associativity === "left" ? info.precedence + 1 : info.precedence;
577
+ const right = this.parseBinary(nextMin);
578
+ left = this.makeBinary(info.token, left, right);
579
+ }
580
+ return left;
581
+ }
582
+ makeBinary(op, left, right) {
583
+ const span = spanBetween(left.span, right.span);
584
+ if (op === "and" || op === "or") {
585
+ return { kind: "Logical", op, left, right, span };
586
+ }
587
+ if (op === "in") {
588
+ return { kind: "Membership", negated: false, element: left, collection: right, span };
589
+ }
590
+ if (ARITHMETIC.has(op) || COMPARISON.has(op)) {
591
+ return { kind: "Binary", op, left, right, span };
592
+ }
593
+ this.fail(`Unknown operator '${op}'`, span);
594
+ }
595
+ parseUnary() {
596
+ const tok = this.peek();
597
+ const isNot = tok.type === "keyword" && tok.value === "not";
598
+ const isBang = tok.type === "operator" && tok.value === "!";
599
+ const isNeg = tok.type === "operator" && tok.value === "-";
600
+ if (isNot || isBang || isNeg) {
601
+ this.next();
602
+ const operand = this.parseUnary();
603
+ return {
604
+ kind: "Unary",
605
+ op: isNeg ? "neg" : "not",
606
+ operand,
607
+ span: spanBetween(tok.span, operand.span)
608
+ };
609
+ }
610
+ return this.parsePostfix();
611
+ }
612
+ parsePostfix() {
613
+ let expr = this.parsePrimary();
614
+ for (; ; ) {
615
+ const tok = this.peek();
616
+ if (tok.type === "dot") {
617
+ this.next();
618
+ const prop = this.peek();
619
+ if (prop.type !== "identifier") {
620
+ this.fail('Expected a property name after "."', prop.span);
621
+ }
622
+ this.next();
623
+ expr = { kind: "Member", object: expr, property: prop.value, span: spanBetween(expr.span, prop.span) };
624
+ continue;
625
+ }
626
+ if (tok.type === "lbracket") {
627
+ this.next();
628
+ const index = this.parseExpression();
629
+ const close = this.peek();
630
+ if (close.type !== "rbracket") this.fail('Expected "]"', close.span);
631
+ this.next();
632
+ expr = { kind: "Index", object: expr, index, span: spanBetween(expr.span, close.span) };
633
+ continue;
634
+ }
635
+ break;
636
+ }
637
+ return expr;
638
+ }
639
+ parsePrimary() {
640
+ const tok = this.peek();
641
+ switch (tok.type) {
642
+ case "number":
643
+ this.next();
644
+ return { kind: "NumberLiteral", value: tok.value, span: tok.span };
645
+ case "string":
646
+ this.next();
647
+ return { kind: "StringLiteral", value: tok.value, span: tok.span };
648
+ case "keyword":
649
+ if (tok.value === "true" || tok.value === "false") {
650
+ this.next();
651
+ return { kind: "BooleanLiteral", value: tok.value === "true", span: tok.span };
652
+ }
653
+ if (tok.value === "null") {
654
+ this.next();
655
+ return { kind: "NullLiteral", span: tok.span };
656
+ }
657
+ this.fail(`Unexpected keyword '${tok.value}'`, tok.span);
658
+ break;
659
+ case "identifier": {
660
+ this.next();
661
+ if (this.peek().type === "lparen") {
662
+ return this.parseCall(tok.value, tok.span);
663
+ }
664
+ return { kind: "Identifier", name: tok.value, span: tok.span };
665
+ }
666
+ case "lparen": {
667
+ this.next();
668
+ const inner = this.parseExpression();
669
+ const close = this.peek();
670
+ if (close.type !== "rparen") this.fail('Expected ")"', close.span);
671
+ this.next();
672
+ return inner;
673
+ }
674
+ case "lbracket":
675
+ return this.parseArrayLiteral(tok.span);
676
+ default: {
677
+ const forbidden = FORBIDDEN_OPERATORS[tok.value];
678
+ if (forbidden) this.fail(forbidden, tok.span, "forbidden-operator");
679
+ this.fail(`Unexpected '${tok.value || "end of input"}'`, tok.span);
680
+ }
681
+ }
682
+ }
683
+ parseCall(callee, calleeSpan) {
684
+ this.next();
685
+ const args = [];
686
+ if (this.peek().type !== "rparen") {
687
+ for (; ; ) {
688
+ args.push(this.parseExpression());
689
+ if (this.peek().type === "comma") {
690
+ this.next();
691
+ continue;
692
+ }
693
+ break;
694
+ }
695
+ }
696
+ const close = this.peek();
697
+ if (close.type !== "rparen") this.fail('Expected ")" to close arguments', close.span);
698
+ this.next();
699
+ return { kind: "Call", callee, args, span: spanBetween(calleeSpan, close.span) };
700
+ }
701
+ parseArrayLiteral(openSpan) {
702
+ this.next();
703
+ const elements = [];
704
+ if (this.peek().type !== "rbracket") {
705
+ for (; ; ) {
706
+ elements.push(this.parseExpression());
707
+ if (this.peek().type === "comma") {
708
+ this.next();
709
+ continue;
710
+ }
711
+ break;
712
+ }
713
+ }
714
+ const close = this.peek();
715
+ if (close.type !== "rbracket") this.fail('Expected "]" to close array', close.span);
716
+ this.next();
717
+ return { kind: "ArrayLiteral", elements, span: spanBetween(openSpan, close.span) };
718
+ }
719
+ };
720
+ function parse(source) {
721
+ try {
722
+ const tokens = tokenize(source);
723
+ const ast = new Parser(tokens).parse();
724
+ return { ast, errors: [] };
725
+ } catch (e) {
726
+ if (e instanceof ParseError) {
727
+ return { ast: null, errors: [e.diagnostic] };
728
+ }
729
+ if (e instanceof LexError) {
730
+ const span = { start: e.position, end: e.position };
731
+ return { ast: null, errors: [{ severity: "error", code: "syntax", message: e.message, span }] };
732
+ }
733
+ throw e;
734
+ }
735
+ }
736
+ function parseOrThrow(source) {
737
+ const { ast, errors } = parse(source);
738
+ if (!ast) {
739
+ const first = errors[0];
740
+ throw new Error(first ? `${first.message} (at ${first.span.start.line}:${first.span.start.column})` : "Parse failed");
741
+ }
742
+ return ast;
743
+ }
744
+
745
+ // src/analyze/references.ts
746
+ function staticPath(node) {
747
+ if (node.kind === "Identifier") return node.name;
748
+ if (node.kind === "Member") {
749
+ const base = staticPath(node.object);
750
+ return base === null ? null : `${base}.${node.property}`;
751
+ }
752
+ return null;
753
+ }
754
+ function extractReferences(ast) {
755
+ const paths = /* @__PURE__ */ new Set();
756
+ let fullyStatic = true;
757
+ const visit = (node) => {
758
+ switch (node.kind) {
759
+ case "Identifier":
760
+ paths.add(node.name);
761
+ return;
762
+ case "Member": {
763
+ const path = staticPath(node);
764
+ if (path !== null) {
765
+ paths.add(path);
766
+ } else {
767
+ visit(node.object);
768
+ }
769
+ return;
770
+ }
771
+ case "Index":
772
+ fullyStatic = false;
773
+ visit(node.object);
774
+ visit(node.index);
775
+ return;
776
+ case "Unary":
777
+ visit(node.operand);
778
+ return;
779
+ case "Binary":
780
+ case "Logical":
781
+ visit(node.left);
782
+ visit(node.right);
783
+ return;
784
+ case "Membership":
785
+ visit(node.element);
786
+ visit(node.collection);
787
+ return;
788
+ case "Conditional":
789
+ visit(node.test);
790
+ visit(node.consequent);
791
+ visit(node.alternate);
792
+ return;
793
+ case "Call":
794
+ for (const arg2 of node.args) visit(arg2);
795
+ return;
796
+ case "ArrayLiteral":
797
+ for (const el of node.elements) visit(el);
798
+ return;
799
+ case "NumberLiteral":
800
+ case "StringLiteral":
801
+ case "BooleanLiteral":
802
+ case "NullLiteral":
803
+ return;
804
+ }
805
+ };
806
+ visit(ast);
807
+ return { paths: [...paths].sort(), fullyStatic };
808
+ }
809
+
810
+ // src/decimal/decimal.ts
811
+ var DIV_SCALE = 20;
812
+ var DivisionByZeroError = class extends Error {
813
+ constructor() {
814
+ super("Division by zero");
815
+ this.name = "DivisionByZeroError";
816
+ }
817
+ };
818
+ var DECIMAL_RE = /^-?\d+(\.\d+)?$/;
819
+ function pow10(n) {
820
+ return 10n ** BigInt(n);
821
+ }
822
+ function absBig(x) {
823
+ return x < 0n ? -x : x;
824
+ }
825
+ var Decimal = class _Decimal {
826
+ constructor(n, scale) {
827
+ this.n = n;
828
+ this.scale = scale;
829
+ }
830
+ n;
831
+ scale;
832
+ static fromString(s) {
833
+ const str = s.trim();
834
+ if (!DECIMAL_RE.test(str)) {
835
+ throw new RangeError(`Invalid decimal literal: ${JSON.stringify(s)}`);
836
+ }
837
+ const negative = str.startsWith("-");
838
+ const body = negative ? str.slice(1) : str;
839
+ const dot = body.indexOf(".");
840
+ if (dot === -1) {
841
+ return new _Decimal(BigInt((negative ? "-" : "") + body), 0);
842
+ }
843
+ const intPart = body.slice(0, dot);
844
+ const fracPart = body.slice(dot + 1);
845
+ const digits = (intPart + fracPart).replace(/^0+(?=\d)/, "");
846
+ const mag = BigInt(digits === "" ? "0" : digits);
847
+ return new _Decimal(negative ? -mag : mag, fracPart.length).trim();
848
+ }
849
+ static fromInt(i) {
850
+ return new _Decimal(typeof i === "bigint" ? i : BigInt(Math.trunc(i)), 0);
851
+ }
852
+ static ZERO = new _Decimal(0n, 0);
853
+ /** Bring two decimals to a common scale, returning aligned magnitudes. */
854
+ static align(a, b) {
855
+ if (a.scale === b.scale) return { an: a.n, bn: b.n, scale: a.scale };
856
+ const scale = Math.max(a.scale, b.scale);
857
+ return {
858
+ an: a.n * pow10(scale - a.scale),
859
+ bn: b.n * pow10(scale - b.scale),
860
+ scale
861
+ };
862
+ }
863
+ add(b) {
864
+ const { an, bn, scale } = _Decimal.align(this, b);
865
+ return new _Decimal(an + bn, scale).trim();
866
+ }
867
+ sub(b) {
868
+ const { an, bn, scale } = _Decimal.align(this, b);
869
+ return new _Decimal(an - bn, scale).trim();
870
+ }
871
+ mul(b) {
872
+ return new _Decimal(this.n * b.n, this.scale + b.scale).trim();
873
+ }
874
+ div(b, rm = "half-up") {
875
+ if (b.n === 0n) throw new DivisionByZeroError();
876
+ const exp = DIV_SCALE + b.scale - this.scale;
877
+ let num = this.n;
878
+ let den = b.n;
879
+ if (exp >= 0) num *= pow10(exp);
880
+ else den *= pow10(-exp);
881
+ return new _Decimal(roundDiv(num, den, rm), DIV_SCALE).trim();
882
+ }
883
+ mod(b) {
884
+ if (b.n === 0n) throw new DivisionByZeroError();
885
+ const { an, bn, scale } = _Decimal.align(this, b);
886
+ return new _Decimal(an % bn, scale).trim();
887
+ }
888
+ neg() {
889
+ return new _Decimal(-this.n, this.scale);
890
+ }
891
+ abs() {
892
+ return this.n < 0n ? this.neg() : this;
893
+ }
894
+ /** -1, 0, or 1. */
895
+ cmp(b) {
896
+ const { an, bn } = _Decimal.align(this, b);
897
+ return an < bn ? -1 : an > bn ? 1 : 0;
898
+ }
899
+ eq(b) {
900
+ return this.cmp(b) === 0;
901
+ }
902
+ lt(b) {
903
+ return this.cmp(b) < 0;
904
+ }
905
+ lte(b) {
906
+ return this.cmp(b) <= 0;
907
+ }
908
+ gt(b) {
909
+ return this.cmp(b) > 0;
910
+ }
911
+ gte(b) {
912
+ return this.cmp(b) >= 0;
913
+ }
914
+ isZero() {
915
+ return this.n === 0n;
916
+ }
917
+ /** Round to `digits` decimal places (default 0). Negative/fractional
918
+ * digits are clamped to a valid non-negative integer scale. */
919
+ round(digits = 0, rm = "half-up") {
920
+ return this.toScale(Math.max(0, Math.trunc(digits)), rm);
921
+ }
922
+ floor() {
923
+ return this.toScale(0, "floor");
924
+ }
925
+ ceil() {
926
+ return this.toScale(0, "ceil");
927
+ }
928
+ toScale(target, rm) {
929
+ if (target >= this.scale) {
930
+ return new _Decimal(this.n * pow10(target - this.scale), target).trim();
931
+ }
932
+ const factor = pow10(this.scale - target);
933
+ return new _Decimal(roundDiv(this.n, factor, rm), target).trim();
934
+ }
935
+ /** Remove trailing fractional zeros, keeping the value identical. */
936
+ trim() {
937
+ let { n, scale } = this;
938
+ while (scale > 0 && n % 10n === 0n) {
939
+ n /= 10n;
940
+ scale--;
941
+ }
942
+ return new _Decimal(n, scale);
943
+ }
944
+ toString() {
945
+ const neg = this.n < 0n;
946
+ const digits = absBig(this.n).toString();
947
+ if (this.scale === 0) return (neg ? "-" : "") + digits;
948
+ const padded = digits.padStart(this.scale + 1, "0");
949
+ const cut = padded.length - this.scale;
950
+ return `${neg ? "-" : ""}${padded.slice(0, cut)}.${padded.slice(cut)}`;
951
+ }
952
+ /** Lossy conversion to a JS number, for interop only. */
953
+ toNumber() {
954
+ return Number(this.toString());
955
+ }
956
+ toJSON() {
957
+ return this.toString();
958
+ }
959
+ };
960
+ function roundDiv(num, den, rm) {
961
+ if (den < 0n) {
962
+ num = -num;
963
+ den = -den;
964
+ }
965
+ const q = num / den;
966
+ const r = num % den;
967
+ if (r === 0n) return q;
968
+ const negative = num < 0n;
969
+ switch (rm) {
970
+ case "down":
971
+ return q;
972
+ case "floor":
973
+ return negative ? q - 1n : q;
974
+ case "ceil":
975
+ return negative ? q : q + 1n;
976
+ case "half-up": {
977
+ if (absBig(r) * 2n >= den) return negative ? q - 1n : q + 1n;
978
+ return q;
979
+ }
980
+ }
981
+ }
982
+
983
+ // src/eval/values.ts
984
+ var NULL = { kind: "null" };
985
+ var Values = {
986
+ number: (d) => ({ kind: "number", value: d }),
987
+ num: (s) => ({ kind: "number", value: Decimal.fromString(s) }),
988
+ string: (s) => ({ kind: "string", value: s }),
989
+ boolean: (b) => ({ kind: "boolean", value: b }),
990
+ null: NULL,
991
+ array: (v) => ({ kind: "array", value: v }),
992
+ object: (entries) => ({
993
+ kind: "object",
994
+ value: new Map(entries)
995
+ })
996
+ };
997
+ function numberToDecimalString(n) {
998
+ if (!Number.isFinite(n)) throw new RangeError("Cannot convert a non-finite number");
999
+ if (Number.isInteger(n)) return n.toString();
1000
+ const s = n.toString();
1001
+ if (s.includes("e") || s.includes("E")) {
1002
+ return n.toFixed(20).replace(/0+$/, "").replace(/\.$/, "");
1003
+ }
1004
+ return s;
1005
+ }
1006
+ function toValue(js) {
1007
+ if (js === null || js === void 0) return NULL;
1008
+ switch (typeof js) {
1009
+ case "boolean":
1010
+ return Values.boolean(js);
1011
+ case "number":
1012
+ return Values.number(Decimal.fromString(numberToDecimalString(js)));
1013
+ case "bigint":
1014
+ return Values.number(Decimal.fromInt(js));
1015
+ case "string":
1016
+ return Values.string(js);
1017
+ case "object": {
1018
+ if (Array.isArray(js)) return Values.array(js.map(toValue));
1019
+ const entries = [];
1020
+ for (const [k, v] of Object.entries(js)) {
1021
+ entries.push([k, toValue(v)]);
1022
+ }
1023
+ return Values.object(entries);
1024
+ }
1025
+ default:
1026
+ return NULL;
1027
+ }
1028
+ }
1029
+ function truthy(v) {
1030
+ switch (v.kind) {
1031
+ case "null":
1032
+ return false;
1033
+ case "boolean":
1034
+ return v.value;
1035
+ case "number":
1036
+ return !v.value.isZero();
1037
+ case "string":
1038
+ return v.value.length > 0;
1039
+ case "array":
1040
+ return v.value.length > 0;
1041
+ case "object":
1042
+ return true;
1043
+ }
1044
+ }
1045
+ function valueToString(v) {
1046
+ switch (v.kind) {
1047
+ case "null":
1048
+ return "";
1049
+ case "boolean":
1050
+ return v.value ? "true" : "false";
1051
+ case "number":
1052
+ return v.value.toString();
1053
+ case "string":
1054
+ return v.value;
1055
+ case "array":
1056
+ return v.value.map(valueToString).join(",");
1057
+ case "object":
1058
+ return "[object]";
1059
+ }
1060
+ }
1061
+ function valueEquals(a, b) {
1062
+ if (a.kind !== b.kind) return false;
1063
+ switch (a.kind) {
1064
+ case "null":
1065
+ return true;
1066
+ case "boolean":
1067
+ return a.value === b.value;
1068
+ case "string":
1069
+ return a.value === b.value;
1070
+ case "number":
1071
+ return a.value.eq(b.value);
1072
+ case "array": {
1073
+ const bv = b.value;
1074
+ return a.value.length === bv.length && a.value.every((el, i) => valueEquals(el, bv[i]));
1075
+ }
1076
+ case "object": {
1077
+ const bv = b.value;
1078
+ if (a.value.size !== bv.size) return false;
1079
+ for (const [k, v] of a.value) {
1080
+ const other = bv.get(k);
1081
+ if (other === void 0 || !valueEquals(v, other)) return false;
1082
+ }
1083
+ return true;
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ // src/eval/errors.ts
1089
+ var EvaluationError = class extends Error {
1090
+ constructor(code, message) {
1091
+ super(message);
1092
+ this.code = code;
1093
+ this.name = "EvaluationError";
1094
+ }
1095
+ code;
1096
+ };
1097
+
1098
+ // src/eval/context.ts
1099
+ function createContext(data, opts = {}) {
1100
+ const root = toValue(data);
1101
+ const map = root.kind === "object" ? root.value : /* @__PURE__ */ new Map();
1102
+ return {
1103
+ lookup: (name) => map.get(name),
1104
+ asOf: opts.asOf,
1105
+ hostFunctions: opts.hostFunctions
1106
+ };
1107
+ }
1108
+
1109
+ // src/eval/temporal.ts
1110
+ var MS_PER_DAY = 864e5;
1111
+ function parse2(s) {
1112
+ const d = new Date(s);
1113
+ if (Number.isNaN(d.getTime())) {
1114
+ throw new EvaluationError("type-error", `Invalid date: ${JSON.stringify(s)}`);
1115
+ }
1116
+ return d;
1117
+ }
1118
+ function formatDate(d) {
1119
+ return d.toISOString().slice(0, 10);
1120
+ }
1121
+ function diffDays(a, b) {
1122
+ return Math.trunc((parse2(b).getTime() - parse2(a).getTime()) / MS_PER_DAY);
1123
+ }
1124
+ function yearsBetween(from, to) {
1125
+ const a = parse2(from);
1126
+ const b = parse2(to);
1127
+ let years = b.getUTCFullYear() - a.getUTCFullYear();
1128
+ const beforeAnniversary = b.getUTCMonth() < a.getUTCMonth() || b.getUTCMonth() === a.getUTCMonth() && b.getUTCDate() < a.getUTCDate();
1129
+ if (beforeAnniversary) years--;
1130
+ return years;
1131
+ }
1132
+ function dateDiff(from, to, unit = "days") {
1133
+ switch (unit) {
1134
+ case "days":
1135
+ return diffDays(from, to);
1136
+ case "years":
1137
+ return yearsBetween(from, to);
1138
+ case "months": {
1139
+ const a = parse2(from);
1140
+ const b = parse2(to);
1141
+ let months = (b.getUTCFullYear() - a.getUTCFullYear()) * 12 + (b.getUTCMonth() - a.getUTCMonth());
1142
+ if (b.getUTCDate() < a.getUTCDate()) months--;
1143
+ return months;
1144
+ }
1145
+ default:
1146
+ throw new EvaluationError("type-error", `Unknown date unit: ${JSON.stringify(unit)}`);
1147
+ }
1148
+ }
1149
+ function addDays(date, days) {
1150
+ const d = parse2(date);
1151
+ d.setUTCDate(d.getUTCDate() + Math.trunc(days));
1152
+ return formatDate(d);
1153
+ }
1154
+ var DURATION_RE = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?$/;
1155
+ function addDuration(date, duration) {
1156
+ const m = DURATION_RE.exec(duration);
1157
+ if (!m || duration === "P") {
1158
+ throw new EvaluationError("type-error", `Invalid duration: ${JSON.stringify(duration)}`);
1159
+ }
1160
+ const [, y, mo, w, d] = m;
1161
+ const result = parse2(date);
1162
+ if (y) result.setUTCFullYear(result.getUTCFullYear() + Number(y));
1163
+ if (mo) result.setUTCMonth(result.getUTCMonth() + Number(mo));
1164
+ const days = (w ? Number(w) * 7 : 0) + (d ? Number(d) : 0);
1165
+ if (days) result.setUTCDate(result.getUTCDate() + days);
1166
+ return formatDate(result);
1167
+ }
1168
+
1169
+ // src/eval/functions.ts
1170
+ function arg(args, i) {
1171
+ return args[i] ?? NULL;
1172
+ }
1173
+ function asString(v, fn) {
1174
+ if (v.kind === "string") return v.value;
1175
+ throw new EvaluationError("type-error", `${fn} expects a string, got ${v.kind}`);
1176
+ }
1177
+ function asNumber(v, fn) {
1178
+ if (v.kind === "number") return v.value;
1179
+ throw new EvaluationError("type-error", `${fn} expects a number, got ${v.kind}`);
1180
+ }
1181
+ function isEmptyValue(v) {
1182
+ if (v.kind === "null") return true;
1183
+ if (v.kind === "string") return v.value.length === 0;
1184
+ if (v.kind === "array") return v.value.length === 0;
1185
+ return false;
1186
+ }
1187
+ var BUILTIN_IMPLS = {
1188
+ // --- string ---
1189
+ contains: (args) => {
1190
+ const haystack = arg(args, 0);
1191
+ const needle = arg(args, 1);
1192
+ if (haystack.kind === "null") return Values.boolean(false);
1193
+ if (haystack.kind === "string") return Values.boolean(haystack.value.includes(asString(needle, "contains")));
1194
+ if (haystack.kind === "array") return Values.boolean(haystack.value.some((el) => valueEquals(el, needle)));
1195
+ throw new EvaluationError("type-error", `contains expects a string or array, got ${haystack.kind}`);
1196
+ },
1197
+ startsWith: (args) => Values.boolean(asString(arg(args, 0), "startsWith").startsWith(asString(arg(args, 1), "startsWith"))),
1198
+ endsWith: (args) => Values.boolean(asString(arg(args, 0), "endsWith").endsWith(asString(arg(args, 1), "endsWith"))),
1199
+ trim: (args) => Values.string(asString(arg(args, 0), "trim").trim()),
1200
+ lower: (args) => Values.string(asString(arg(args, 0), "lower").toLowerCase()),
1201
+ upper: (args) => Values.string(asString(arg(args, 0), "upper").toUpperCase()),
1202
+ matches: (args) => {
1203
+ const value = asString(arg(args, 0), "matches");
1204
+ const pattern = asString(arg(args, 1), "matches");
1205
+ let re;
1206
+ try {
1207
+ re = new RegExp(pattern);
1208
+ } catch {
1209
+ throw new EvaluationError("type-error", `Invalid pattern: ${JSON.stringify(pattern)}`);
1210
+ }
1211
+ return Values.boolean(re.test(value));
1212
+ },
1213
+ isEmpty: (args) => Values.boolean(isEmptyValue(arg(args, 0))),
1214
+ isNotEmpty: (args) => Values.boolean(!isEmptyValue(arg(args, 0))),
1215
+ length: (args) => {
1216
+ const v = arg(args, 0);
1217
+ if (v.kind === "null") return Values.num("0");
1218
+ if (v.kind === "string") return Values.num(String(v.value.length));
1219
+ if (v.kind === "array") return Values.num(String(v.value.length));
1220
+ throw new EvaluationError("type-error", `length expects a string or array, got ${v.kind}`);
1221
+ },
1222
+ coalesce: (args) => {
1223
+ for (const a of args) if (a.kind !== "null") return a;
1224
+ return NULL;
1225
+ },
1226
+ // --- number (exact decimal) ---
1227
+ round: (args) => {
1228
+ const value = asNumber(arg(args, 0), "round");
1229
+ const digitsArg = args[1];
1230
+ const digits = digitsArg && digitsArg.kind === "number" ? digitsArg.value.toNumber() : 0;
1231
+ return Values.number(value.round(digits));
1232
+ },
1233
+ floor: (args) => Values.number(asNumber(arg(args, 0), "floor").floor()),
1234
+ ceil: (args) => Values.number(asNumber(arg(args, 0), "ceil").ceil()),
1235
+ abs: (args) => Values.number(asNumber(arg(args, 0), "abs").abs()),
1236
+ min: (args) => reduceNumbers(args, "min", (a, b) => a.lte(b) ? a : b),
1237
+ max: (args) => reduceNumbers(args, "max", (a, b) => a.gte(b) ? a : b),
1238
+ // --- date (today/now read the injected as-of clock) ---
1239
+ today: (_args, ctx) => {
1240
+ if (!ctx.asOf) throw new EvaluationError("missing-clock", "today() requires an as-of date in the context");
1241
+ return Values.string(ctx.asOf.date);
1242
+ },
1243
+ now: (_args, ctx) => {
1244
+ if (!ctx.asOf) throw new EvaluationError("missing-clock", "now() requires an as-of datetime in the context");
1245
+ return Values.string(ctx.asOf.datetime);
1246
+ },
1247
+ yearsBetween: (args) => Values.num(String(yearsBetween(asString(arg(args, 0), "yearsBetween"), asString(arg(args, 1), "yearsBetween")))),
1248
+ dateDiff: (args) => {
1249
+ const unitArg = args[2];
1250
+ const unit = unitArg && unitArg.kind === "string" ? unitArg.value : void 0;
1251
+ return Values.num(String(dateDiff(asString(arg(args, 0), "dateDiff"), asString(arg(args, 1), "dateDiff"), unit)));
1252
+ },
1253
+ addDuration: (args) => Values.string(addDuration(asString(arg(args, 0), "addDuration"), asString(arg(args, 1), "addDuration"))),
1254
+ addDays: (args) => Values.string(addDays(asString(arg(args, 0), "addDays"), asNumber(arg(args, 1), "addDays").toNumber()))
1255
+ };
1256
+ function reduceNumbers(args, fn, pick) {
1257
+ if (args.length === 0) throw new EvaluationError("arity", `${fn} requires at least one argument`);
1258
+ let acc = asNumber(arg(args, 0), fn);
1259
+ for (let i = 1; i < args.length; i++) acc = pick(acc, asNumber(arg(args, i), fn));
1260
+ return Values.number(acc);
1261
+ }
1262
+
1263
+ // src/eval/evaluate.ts
1264
+ function evaluate(node, ctx) {
1265
+ switch (node.kind) {
1266
+ case "NumberLiteral":
1267
+ return Values.number(Decimal.fromString(node.value));
1268
+ case "StringLiteral":
1269
+ return Values.string(node.value);
1270
+ case "BooleanLiteral":
1271
+ return Values.boolean(node.value);
1272
+ case "NullLiteral":
1273
+ return NULL;
1274
+ case "ArrayLiteral":
1275
+ return Values.array(node.elements.map((el) => evaluate(el, ctx)));
1276
+ case "Identifier":
1277
+ return ctx.lookup(node.name) ?? NULL;
1278
+ case "Member": {
1279
+ const obj = evaluate(node.object, ctx);
1280
+ if (obj.kind === "object") return obj.value.get(node.property) ?? NULL;
1281
+ return NULL;
1282
+ }
1283
+ case "Index": {
1284
+ const obj = evaluate(node.object, ctx);
1285
+ const idx = evaluate(node.index, ctx);
1286
+ if (obj.kind === "array" && idx.kind === "number") {
1287
+ return obj.value[idx.value.toNumber()] ?? NULL;
1288
+ }
1289
+ return NULL;
1290
+ }
1291
+ case "Unary":
1292
+ return evalUnary(node.op, evaluate(node.operand, ctx));
1293
+ case "Binary":
1294
+ return evalBinary(node.op, node, ctx);
1295
+ case "Logical":
1296
+ return evalLogical(node.op, node, ctx);
1297
+ case "Membership":
1298
+ return evalMembership(node, ctx);
1299
+ case "Conditional":
1300
+ return truthy(evaluate(node.test, ctx)) ? evaluate(node.consequent, ctx) : evaluate(node.alternate, ctx);
1301
+ case "Call":
1302
+ return evalCall(node, ctx);
1303
+ }
1304
+ }
1305
+ function evalUnary(op, operand) {
1306
+ if (op === "not") return Values.boolean(!truthy(operand));
1307
+ if (operand.kind !== "number") {
1308
+ throw new EvaluationError("type-error", `Cannot negate ${operand.kind}`);
1309
+ }
1310
+ return Values.number(operand.value.neg());
1311
+ }
1312
+ function evalBinary(op, node, ctx) {
1313
+ const left = evaluate(node.left, ctx);
1314
+ const right = evaluate(node.right, ctx);
1315
+ switch (op) {
1316
+ case "==":
1317
+ return Values.boolean(valueEquals(left, right));
1318
+ case "!=":
1319
+ return Values.boolean(!valueEquals(left, right));
1320
+ case "<":
1321
+ case "<=":
1322
+ case ">":
1323
+ case ">=":
1324
+ return compareOrdered(op, left, right);
1325
+ case "+":
1326
+ if (left.kind === "number" && right.kind === "number") {
1327
+ return Values.number(left.value.add(right.value));
1328
+ }
1329
+ if (left.kind === "string" || right.kind === "string") {
1330
+ return Values.string(valueToString(left) + valueToString(right));
1331
+ }
1332
+ throw new EvaluationError("type-error", `Cannot add ${left.kind} and ${right.kind}`);
1333
+ case "-":
1334
+ case "*":
1335
+ case "/":
1336
+ case "%":
1337
+ return arithmetic(op, left, right);
1338
+ default:
1339
+ throw new EvaluationError("type-error", `Unknown operator ${op}`);
1340
+ }
1341
+ }
1342
+ function arithmetic(op, left, right) {
1343
+ if (left.kind !== "number" || right.kind !== "number") {
1344
+ throw new EvaluationError("type-error", `Operator '${op}' requires numbers, got ${left.kind} and ${right.kind}`);
1345
+ }
1346
+ try {
1347
+ switch (op) {
1348
+ case "-":
1349
+ return Values.number(left.value.sub(right.value));
1350
+ case "*":
1351
+ return Values.number(left.value.mul(right.value));
1352
+ case "/":
1353
+ return Values.number(left.value.div(right.value));
1354
+ case "%":
1355
+ return Values.number(left.value.mod(right.value));
1356
+ default:
1357
+ throw new EvaluationError("type-error", `Unknown operator ${op}`);
1358
+ }
1359
+ } catch (e) {
1360
+ if (e instanceof DivisionByZeroError) {
1361
+ throw new EvaluationError("division-by-zero", "Division by zero");
1362
+ }
1363
+ throw e;
1364
+ }
1365
+ }
1366
+ function compareOrdered(op, left, right) {
1367
+ let c;
1368
+ if (left.kind === "number" && right.kind === "number") {
1369
+ c = left.value.cmp(right.value);
1370
+ } else if (left.kind === "string" && right.kind === "string") {
1371
+ c = left.value < right.value ? -1 : left.value > right.value ? 1 : 0;
1372
+ } else {
1373
+ return Values.boolean(false);
1374
+ }
1375
+ switch (op) {
1376
+ case "<":
1377
+ return Values.boolean(c < 0);
1378
+ case "<=":
1379
+ return Values.boolean(c <= 0);
1380
+ case ">":
1381
+ return Values.boolean(c > 0);
1382
+ default:
1383
+ return Values.boolean(c >= 0);
1384
+ }
1385
+ }
1386
+ function evalLogical(op, node, ctx) {
1387
+ const left = truthy(evaluate(node.left, ctx));
1388
+ if (op === "and") return left ? Values.boolean(truthy(evaluate(node.right, ctx))) : Values.boolean(false);
1389
+ return left ? Values.boolean(true) : Values.boolean(truthy(evaluate(node.right, ctx)));
1390
+ }
1391
+ function evalMembership(node, ctx) {
1392
+ const element = evaluate(node.element, ctx);
1393
+ const collection = evaluate(node.collection, ctx);
1394
+ let present;
1395
+ if (collection.kind === "array") {
1396
+ present = collection.value.some((el) => valueEquals(el, element));
1397
+ } else if (collection.kind === "string") {
1398
+ if (element.kind !== "string") {
1399
+ throw new EvaluationError("type-error", `Cannot test ${element.kind} membership in a string`);
1400
+ }
1401
+ present = collection.value.includes(element.value);
1402
+ } else if (collection.kind === "null") {
1403
+ present = false;
1404
+ } else {
1405
+ throw new EvaluationError("type-error", `'in' requires an array or string, got ${collection.kind}`);
1406
+ }
1407
+ return Values.boolean(node.negated ? !present : present);
1408
+ }
1409
+ function evalCall(node, ctx) {
1410
+ const args = node.args.map((a) => evaluate(a, ctx));
1411
+ const host = ctx.hostFunctions?.[node.callee];
1412
+ if (host) return host(args);
1413
+ const impl = BUILTIN_IMPLS[node.callee];
1414
+ if (impl) return impl(args, ctx);
1415
+ throw new EvaluationError("unknown-function", `Unknown function: ${node.callee}`);
1416
+ }
1417
+ function evaluateExpression(source, ctx) {
1418
+ const { ast, errors } = parse(source);
1419
+ if (!ast) {
1420
+ return { success: false, error: errors[0]?.message ?? "Parse error", diagnostics: errors };
1421
+ }
1422
+ try {
1423
+ return { success: true, value: evaluate(ast, ctx) };
1424
+ } catch (e) {
1425
+ if (e instanceof EvaluationError) return { success: false, error: e.message, code: e.code };
1426
+ throw e;
1427
+ }
1428
+ }
1429
+ function evaluateBoolean(condExpr, ctx, defaultValue) {
1430
+ if (condExpr === void 0) return defaultValue;
1431
+ if (typeof condExpr === "boolean") return condExpr;
1432
+ const result = evaluateExpression(condExpr, ctx);
1433
+ return result.success ? truthy(result.value) : defaultValue;
1434
+ }
1435
+
1436
+ // src/check/checker.ts
1437
+ function staticPath2(node) {
1438
+ if (node.kind === "Identifier") return node.name;
1439
+ if (node.kind === "Member") {
1440
+ const base = staticPath2(node.object);
1441
+ return base === null ? null : `${base}.${node.property}`;
1442
+ }
1443
+ return null;
1444
+ }
1445
+ function assignable(expected, actual) {
1446
+ return expected.kind === "unknown" || actual.kind === "unknown" || typesEqual(expected, actual);
1447
+ }
1448
+ var NUMERIC = (t) => t.kind === "number" || t.kind === "unknown";
1449
+ var STRINGY = (t) => t.kind === "string" || t.kind === "unknown";
1450
+ var Checker = class {
1451
+ constructor(env) {
1452
+ this.env = env;
1453
+ }
1454
+ env;
1455
+ diagnostics = [];
1456
+ error(code, message, span) {
1457
+ this.diagnostics.push({ severity: "error", code, message, span });
1458
+ }
1459
+ infer(node) {
1460
+ switch (node.kind) {
1461
+ case "NumberLiteral":
1462
+ return T.number;
1463
+ case "StringLiteral":
1464
+ return T.string;
1465
+ case "BooleanLiteral":
1466
+ return T.boolean;
1467
+ case "NullLiteral":
1468
+ return T.null;
1469
+ case "ArrayLiteral":
1470
+ return this.inferArray(node);
1471
+ case "Identifier":
1472
+ return this.inferRef(node.name, node.span);
1473
+ case "Member":
1474
+ return this.inferMember(node);
1475
+ case "Index": {
1476
+ const obj = this.infer(node.object);
1477
+ const idx = this.infer(node.index);
1478
+ if (!NUMERIC(idx)) this.error("type-mismatch", "Array index must be a number", node.index.span);
1479
+ return obj.kind === "array" ? obj.element : T.unknown;
1480
+ }
1481
+ case "Unary":
1482
+ return this.inferUnary(node);
1483
+ case "Binary":
1484
+ return this.inferBinary(node);
1485
+ case "Logical":
1486
+ this.infer(node.left);
1487
+ this.infer(node.right);
1488
+ return T.boolean;
1489
+ case "Membership":
1490
+ return this.inferMembership(node);
1491
+ case "Conditional":
1492
+ return this.inferConditional(node);
1493
+ case "Call":
1494
+ return this.inferCall(node);
1495
+ }
1496
+ }
1497
+ inferArray(node) {
1498
+ if (node.elements.length === 0) return T.array(T.unknown);
1499
+ const types = node.elements.map((el) => this.infer(el));
1500
+ const first = types[0];
1501
+ const uniform = types.every((t) => typesEqual(t, first));
1502
+ return T.array(uniform ? first : T.unknown);
1503
+ }
1504
+ inferRef(path, span) {
1505
+ const t = this.env.resolve(path);
1506
+ if (t === void 0) {
1507
+ this.error("unknown-identifier", `Unknown reference: ${path}`, span);
1508
+ return T.unknown;
1509
+ }
1510
+ return t;
1511
+ }
1512
+ inferMember(node) {
1513
+ const path = staticPath2(node);
1514
+ if (path !== null) return this.inferRef(path, node.span);
1515
+ this.infer(node.object);
1516
+ return T.unknown;
1517
+ }
1518
+ inferUnary(node) {
1519
+ const t = this.infer(node.operand);
1520
+ if (node.op === "not") return T.boolean;
1521
+ if (!NUMERIC(t)) this.error("type-mismatch", `Cannot negate ${formatType(t)}`, node.span);
1522
+ return T.number;
1523
+ }
1524
+ inferBinary(node) {
1525
+ const l = this.infer(node.left);
1526
+ const r = this.infer(node.right);
1527
+ const op = node.op;
1528
+ if (op === "==" || op === "!=") return T.boolean;
1529
+ if (op === "<" || op === "<=" || op === ">" || op === ">=") {
1530
+ const ok = NUMERIC(l) && NUMERIC(r) || STRINGY(l) && STRINGY(r);
1531
+ if (!ok) this.error("type-mismatch", `Cannot compare ${formatType(l)} and ${formatType(r)}`, node.span);
1532
+ return T.boolean;
1533
+ }
1534
+ if (op === "+") {
1535
+ if (NUMERIC(l) && NUMERIC(r)) return T.number;
1536
+ if (l.kind === "string" || r.kind === "string") return T.string;
1537
+ if (l.kind === "unknown" || r.kind === "unknown") return T.unknown;
1538
+ this.error("type-mismatch", `Cannot add ${formatType(l)} and ${formatType(r)}`, node.span);
1539
+ return T.unknown;
1540
+ }
1541
+ if (!NUMERIC(l) || !NUMERIC(r)) {
1542
+ this.error("type-mismatch", `Operator '${op}' requires numbers, got ${formatType(l)} and ${formatType(r)}`, node.span);
1543
+ }
1544
+ return T.number;
1545
+ }
1546
+ inferMembership(node) {
1547
+ this.infer(node.element);
1548
+ const coll = this.infer(node.collection);
1549
+ if (coll.kind !== "array" && coll.kind !== "string" && coll.kind !== "unknown") {
1550
+ this.error("type-mismatch", `'in' requires an array or string, got ${formatType(coll)}`, node.collection.span);
1551
+ }
1552
+ return T.boolean;
1553
+ }
1554
+ inferConditional(node) {
1555
+ this.infer(node.test);
1556
+ const a = this.infer(node.consequent);
1557
+ const b = this.infer(node.alternate);
1558
+ return typesEqual(a, b) ? a : T.unknown;
1559
+ }
1560
+ inferCall(node) {
1561
+ const sig = this.env.registry.get(node.callee);
1562
+ if (!sig) {
1563
+ this.error("unknown-function", `Unknown function: ${node.callee}`, node.span);
1564
+ node.args.forEach((a) => this.infer(a));
1565
+ return T.unknown;
1566
+ }
1567
+ const argTypes = node.args.map((a) => this.infer(a));
1568
+ const required = sig.params.filter((p) => !p.optional).length;
1569
+ const max = sig.variadic ? Infinity : sig.params.length;
1570
+ if (argTypes.length < required || argTypes.length > max) {
1571
+ this.error("arity", `${node.callee} expects ${arityText(required, max)}, got ${argTypes.length}`, node.span);
1572
+ }
1573
+ argTypes.forEach((at, i) => {
1574
+ const param = sig.params[Math.min(i, sig.params.length - 1)];
1575
+ if (param && !assignable(param.type, at)) {
1576
+ this.error("type-mismatch", `${node.callee}: argument ${i + 1} expects ${formatType(param.type)}, got ${formatType(at)}`, node.args[i].span);
1577
+ }
1578
+ });
1579
+ return resolveReturn(sig.returns, argTypes);
1580
+ }
1581
+ };
1582
+ function arityText(required, max) {
1583
+ if (max === Infinity) return `at least ${required} argument(s)`;
1584
+ if (required === max) return `${required} argument(s)`;
1585
+ return `${required} to ${max} argument(s)`;
1586
+ }
1587
+ function resolveReturn(spec, argTypes) {
1588
+ if (spec.kind === "fixed") return spec.type;
1589
+ if (spec.kind === "elementOf") {
1590
+ const t = argTypes[spec.arg];
1591
+ return t && t.kind === "array" ? t.element : T.unknown;
1592
+ }
1593
+ const known = argTypes.filter((t) => t.kind !== "null" && t.kind !== "unknown");
1594
+ const first = known[0];
1595
+ if (first && known.every((t) => typesEqual(t, first))) return first;
1596
+ return T.unknown;
1597
+ }
1598
+ function createTypeEnv(paths, registry = buildRegistry()) {
1599
+ const map = new Map(Object.entries(paths));
1600
+ return { resolve: (p) => map.get(p), registry };
1601
+ }
1602
+ function checkAst(ast, env) {
1603
+ const checker = new Checker(env);
1604
+ const type = checker.infer(ast);
1605
+ return { type, diagnostics: checker.diagnostics };
1606
+ }
1607
+ function check(source, env) {
1608
+ const { ast, errors } = parse(source);
1609
+ if (!ast) return { type: T.unknown, diagnostics: errors };
1610
+ return checkAst(ast, env);
1611
+ }
1612
+ function checkBooleanGate(source, env) {
1613
+ const result = check(source, env);
1614
+ if (result.diagnostics.length === 0 && result.type.kind !== "boolean" && result.type.kind !== "unknown") {
1615
+ const { ast } = parse(source);
1616
+ const span = ast ? ast.span : { start: { offset: 0, line: 1, column: 1 }, end: { offset: 0, line: 1, column: 1 } };
1617
+ return {
1618
+ type: result.type,
1619
+ diagnostics: [{ severity: "error", code: "non-boolean-gate", message: `A gate must be boolean, got ${formatType(result.type)}`, span }]
1620
+ };
1621
+ }
1622
+ return result;
1623
+ }
1624
+ export {
1625
+ BINARY_OPERATORS,
1626
+ BINARY_OPERATOR_TOKENS,
1627
+ BUILTIN_IMPLS,
1628
+ DEFAULT_SIGNATURES,
1629
+ Decimal,
1630
+ DivisionByZeroError,
1631
+ EvaluationError,
1632
+ FORBIDDEN_OPERATORS,
1633
+ KEYWORDS,
1634
+ KEYWORD_SET,
1635
+ LexError,
1636
+ NULL,
1637
+ T,
1638
+ UNARY_OPERATORS,
1639
+ Values,
1640
+ buildRegistry,
1641
+ check,
1642
+ checkAst,
1643
+ checkBooleanGate,
1644
+ createContext,
1645
+ createTypeEnv,
1646
+ evaluate,
1647
+ evaluateBoolean,
1648
+ evaluateExpression,
1649
+ extractReferences,
1650
+ formatType,
1651
+ parse,
1652
+ parseOrThrow,
1653
+ toValue,
1654
+ tokenize,
1655
+ truthy,
1656
+ typesEqual,
1657
+ valueEquals,
1658
+ valueToString
1659
+ };