@texuli/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +11 -5
  2. package/dist/data-source/collection.d.ts +67 -0
  3. package/dist/data-source/collection.js +247 -0
  4. package/dist/data-source/errors.d.ts +58 -0
  5. package/dist/data-source/errors.js +138 -0
  6. package/dist/data-source/index.d.ts +17 -0
  7. package/dist/data-source/index.js +17 -0
  8. package/dist/data-source/loader.d.ts +120 -0
  9. package/dist/data-source/loader.js +571 -0
  10. package/dist/data-source/query.d.ts +78 -0
  11. package/dist/data-source/query.js +339 -0
  12. package/dist/data-source/registry.d.ts +83 -0
  13. package/dist/data-source/registry.js +63 -0
  14. package/dist/data-source/schema.d.ts +122 -0
  15. package/dist/data-source/schema.js +197 -0
  16. package/dist/expression/analyze.d.ts +17 -0
  17. package/dist/expression/analyze.js +151 -0
  18. package/dist/expression/evaluate.d.ts +55 -0
  19. package/dist/expression/evaluate.js +365 -0
  20. package/dist/expression/functions.d.ts +7 -0
  21. package/dist/expression/functions.js +374 -0
  22. package/dist/expression/index.d.ts +7 -0
  23. package/dist/expression/index.js +12 -0
  24. package/dist/expression/parse.d.ts +19 -0
  25. package/dist/expression/parse.js +284 -0
  26. package/dist/expression/text.d.ts +15 -0
  27. package/dist/expression/text.js +489 -0
  28. package/dist/expression/types.d.ts +197 -0
  29. package/dist/expression/types.js +35 -0
  30. package/dist/expression/values.d.ts +14 -0
  31. package/dist/expression/values.js +68 -0
  32. package/dist/i18n/catalog.d.ts +51 -0
  33. package/dist/i18n/catalog.js +109 -0
  34. package/dist/i18n/format.d.ts +41 -0
  35. package/dist/i18n/format.js +117 -0
  36. package/dist/i18n/i18n.d.ts +70 -0
  37. package/dist/i18n/i18n.js +185 -0
  38. package/dist/i18n/index.d.ts +8 -0
  39. package/dist/i18n/index.js +8 -0
  40. package/dist/i18n/pseudo.d.ts +4 -0
  41. package/dist/i18n/pseudo.js +41 -0
  42. package/package.json +4 -1
@@ -0,0 +1,489 @@
1
+ import { parseExpression } from "./parse.js";
2
+ import { EXPRESSION_LIMITS, } from "./types.js";
3
+ class TextSyntaxError extends Error {
4
+ code;
5
+ at;
6
+ constructor(code, at, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.at = at;
10
+ }
11
+ }
12
+ const OPERATORS = [
13
+ '==',
14
+ '!=',
15
+ '<>',
16
+ '<=',
17
+ '>=',
18
+ '&&',
19
+ '||',
20
+ '<',
21
+ '>',
22
+ '=',
23
+ '!',
24
+ '+',
25
+ '-',
26
+ '*',
27
+ '/',
28
+ '%',
29
+ '^',
30
+ '&',
31
+ '(',
32
+ ')',
33
+ ',',
34
+ ];
35
+ const ESCAPES = { n: '\n', t: '\t', r: '\r' };
36
+ function isDigit(code) {
37
+ return code >= 48 && code <= 57;
38
+ }
39
+ function isNameStart(code) {
40
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
41
+ }
42
+ function tokenize(source) {
43
+ const tokens = [];
44
+ let i = 0;
45
+ while (i < source.length) {
46
+ const code = source.charCodeAt(i);
47
+ if (code === 32 || code === 9 || code === 10 || code === 13) {
48
+ i += 1;
49
+ continue;
50
+ }
51
+ const start = i;
52
+ if (isDigit(code) || (code === 46 && isDigit(source.charCodeAt(i + 1)))) {
53
+ while (isDigit(source.charCodeAt(i)))
54
+ i += 1;
55
+ if (source[i] === '.') {
56
+ i += 1;
57
+ while (isDigit(source.charCodeAt(i)))
58
+ i += 1;
59
+ }
60
+ if (source[i] === 'e' || source[i] === 'E') {
61
+ let j = i + 1;
62
+ if (source[j] === '+' || source[j] === '-')
63
+ j += 1;
64
+ if (isDigit(source.charCodeAt(j))) {
65
+ i = j;
66
+ while (isDigit(source.charCodeAt(i)))
67
+ i += 1;
68
+ }
69
+ }
70
+ const text = source.slice(start, i);
71
+ const value = Number(text);
72
+ if (!Number.isFinite(value))
73
+ throw new TextSyntaxError('syntax', start, 'Number out of range.');
74
+ tokens.push({ kind: 'num', text, value, at: start });
75
+ continue;
76
+ }
77
+ if (code === 34 || code === 39) {
78
+ const quote = source[i];
79
+ i += 1;
80
+ let out = '';
81
+ let closed = false;
82
+ while (i < source.length) {
83
+ const ch = source[i];
84
+ if (ch === quote) {
85
+ closed = true;
86
+ i += 1;
87
+ break;
88
+ }
89
+ if (ch === '\\') {
90
+ const next = source[i + 1];
91
+ if (next === undefined)
92
+ break;
93
+ out += ESCAPES[next] ?? next;
94
+ i += 2;
95
+ continue;
96
+ }
97
+ out += ch;
98
+ i += 1;
99
+ }
100
+ if (!closed)
101
+ throw new TextSyntaxError('syntax', start, 'Unterminated string.');
102
+ if (out.length > EXPRESSION_LIMITS.stringLength) {
103
+ throw new TextSyntaxError('too-long', start, 'String too long.');
104
+ }
105
+ tokens.push({ kind: 'str', text: source.slice(start, i), value: out, at: start });
106
+ continue;
107
+ }
108
+ if (isNameStart(code)) {
109
+ while (i < source.length) {
110
+ const c = source.charCodeAt(i);
111
+ if (!isNameStart(c) && !isDigit(c))
112
+ break;
113
+ i += 1;
114
+ }
115
+ tokens.push({ kind: 'name', text: source.slice(start, i), at: start });
116
+ continue;
117
+ }
118
+ const op = OPERATORS.find((candidate) => source.startsWith(candidate, i));
119
+ if (!op) {
120
+ throw new TextSyntaxError('syntax', start, `Unexpected character "${source[i]}".`);
121
+ }
122
+ tokens.push({ kind: 'op', text: op, at: start });
123
+ i += op.length;
124
+ }
125
+ tokens.push({ kind: 'end', text: '', at: source.length });
126
+ return tokens;
127
+ }
128
+ const COMPARE = {
129
+ '==': 'eq',
130
+ '=': 'eq',
131
+ '!=': 'neq',
132
+ '<>': 'neq',
133
+ '<': 'lt',
134
+ '<=': 'lte',
135
+ '>': 'gt',
136
+ '>=': 'gte',
137
+ };
138
+ /** Names that are syntax, not fields. */
139
+ const KEYWORDS = new Set(['and', 'or', 'not', 'true', 'false', 'null']);
140
+ function build(tokens) {
141
+ let index = 0;
142
+ let level = 0;
143
+ let nodes = 0;
144
+ const peek = () => tokens[index];
145
+ const take = () => tokens[index++];
146
+ const isOp = (text) => peek().kind === 'op' && peek().text === text;
147
+ const isWord = (text) => peek().kind === 'name' && peek().text === text;
148
+ const expect = (text) => {
149
+ if (!isOp(text))
150
+ throw new TextSyntaxError('syntax', peek().at, `Expected "${text}".`);
151
+ take();
152
+ };
153
+ const node = (value) => {
154
+ nodes += 1;
155
+ if (nodes > EXPRESSION_LIMITS.nodes) {
156
+ throw new TextSyntaxError('too-many-nodes', peek().at, `An expression has at most ${EXPRESSION_LIMITS.nodes} nodes.`);
157
+ }
158
+ return value;
159
+ };
160
+ const enter = () => {
161
+ level += 1;
162
+ if (level > EXPRESSION_LIMITS.depth) {
163
+ throw new TextSyntaxError('too-deep', peek().at, `Expressions nest at most ${EXPRESSION_LIMITS.depth} levels.`);
164
+ }
165
+ };
166
+ const leave = () => {
167
+ level -= 1;
168
+ };
169
+ // A chain of the same associative op becomes one list node.
170
+ const chain = (op, first, next, more) => {
171
+ const args = [first];
172
+ while (more()) {
173
+ take();
174
+ args.push(next());
175
+ }
176
+ if (args.length === 1)
177
+ return first;
178
+ // Past the argument cap, nest: (a, …, b) → op(op(a, …), …).
179
+ let current = args.slice(0, EXPRESSION_LIMITS.args);
180
+ let rest = args.slice(EXPRESSION_LIMITS.args);
181
+ let out = node({ op, args: current });
182
+ while (rest.length > 0) {
183
+ current = [out, ...rest.slice(0, EXPRESSION_LIMITS.args - 1)];
184
+ rest = rest.slice(EXPRESSION_LIMITS.args - 1);
185
+ out = node({ op, args: current });
186
+ }
187
+ return out;
188
+ };
189
+ const parseOr = () => chain('or', parseAnd(), parseAnd, () => isWord('or') || isOp('||'));
190
+ const parseAnd = () => chain('and', parseNot(), parseNot, () => isWord('and') || isOp('&&'));
191
+ const parseNot = () => {
192
+ if (isWord('not') || isOp('!')) {
193
+ take();
194
+ enter();
195
+ const value = parseNot();
196
+ leave();
197
+ return node({ op: 'not', value });
198
+ }
199
+ return parseCompare();
200
+ };
201
+ const parseCompare = () => {
202
+ const left = parseConcat();
203
+ const token = peek();
204
+ const op = token.kind === 'op' ? COMPARE[token.text] : undefined;
205
+ if (!op)
206
+ return left;
207
+ take();
208
+ const right = parseConcat();
209
+ const after = peek();
210
+ if (after.kind === 'op' && COMPARE[after.text]) {
211
+ throw new TextSyntaxError('syntax', after.at, 'Comparisons do not chain; use "and".');
212
+ }
213
+ return node({ op, left, right });
214
+ };
215
+ const parseConcat = () => chain('concat', parseSum(), parseSum, () => isOp('&'));
216
+ const parseSum = () => {
217
+ let left = parseProduct();
218
+ while (isOp('+') || isOp('-')) {
219
+ const op = take().text === '+' ? 'add' : 'sub';
220
+ left = node({ op, left, right: parseProduct() });
221
+ }
222
+ return left;
223
+ };
224
+ const parseProduct = () => {
225
+ let left = parseUnary();
226
+ while (isOp('*') || isOp('/') || isOp('%')) {
227
+ const text = take().text;
228
+ const op = text === '*' ? 'mul' : text === '/' ? 'div' : 'mod';
229
+ left = node({ op, left, right: parseUnary() });
230
+ }
231
+ return left;
232
+ };
233
+ const parseUnary = () => {
234
+ if (isOp('-')) {
235
+ take();
236
+ enter();
237
+ const value = parseUnary();
238
+ leave();
239
+ return node({ op: 'neg', value });
240
+ }
241
+ if (isOp('+')) {
242
+ take();
243
+ enter();
244
+ const value = parseUnary();
245
+ leave();
246
+ return value;
247
+ }
248
+ return parsePower();
249
+ };
250
+ const parsePower = () => {
251
+ const base = parsePrimary();
252
+ if (!isOp('^'))
253
+ return base;
254
+ take();
255
+ enter();
256
+ const exponent = parseUnary();
257
+ leave();
258
+ return node({ op: 'pow', base, exponent });
259
+ };
260
+ const parseArgs = () => {
261
+ expect('(');
262
+ const args = [];
263
+ if (isOp(')')) {
264
+ take();
265
+ return args;
266
+ }
267
+ enter();
268
+ for (;;) {
269
+ args.push(parseOr());
270
+ if (args.length > EXPRESSION_LIMITS.args) {
271
+ throw new TextSyntaxError('syntax', peek().at, `At most ${EXPRESSION_LIMITS.args} arguments.`);
272
+ }
273
+ if (isOp(',')) {
274
+ take();
275
+ continue;
276
+ }
277
+ break;
278
+ }
279
+ leave();
280
+ expect(')');
281
+ return args;
282
+ };
283
+ const parsePrimary = () => {
284
+ const token = take();
285
+ if (token.kind === 'num')
286
+ return node({ op: 'num', value: token.value });
287
+ if (token.kind === 'str')
288
+ return node({ op: 'str', value: token.value });
289
+ if (token.kind === 'op' && token.text === '(') {
290
+ enter();
291
+ const inner = parseOr();
292
+ leave();
293
+ expect(')');
294
+ return inner;
295
+ }
296
+ if (token.kind === 'name') {
297
+ if (token.text === 'true' || token.text === 'false') {
298
+ return node({ op: 'bool', value: token.text === 'true' });
299
+ }
300
+ if (token.text === 'null')
301
+ return node({ op: 'null' });
302
+ if (KEYWORDS.has(token.text)) {
303
+ throw new TextSyntaxError('syntax', token.at, `Unexpected "${token.text}".`);
304
+ }
305
+ if (!isOp('('))
306
+ return node({ op: 'field', name: token.text });
307
+ const args = parseArgs();
308
+ return call(token, args);
309
+ }
310
+ throw new TextSyntaxError('syntax', token.at, token.kind === 'end' ? 'Unexpected end of expression.' : `Unexpected "${token.text}".`);
311
+ };
312
+ const call = (token, args) => {
313
+ const name = token.text;
314
+ const arity = (min, max) => {
315
+ if (args.length < min || args.length > max) {
316
+ throw new TextSyntaxError('arity', token.at, min === max
317
+ ? `"${name}" takes ${min} arguments.`
318
+ : `"${name}" takes ${min} to ${max} arguments.`);
319
+ }
320
+ };
321
+ switch (name) {
322
+ case 'if': {
323
+ arity(3, 3);
324
+ const [test, ifTrue, ifFalse] = args;
325
+ return node({ op: 'if', test, ifTrue, ifFalse });
326
+ }
327
+ case 'min':
328
+ case 'max':
329
+ case 'concat':
330
+ arity(1, EXPRESSION_LIMITS.args);
331
+ return node({ op: name, args });
332
+ case 'clamp': {
333
+ arity(3, 3);
334
+ const [value, min, max] = args;
335
+ return node({ op: 'clamp', value, min, max });
336
+ }
337
+ case 'round': {
338
+ arity(1, 2);
339
+ const [value, digits] = args;
340
+ if (!digits)
341
+ return node({ op: 'round', value });
342
+ const literal = digits.op === 'num'
343
+ ? digits.value
344
+ : digits.op === 'neg' && digits.value.op === 'num'
345
+ ? -digits.value.value
346
+ : undefined;
347
+ if (literal === undefined) {
348
+ throw new TextSyntaxError('syntax', token.at, '"round" digits must be a number literal.');
349
+ }
350
+ return node({ op: 'round', value, digits: literal });
351
+ }
352
+ default:
353
+ return node({ op: 'call', fn: name, args });
354
+ }
355
+ };
356
+ const tree = parseOr();
357
+ const rest = peek();
358
+ if (rest.kind !== 'end')
359
+ throw new TextSyntaxError('syntax', rest.at, `Unexpected "${rest.text}".`);
360
+ return tree;
361
+ }
362
+ /**
363
+ * Parse expression text (`age >= 18 and country == "CO"`) into a validated
364
+ * expression. Names read fields (`{ op: "field" }`); `if`, `min`, `max`,
365
+ * `concat`, `round` and `clamp` map to their AST ops, and other calls must
366
+ * be in the function table. Never throws.
367
+ */
368
+ export function parseExpressionText(source, options = {}) {
369
+ const fail = (code, at, message) => Object.freeze({
370
+ ok: false,
371
+ expression: null,
372
+ diagnostics: Object.freeze([Object.freeze({ code, path: `@${at}`, message })]),
373
+ });
374
+ if (typeof source !== 'string')
375
+ return fail('syntax', 0, 'Expression text is a string.');
376
+ if (source.length > EXPRESSION_LIMITS.sourceLength) {
377
+ return fail('too-long', 0, `Expression text is at most ${EXPRESSION_LIMITS.sourceLength} characters.`);
378
+ }
379
+ let tree;
380
+ try {
381
+ tree = build(tokenize(source));
382
+ }
383
+ catch (error) {
384
+ if (error instanceof TextSyntaxError)
385
+ return fail(error.code, error.at, error.message);
386
+ return fail('syntax', 0, 'Unreadable expression.');
387
+ }
388
+ return parseExpression(tree, options);
389
+ }
390
+ const PRECEDENCE = {
391
+ or: 1,
392
+ and: 2,
393
+ not: 3,
394
+ lt: 4,
395
+ lte: 4,
396
+ gt: 4,
397
+ gte: 4,
398
+ eq: 4,
399
+ neq: 4,
400
+ concat: 5,
401
+ add: 6,
402
+ sub: 6,
403
+ mul: 7,
404
+ div: 7,
405
+ mod: 7,
406
+ neg: 8,
407
+ pow: 9,
408
+ };
409
+ const SYMBOL = {
410
+ lt: '<',
411
+ lte: '<=',
412
+ gt: '>',
413
+ gte: '>=',
414
+ eq: '==',
415
+ neq: '!=',
416
+ add: '+',
417
+ sub: '-',
418
+ mul: '*',
419
+ div: '/',
420
+ mod: '%',
421
+ pow: '^',
422
+ };
423
+ function quote(value) {
424
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/\r/g, '\\r')}"`;
425
+ }
426
+ /**
427
+ * Print an AST as expression text that parses back to an equivalent
428
+ * expression (`var` prints as a plain name, which reads back as `field`;
429
+ * `ref` and `range` print their dialect text).
430
+ */
431
+ export function formatExpression(ast) {
432
+ const wrap = (child, parent, right = false) => {
433
+ const own = PRECEDENCE[child.op] ?? 10;
434
+ const text = formatExpression(child);
435
+ return own < parent || (right && own === parent) ? `(${text})` : text;
436
+ };
437
+ switch (ast.op) {
438
+ case 'num':
439
+ return ast.value < 0 ? `(${String(ast.value)})` : String(ast.value);
440
+ case 'str':
441
+ return quote(ast.value);
442
+ case 'bool':
443
+ return String(ast.value);
444
+ case 'null':
445
+ return 'null';
446
+ case 'var':
447
+ case 'field':
448
+ return ast.name;
449
+ case 'ref':
450
+ return ast.ref;
451
+ case 'range':
452
+ return `${ast.from}:${ast.to}`;
453
+ case 'and':
454
+ case 'or':
455
+ return ast.args
456
+ .map((arg) => wrap(arg, PRECEDENCE[ast.op] + 1))
457
+ .join(` ${ast.op} `);
458
+ case 'concat':
459
+ return ast.args.length === 1
460
+ ? `concat(${formatExpression(ast.args[0])})`
461
+ : ast.args.map((arg) => wrap(arg, 6)).join(' & ');
462
+ case 'not':
463
+ return `not ${wrap(ast.value, 3)}`;
464
+ case 'neg':
465
+ return `-${wrap(ast.value, 8)}`;
466
+ case 'pow':
467
+ return `${wrap(ast.base, 10)} ^ ${wrap(ast.exponent, 8)}`;
468
+ case 'if':
469
+ return `if(${formatExpression(ast.test)}, ${formatExpression(ast.ifTrue)}, ${formatExpression(ast.ifFalse)})`;
470
+ case 'min':
471
+ case 'max':
472
+ return `${ast.op}(${ast.args.map(formatExpression).join(', ')})`;
473
+ case 'round':
474
+ return ast.digits === undefined
475
+ ? `round(${formatExpression(ast.value)})`
476
+ : `round(${formatExpression(ast.value)}, ${ast.digits})`;
477
+ case 'clamp':
478
+ return `clamp(${formatExpression(ast.value)}, ${formatExpression(ast.min)}, ${formatExpression(ast.max)})`;
479
+ case 'call':
480
+ return `${ast.fn}(${ast.args.map(formatExpression).join(', ')})`;
481
+ default: {
482
+ const own = PRECEDENCE[ast.op];
483
+ const symbol = SYMBOL[ast.op];
484
+ const node = ast;
485
+ const comparison = own === 4;
486
+ return `${wrap(node.left, comparison ? 5 : own)} ${symbol} ${wrap(node.right, comparison ? 5 : own, true)}`;
487
+ }
488
+ }
489
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The closed expression language of ADR-015: one JSON AST, one bounded
3
+ * evaluator. Every node names its operation in `op`. The base is the
4
+ * blueprint spec's `numberFormula` and `condition` shapes (ADR-008), which
5
+ * stay valid expressions; the extensions add strings, booleans, null,
6
+ * field references, calls to a closed function table, and the `ref` and
7
+ * `range` reference nodes a dialect (the planned sheets formula layer) can
8
+ * enable. There is no loop, no closure, no dynamic reference and no way to
9
+ * reach host objects.
10
+ */
11
+ /** Binary arithmetic. */
12
+ export type ExpressionArithmeticOp = 'add' | 'sub' | 'mul' | 'div' | 'mod';
13
+ /** Comparisons; numbers compare numerically, strings by code unit (so ISO dates sort). */
14
+ export type ExpressionComparisonOp = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'neq';
15
+ /** One node of the closed expression AST. */
16
+ export type ExpressionNode = {
17
+ readonly op: 'num';
18
+ readonly value: number;
19
+ } | {
20
+ readonly op: 'str';
21
+ readonly value: string;
22
+ } | {
23
+ readonly op: 'bool';
24
+ readonly value: boolean;
25
+ } | {
26
+ readonly op: 'null';
27
+ } | {
28
+ readonly op: 'var';
29
+ readonly name: string;
30
+ } | {
31
+ readonly op: 'field';
32
+ readonly name: string;
33
+ } | {
34
+ readonly op: 'ref';
35
+ readonly ref: string;
36
+ } | {
37
+ readonly op: 'range';
38
+ readonly from: string;
39
+ readonly to: string;
40
+ } | {
41
+ readonly op: ExpressionArithmeticOp;
42
+ readonly left: ExpressionNode;
43
+ readonly right: ExpressionNode;
44
+ } | {
45
+ readonly op: 'pow';
46
+ readonly base: ExpressionNode;
47
+ readonly exponent: ExpressionNode;
48
+ } | {
49
+ readonly op: 'neg';
50
+ readonly value: ExpressionNode;
51
+ } | {
52
+ readonly op: ExpressionComparisonOp;
53
+ readonly left: ExpressionNode;
54
+ readonly right: ExpressionNode;
55
+ } | {
56
+ readonly op: 'and' | 'or';
57
+ readonly args: readonly ExpressionNode[];
58
+ } | {
59
+ readonly op: 'not';
60
+ readonly value: ExpressionNode;
61
+ } | {
62
+ readonly op: 'if';
63
+ readonly test: ExpressionNode;
64
+ readonly ifTrue: ExpressionNode;
65
+ readonly ifFalse: ExpressionNode;
66
+ } | {
67
+ readonly op: 'min' | 'max' | 'concat';
68
+ readonly args: readonly ExpressionNode[];
69
+ } | {
70
+ readonly op: 'round';
71
+ readonly value: ExpressionNode;
72
+ readonly digits?: number;
73
+ } | {
74
+ readonly op: 'clamp';
75
+ readonly value: ExpressionNode;
76
+ readonly min: ExpressionNode;
77
+ readonly max: ExpressionNode;
78
+ } | {
79
+ readonly op: 'call';
80
+ readonly fn: string;
81
+ readonly args: readonly ExpressionNode[];
82
+ };
83
+ /** Every `op` the AST knows. */
84
+ export type ExpressionOp = ExpressionNode['op'];
85
+ /** The error codes an evaluation can produce (spreadsheet-style values, never exceptions). */
86
+ export type ExpressionErrorCode = '#DIV/0!' | '#VALUE!' | '#REF!' | '#NAME?' | '#NUM!' | '#N/A' | '#CYCLE!' | '#LIMIT!';
87
+ /** An error value. Errors propagate through operators and calls. */
88
+ export interface ExpressionError {
89
+ readonly error: ExpressionErrorCode;
90
+ }
91
+ /** A scalar value. Dates are ISO `YYYY-MM-DD` strings. */
92
+ export type ExpressionScalar = number | string | boolean | null;
93
+ /**
94
+ * A value an expression reads or produces: a scalar, a flat list of scalars
95
+ * (multiple-choice answers, a sheet range) or an error.
96
+ */
97
+ export type ExpressionValue = ExpressionScalar | readonly ExpressionScalar[] | ExpressionError;
98
+ /** The static types the analyzer knows. `any` accepts everything. */
99
+ export type ExpressionType = 'number' | 'string' | 'boolean' | 'date' | 'list' | 'null' | 'any';
100
+ /** What a function receives besides its arguments. */
101
+ export interface ExpressionFunctionContext {
102
+ /**
103
+ * Charge `steps` evaluation steps (for work proportional to input size).
104
+ * Returns `false` once the budget is spent; return `#LIMIT!` then.
105
+ */
106
+ charge(steps: number): boolean;
107
+ /** The host clock in milliseconds, or `undefined` when the host gave none. */
108
+ now(): number | undefined;
109
+ }
110
+ /**
111
+ * One entry of a closed function table. Arguments arrive evaluated; an error
112
+ * argument short-circuits the call unless `acceptsErrors` is set. `call`
113
+ * must be pure, bounded and total: a thrown exception becomes `#VALUE!`.
114
+ */
115
+ export interface ExpressionFunction {
116
+ readonly minArgs: number;
117
+ readonly maxArgs: number;
118
+ /** Parameter types by position; the last one repeats for variadic functions. */
119
+ readonly params: readonly ExpressionType[];
120
+ readonly returns: ExpressionType;
121
+ /** Pass error arguments through instead of short-circuiting (for an `IFERROR`). */
122
+ readonly acceptsErrors?: boolean;
123
+ readonly description?: string;
124
+ call(args: readonly ExpressionValue[], context: ExpressionFunctionContext): ExpressionValue;
125
+ }
126
+ /** A closed function table: names to functions. Expressions can only name these. */
127
+ export type ExpressionFunctionTable = Readonly<Record<string, ExpressionFunction>>;
128
+ /** A problem with an expression, addressed like authoring diagnostics. */
129
+ export interface ExpressionDiagnostic {
130
+ readonly code: ExpressionDiagnosticCode;
131
+ /** A JSON path into the AST (`$.args[0].left`) or a text offset (`@12`). */
132
+ readonly path: string;
133
+ readonly message: string;
134
+ }
135
+ export type ExpressionDiagnosticCode = 'not-a-node' | 'unknown-op' | 'unknown-member' | 'missing-member' | 'bad-member' | 'bad-name' | 'unknown-function' | 'arity' | 'refs-disabled' | 'bad-ref' | 'too-many-nodes' | 'too-deep' | 'too-long' | 'syntax' | 'unknown-field' | 'type';
136
+ /**
137
+ * Lets a dialect enable `ref` and `range` nodes. The forms dialect leaves it
138
+ * out (references are refused); the planned sheets formula layer passes a
139
+ * checker for its reference syntax.
140
+ */
141
+ export interface ExpressionReferenceDialect {
142
+ /** Whether `ref` is a reference this dialect accepts (for example `A1` or `Sheet2!B3`). */
143
+ isRef(ref: string): boolean;
144
+ }
145
+ /** A validated, frozen expression: the AST plus what it depends on. */
146
+ export interface Expression {
147
+ readonly ast: ExpressionNode;
148
+ /** Node count (at most `EXPRESSION_LIMITS.nodes`). */
149
+ readonly nodes: number;
150
+ /** Nesting depth (at most `EXPRESSION_LIMITS.depth`). */
151
+ readonly depth: number;
152
+ /** Field and var names read, sorted and unique. */
153
+ readonly fields: readonly string[];
154
+ /** Function names called, sorted and unique. */
155
+ readonly functions: readonly string[];
156
+ /** `ref` nodes, sorted and unique (empty unless a dialect enabled them). */
157
+ readonly refs: readonly string[];
158
+ /** `range` nodes in source order (an interval each, never expanded). */
159
+ readonly ranges: readonly {
160
+ readonly from: string;
161
+ readonly to: string;
162
+ }[];
163
+ }
164
+ export type ExpressionParseResult = {
165
+ readonly ok: true;
166
+ readonly expression: Expression;
167
+ readonly diagnostics: readonly [];
168
+ } | {
169
+ readonly ok: false;
170
+ readonly expression: null;
171
+ readonly diagnostics: readonly ExpressionDiagnostic[];
172
+ };
173
+ /** The limits of ADR-015. Exceeding one is a diagnostic at parse time or `#LIMIT!` at run time. */
174
+ export declare const EXPRESSION_LIMITS: Readonly<{
175
+ /** AST nodes per expression. */
176
+ nodes: 1000;
177
+ /** Nesting depth per expression. */
178
+ depth: 32;
179
+ /** Evaluation steps per expression. */
180
+ steps: 100000;
181
+ /** Evaluation steps per recalculation pass (an `ExpressionBudget`). */
182
+ passSteps: 10000000;
183
+ /** UTF-16 code units in any string value. */
184
+ stringLength: 32768;
185
+ /** Items in any list value. */
186
+ listItems: 10000;
187
+ /** Characters of expression text the text parser reads. */
188
+ sourceLength: 16384;
189
+ /** Arguments of one `and`, `or`, `min`, `max`, `concat` or `call`. */
190
+ args: 64;
191
+ /** Largest absolute `pow` exponent. */
192
+ exponent: 64;
193
+ /** `round` digits range (±). */
194
+ roundDigits: 12;
195
+ }>;
196
+ /** Field, var and function names: a letter or underscore, then letters, digits or underscores. */
197
+ export declare const EXPRESSION_NAME: RegExp;