littlewing 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gabriel Vaquer <brielov@icloud.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # littlewing
2
+
3
+ A minimal, high-performance multi-type expression language for JavaScript. Seven types, zero compromise, built for the browser.
4
+
5
+ ```typescript
6
+ import { evaluate, defaultContext } from "littlewing";
7
+
8
+ // Arithmetic
9
+ evaluate("2 + 3 * 4"); // → 14
10
+
11
+ // Strings
12
+ evaluate('"hello" + " world"'); // → "hello world"
13
+
14
+ // Variables and conditionals
15
+ evaluate('price = 100; if price > 50 then "expensive" else "cheap"'); // → "expensive"
16
+
17
+ // Date arithmetic
18
+ evaluate("DIFFERENCE_IN_DAYS(TODAY(), DATE(2025, 12, 31))", defaultContext);
19
+
20
+ // Array comprehensions
21
+ evaluate("for x in 1..=5 then x ^ 2"); // → [1, 4, 9, 16, 25]
22
+
23
+ // Reduce with accumulator
24
+ evaluate("for x in [1, 2, 3, 4] into sum = 0 then sum + x"); // → 10
25
+
26
+ // Pipe operator — chain values through functions
27
+ evaluate("-5 |> ABS(?) |> STR(?)", defaultContext); // → "5"
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - **Seven types** — Numbers, strings, booleans, dates (`Temporal.PlainDate`), times (`Temporal.PlainTime`), datetimes (`Temporal.PlainDateTime`), and homogeneous arrays
33
+ - **No implicit coercion** — Explicit type conversion via `STR()`, `NUM()`, etc.
34
+ - **Strict boolean logic** — `!`, `&&`, `||`, and `if` conditions require booleans
35
+ - **Control flow** — `if/then/else` expressions and `for/in/then` comprehensions with optional `when` guard and `into` accumulator
36
+ - **Bracket indexing** — `arr[0]`, `str[-1]`, with chaining (`matrix[0][1]`)
37
+ - **Pipe operator** — `x |> FUN(?) |> OTHER(?, 1)` chains values through function calls
38
+ - **Range expressions** — `1..5` (exclusive), `1..=5` (inclusive)
39
+ - **Deep equality** — `[1, 2] == [1, 2]` → `true`; cross-type `==` → `false`
40
+ - **85 built-in functions** — Math, string, array, date, time, and datetime operations
41
+ - **O(n) performance** — Linear time parsing and execution
42
+ - **Safe evaluation** — Tree-walk interpreter, no code generation
43
+ - **Extensible** — Add custom functions and variables via context
44
+ - **Type-safe** — Full TypeScript support with strict types
45
+ - **Zero runtime dependencies** — Requires global `Temporal` API (native or polyfill)
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ npm install littlewing
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### Basic Usage
56
+
57
+ ```typescript
58
+ import { evaluate } from "littlewing";
59
+
60
+ // Arithmetic
61
+ evaluate("2 + 3 * 4"); // → 14
62
+ evaluate("2 ^ 10"); // → 1024
63
+
64
+ // Strings
65
+ evaluate('"hello" + " world"'); // → "hello world"
66
+
67
+ // Booleans (comparisons return boolean, not 1/0)
68
+ evaluate("5 > 3"); // → true
69
+ evaluate("!(5 > 10)"); // → true
70
+
71
+ // Variables
72
+ evaluate("x = 10; y = 20; x + y"); // → 30
73
+
74
+ // Conditionals (condition must be boolean, else is required)
75
+ evaluate('age = 21; if age >= 18 then "adult" else "minor"'); // → "adult"
76
+
77
+ // Arrays and indexing
78
+ evaluate("[10, 20, 30][-1]"); // → 30
79
+ evaluate("[1, 2] + [3, 4]"); // → [1, 2, 3, 4]
80
+
81
+ // Ranges
82
+ evaluate("1..=5"); // → [1, 2, 3, 4, 5]
83
+
84
+ // For comprehensions (map, filter, reduce)
85
+ evaluate("for x in 1..=5 then x * 2"); // → [2, 4, 6, 8, 10]
86
+ evaluate("for x in 1..=10 when x % 2 == 0 then x"); // → [2, 4, 6, 8, 10]
87
+ evaluate("for x in [1, 2, 3] into sum = 0 then sum + x"); // → 6
88
+
89
+ // Pipe operator
90
+ evaluate("-42 |> ABS(?)", defaultContext); // → 42
91
+ evaluate("150 |> CLAMP(?, 0, 100)", defaultContext); // → 100
92
+ evaluate("-3 |> ABS(?) |> STR(?)", defaultContext); // → "3"
93
+ ```
94
+
95
+ ### With Built-in Functions
96
+
97
+ ```typescript
98
+ import { evaluate, defaultContext } from "littlewing";
99
+
100
+ // Math
101
+ evaluate("ABS(-42)", defaultContext); // → 42
102
+ evaluate("ROUND(3.7)", defaultContext); // → 4
103
+
104
+ // Type conversion
105
+ evaluate('NUM("42")', defaultContext); // → 42
106
+ evaluate("STR(42)", defaultContext); // → "42"
107
+
108
+ // String functions
109
+ evaluate('STR_UPPER("hello")', defaultContext); // → "HELLO"
110
+ evaluate('STR_SPLIT("a,b,c", ",")', defaultContext); // → ["a", "b", "c"]
111
+
112
+ // Array functions
113
+ evaluate("ARR_SORT([3, 1, 2])", defaultContext); // → [1, 2, 3]
114
+ evaluate("ARR_SUM([10, 20, 30])", defaultContext); // → 60
115
+ evaluate('ARR_JOIN(["a", "b", "c"], "-")', defaultContext); // → "a-b-c"
116
+
117
+ // Date functions
118
+ evaluate("TODAY()", defaultContext); // → Temporal.PlainDate
119
+ evaluate("ADD_DAYS(TODAY(), 7)", defaultContext); // → 7 days from now
120
+ evaluate("IS_WEEKEND(TODAY())", defaultContext); // → true or false
121
+
122
+ // Time functions
123
+ evaluate("TIME(14, 30, 0)", defaultContext); // → Temporal.PlainTime
124
+ evaluate("ADD_HOURS(TIME(10, 0, 0), 3)", defaultContext); // → 13:00:00
125
+
126
+ // DateTime functions
127
+ evaluate("NOW()", defaultContext); // → Temporal.PlainDateTime
128
+ evaluate("TO_DATE(NOW())", defaultContext); // → today's date
129
+ ```
130
+
131
+ ### Custom Functions and Variables
132
+
133
+ ```typescript
134
+ import { evaluate, assertNumber, assertString } from "littlewing";
135
+
136
+ const context = {
137
+ functions: {
138
+ FAHRENHEIT: (celsius) => {
139
+ assertNumber(celsius, "FAHRENHEIT");
140
+ return (celsius * 9) / 5 + 32;
141
+ },
142
+ DISCOUNT: (price, percent) => {
143
+ assertNumber(price, "DISCOUNT", "price");
144
+ assertNumber(percent, "DISCOUNT", "percent");
145
+ return price * (1 - percent / 100);
146
+ },
147
+ },
148
+ variables: {
149
+ pi: 3.14159,
150
+ taxRate: 0.08,
151
+ },
152
+ };
153
+
154
+ evaluate("FAHRENHEIT(20)", context); // → 68
155
+ evaluate("DISCOUNT(100, 15)", context); // → 85
156
+ evaluate("100 * (1 + taxRate)", context); // → 108
157
+ ```
158
+
159
+ The assertion helpers (`assertNumber`, `assertString`, `assertBoolean`, `assertArray`, `assertDate`, `assertTime`, `assertDateTime`, `assertDateOrDateTime`, `assertTimeOrDateTime`) are the same ones used by the built-in standard library. They throw `TypeError` with consistent messages on type mismatch.
160
+
161
+ ### External Variables Override Script Defaults
162
+
163
+ ```typescript
164
+ const formula = "multiplier = 2; value = 100; value * multiplier";
165
+
166
+ evaluate(formula); // → 200
167
+ evaluate(formula, { variables: { multiplier: 3 } }); // → 300
168
+ evaluate(formula, { variables: { value: 50 } }); // → 100
169
+ ```
170
+
171
+ ## Language Reference
172
+
173
+ For complete language documentation including all operators, control flow, and built-in functions, see [LANGUAGE.md](./LANGUAGE.md).
174
+
175
+ ## API
176
+
177
+ ### Main Functions
178
+
179
+ #### `evaluate(input: string | ASTNode, context?: ExecutionContext): RuntimeValue`
180
+
181
+ Evaluate an expression or AST and return the result.
182
+
183
+ ```typescript
184
+ evaluate("2 + 2"); // → 4
185
+
186
+ // Evaluate pre-parsed AST (parse once, evaluate many)
187
+ const ast = parse("price * quantity");
188
+ evaluate(ast, { variables: { price: 10, quantity: 5 } }); // → 50
189
+ evaluate(ast, { variables: { price: 20, quantity: 3 } }); // → 60
190
+ ```
191
+
192
+ #### `evaluateScope(input: string | ASTNode, context?: ExecutionContext): Record<string, RuntimeValue>`
193
+
194
+ Evaluate and return all assigned variables as a record.
195
+
196
+ ```typescript
197
+ evaluateScope("x = 10; y = x * 2"); // → { x: 10, y: 20 }
198
+ ```
199
+
200
+ #### `parse(source: string): ASTNode`
201
+
202
+ Parse source into an Abstract Syntax Tree without evaluating.
203
+
204
+ ```typescript
205
+ const ast = parse("2 + 3 * 4");
206
+ evaluate(ast); // → 14
207
+ ```
208
+
209
+ #### `generate(node: ASTNode): string`
210
+
211
+ Convert AST back to source code (preserves comments).
212
+
213
+ ```typescript
214
+ generate(parse("2 + 3 * 4")); // → "2 + 3 * 4"
215
+ ```
216
+
217
+ #### `optimize(node: ASTNode, externalVariables?: ReadonlySet<string>): ASTNode`
218
+
219
+ Optimize an AST with constant folding, constant propagation, and dead code elimination.
220
+
221
+ ```typescript
222
+ const ast = parse("2 + 3 * 4");
223
+ optimize(ast); // → NumberLiteral(14)
224
+
225
+ // With external variables: propagates internal constants while preserving external ones
226
+ const ast2 = parse("x = 5; y = 10; x + y");
227
+ optimize(ast2, new Set(["x"])); // Propagates y=10, keeps x as external
228
+ ```
229
+
230
+ #### `extractInputVariables(ast: ASTNode): string[]`
231
+
232
+ Extract variable names assigned to constant values (useful for building UIs with input controls).
233
+
234
+ ```typescript
235
+ const ast = parse("price = 100; tax = price * 0.08");
236
+ extractInputVariables(ast); // → ["price"]
237
+ ```
238
+
239
+ ### AST Visitor Pattern
240
+
241
+ #### `visit<T>(node: ASTNode, visitor: Visitor<T>): T`
242
+
243
+ Exhaustively visit every node in an AST. All 16 node types must be handled.
244
+
245
+ ```typescript
246
+ import { visit, parse } from "littlewing";
247
+
248
+ const count = visit(parse("2 + 3"), {
249
+ Program: (n, recurse) => n.statements.reduce((s, stmt) => s + recurse(stmt), 0),
250
+ NumberLiteral: () => 1,
251
+ StringLiteral: () => 1,
252
+ BooleanLiteral: () => 1,
253
+ ArrayLiteral: (n, recurse) => 1 + n.elements.reduce((s, el) => s + recurse(el), 0),
254
+ Identifier: () => 1,
255
+ BinaryOp: (n, recurse) => 1 + recurse(n.left) + recurse(n.right),
256
+ UnaryOp: (n, recurse) => 1 + recurse(n.argument),
257
+ Assignment: (n, recurse) => 1 + recurse(n.value),
258
+ FunctionCall: (n, recurse) => 1 + n.args.reduce((s, arg) => s + recurse(arg), 0),
259
+ IfExpression: (n, recurse) =>
260
+ 1 + recurse(n.condition) + recurse(n.consequent) + recurse(n.alternate),
261
+ ForExpression: (n, recurse) =>
262
+ 1 + recurse(n.iterable) + (n.guard ? recurse(n.guard) : 0) + recurse(n.body),
263
+ IndexAccess: (n, recurse) => 1 + recurse(n.object) + recurse(n.index),
264
+ RangeExpression: (n, recurse) => 1 + recurse(n.start) + recurse(n.end),
265
+ PipeExpression: (n, recurse) =>
266
+ 1 + recurse(n.value) + n.args.reduce((s, arg) => s + recurse(arg), 0),
267
+ Placeholder: () => 1,
268
+ });
269
+ ```
270
+
271
+ #### `visitPartial<T>(node, visitor, defaultHandler): T`
272
+
273
+ Visit only specific node types with a fallback for unhandled types.
274
+
275
+ ### AST Builder Functions
276
+
277
+ The `ast` namespace provides builder functions for constructing AST nodes:
278
+
279
+ ```typescript
280
+ import { ast, generate } from "littlewing";
281
+
282
+ generate(ast.add(ast.number(2), ast.number(3))); // → "2 + 3"
283
+ generate(ast.ifExpr(ast.boolean(true), ast.number(1), ast.number(0))); // → "if true then 1 else 0"
284
+ generate(
285
+ ast.forExpr(
286
+ "x",
287
+ ast.identifier("arr"),
288
+ null,
289
+ null,
290
+ ast.multiply(ast.identifier("x"), ast.number(2)),
291
+ ),
292
+ );
293
+ // → "for x in arr then x * 2"
294
+ ```
295
+
296
+ **Available builders:**
297
+
298
+ - Core: `program()`, `number()`, `string()`, `boolean()`, `array()`, `identifier()`, `binaryOp()`, `unaryOp()`, `functionCall()`, `assign()`, `ifExpr()`, `forExpr()`, `indexAccess()`, `rangeExpr()`, `pipeExpr()`, `placeholder()`
299
+ - Arithmetic: `add()`, `subtract()`, `multiply()`, `divide()`, `modulo()`, `exponentiate()`, `negate()`
300
+ - Comparison: `equals()`, `notEquals()`, `lessThan()`, `greaterThan()`, `lessEqual()`, `greaterEqual()`
301
+ - Logical: `logicalAnd()`, `logicalOr()`, `logicalNot()`
302
+
303
+ ### ExecutionContext
304
+
305
+ ```typescript
306
+ interface ExecutionContext {
307
+ functions?: Record<string, (...args: RuntimeValue[]) => RuntimeValue>;
308
+ variables?: Record<string, RuntimeValue>;
309
+ }
310
+
311
+ type RuntimeValue =
312
+ | number
313
+ | string
314
+ | boolean
315
+ | Temporal.PlainDate
316
+ | Temporal.PlainTime
317
+ | Temporal.PlainDateTime
318
+ | readonly RuntimeValue[];
319
+ ```
320
+
321
+ ### Default Context Functions
322
+
323
+ The `defaultContext` includes **82 built-in functions**:
324
+
325
+ **Core (8):** `STR`, `NUM`, `TYPE`, `LEN`, `SLICE`, `CONTAINS`, `REVERSE`, `INDEX_OF`
326
+
327
+ **Math (14):** `ABS`, `CEIL`, `FLOOR`, `ROUND`, `SQRT`, `MIN`, `MAX`, `CLAMP`, `SIN`, `COS`, `TAN`, `LOG`, `LOG10`, `EXP`
328
+
329
+ **String (8):** `STR_UPPER`, `STR_LOWER`, `STR_TRIM`, `STR_SPLIT`, `STR_REPLACE`, `STR_STARTS_WITH`, `STR_ENDS_WITH`, `STR_REPEAT`
330
+
331
+ **Array (7):** `ARR_SORT`, `ARR_UNIQUE`, `ARR_FLAT`, `ARR_JOIN`, `ARR_SUM`, `ARR_MIN`, `ARR_MAX`
332
+
333
+ **Date (25):** `TODAY`, `DATE`, `YEAR`, `MONTH`, `DAY`, `WEEKDAY`, `DAY_OF_YEAR`, `QUARTER`, `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS`, `DIFFERENCE_IN_DAYS`, `DIFFERENCE_IN_WEEKS`, `DIFFERENCE_IN_MONTHS`, `DIFFERENCE_IN_YEARS`, `START_OF_MONTH`, `END_OF_MONTH`, `START_OF_YEAR`, `END_OF_YEAR`, `START_OF_WEEK`, `START_OF_QUARTER`, `IS_SAME_DAY`, `IS_WEEKEND`, `IS_LEAP_YEAR`, `AGE`
334
+
335
+ **Time (13):** `TIME`, `NOW_TIME`, `HOUR`, `MINUTE`, `SECOND`, `MILLISECOND`, `ADD_HOURS`, `ADD_MINUTES`, `ADD_SECONDS`, `DIFFERENCE_IN_HOURS`, `DIFFERENCE_IN_MINUTES`, `DIFFERENCE_IN_SECONDS`, `IS_SAME_TIME`
336
+
337
+ **DateTime (7):** `DATETIME`, `NOW`, `TO_DATE`, `TO_TIME`, `COMBINE`, `START_OF_DAY`, `END_OF_DAY`
338
+
339
+ **Temporal type support:** Date functions accept both `PlainDate` and `PlainDateTime` (preserving type). Time functions accept both `PlainTime` and `PlainDateTime`. Difference functions require both arguments to be the same type.
340
+
341
+ ## Performance Optimization
342
+
343
+ ### Parse Once, Evaluate Many
344
+
345
+ For expressions executed multiple times, parse once and reuse the AST:
346
+
347
+ ```typescript
348
+ import { evaluate, parse } from "littlewing";
349
+
350
+ const formula = parse("price * quantity * (1 - discount)");
351
+
352
+ evaluate(formula, { variables: { price: 10, quantity: 5, discount: 0.1 } }); // → 45
353
+ evaluate(formula, { variables: { price: 20, quantity: 3, discount: 0.15 } }); // → 51
354
+ ```
355
+
356
+ ## Use Cases
357
+
358
+ - **User-defined formulas** — Let users write safe expressions
359
+ - **Business rules** — Express logic without `eval()` or `new Function()`
360
+ - **Financial calculators** — Compound interest, loan payments, etc.
361
+ - **Date arithmetic** — Deadlines, scheduling, date calculations
362
+ - **Data transformations** — Map, filter, and reduce arrays
363
+ - **Configuration expressions** — Dynamic config values
364
+
365
+ ## Why Littlewing?
366
+
367
+ ### The Problem
368
+
369
+ Your app needs to evaluate user-provided formulas or dynamic expressions. Using `eval()` is a security risk. Writing a parser is complex. Embedding a full scripting language is overkill.
370
+
371
+ ### The Solution
372
+
373
+ Littlewing provides just enough: expressions with multiple types, variables, and functions. It's safe (no code execution), fast (linear time), and type-safe (no implicit coercion).
374
+
375
+ ### What Makes It Different
376
+
377
+ 1. **Multi-type with strict semantics** — Seven types, no implicit coercion, no surprises
378
+ 2. **External variables override** — Scripts have defaults, runtime provides overrides
379
+ 3. **Full Temporal support** — First-class `PlainDate`, `PlainTime`, and `PlainDateTime`
380
+ 4. **Deep equality** — Arrays and dates compare by value
381
+ 5. **O(n) everything** — Predictable performance at any scale
382
+
383
+ ## Development
384
+
385
+ ```bash
386
+ bun install # Install dependencies
387
+ bun test # Run tests
388
+ bun run build # Build
389
+ bun run --cwd packages/littlewing dev # Watch mode
390
+ ```
391
+
392
+ For detailed development docs, see [CLAUDE.md](./CLAUDE.md).
393
+
394
+ ## License
395
+
396
+ MIT
397
+
398
+ ## Contributing
399
+
400
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
package/dist/index.d.ts CHANGED
@@ -320,6 +320,23 @@ declare function extractInputVariables(ast: ASTNode): string[];
320
320
  */
321
321
  declare function extractAssignedVariables(ast: ASTNode): string[];
322
322
  /**
323
+ * Error thrown when parsing fails. Carries byte offsets into the source string
324
+ * so callers can display precise error locations.
325
+ */
326
+ declare class ParseError extends Error {
327
+ readonly start: number;
328
+ readonly end: number;
329
+ readonly name = "ParseError";
330
+ constructor(message: string, start: number, end: number);
331
+ }
332
+ /**
333
+ * Convert a byte offset into a source string to a 1-based line and column.
334
+ */
335
+ declare function toLineColumn(source: string, offset: number): {
336
+ line: number;
337
+ column: number;
338
+ };
339
+ /**
323
340
  * Generate source code from an AST node
324
341
  */
325
342
  declare function generate(node: ASTNode): string;
@@ -357,31 +374,9 @@ declare function optimize(node: ASTNode, externalVariables?: ReadonlySet<string>
357
374
  */
358
375
  declare function parse(source: string): ASTNode;
359
376
  declare namespace array {
360
- export { ARR_UNIQUE, ARR_SUM, ARR_SORT, ARR_SLICE, ARR_REVERSE, ARR_PUSH, ARR_MIN, ARR_MAX, ARR_LEN, ARR_JOIN, ARR_FLAT, ARR_CONTAINS };
377
+ export { ARR_UNIQUE, ARR_SUM, ARR_SORT, ARR_MIN, ARR_MAX, ARR_JOIN, ARR_FLAT };
361
378
  }
362
379
  /**
363
- * Get the length of an array
364
- */
365
- declare const ARR_LEN: (a: RuntimeValue) => RuntimeValue;
366
- /**
367
- * Append a value to an array, returning a new array
368
- * Validates homogeneity
369
- */
370
- declare const ARR_PUSH: (a: RuntimeValue, value: RuntimeValue) => RuntimeValue;
371
- /**
372
- * Extract a section of an array (0-based indices)
373
- */
374
- declare const ARR_SLICE: (a: RuntimeValue, start: RuntimeValue, end?: RuntimeValue) => RuntimeValue;
375
- /**
376
- * Check if an array contains a value (using deep equality)
377
- * Returns boolean
378
- */
379
- declare const ARR_CONTAINS: (a: RuntimeValue, value: RuntimeValue) => RuntimeValue;
380
- /**
381
- * Reverse an array, returning a new array
382
- */
383
- declare const ARR_REVERSE: (a: RuntimeValue) => RuntimeValue;
384
- /**
385
380
  * Sort an array in ascending order, returning a new array.
386
381
  * Numbers sort numerically, strings lexicographically,
387
382
  * dates/times/datetimes by temporal comparison.
@@ -419,7 +414,7 @@ declare const ARR_MIN: (a: RuntimeValue) => RuntimeValue;
419
414
  */
420
415
  declare const ARR_MAX: (a: RuntimeValue) => RuntimeValue;
421
416
  declare namespace core {
422
- export { TYPE, STR, NUM };
417
+ export { TYPE, STR, SLICE, REVERSE, NUM, LEN, INDEX_OF, CONTAINS };
423
418
  }
424
419
  /**
425
420
  * Convert a value to string representation
@@ -436,8 +431,33 @@ declare const NUM: (v: RuntimeValue) => RuntimeValue;
436
431
  * Returns the type name of a value as a string
437
432
  */
438
433
  declare const TYPE: (v: RuntimeValue) => RuntimeValue;
434
+ /**
435
+ * Get the length of a string or array
436
+ */
437
+ declare const LEN: (v: RuntimeValue) => RuntimeValue;
438
+ /**
439
+ * Extract a section of a string or array (0-based indices)
440
+ */
441
+ declare const SLICE: (v: RuntimeValue, start: RuntimeValue, end?: RuntimeValue) => RuntimeValue;
442
+ /**
443
+ * Check if a string contains a substring or an array contains an element.
444
+ * For strings: both arguments must be strings (substring search).
445
+ * For arrays: uses deep equality to find the element.
446
+ */
447
+ declare const CONTAINS: (v: RuntimeValue, search: RuntimeValue) => RuntimeValue;
448
+ /**
449
+ * Reverse a string or array, returning a new value
450
+ */
451
+ declare const REVERSE: (v: RuntimeValue) => RuntimeValue;
452
+ /**
453
+ * Find the first index of a substring in a string or an element in an array.
454
+ * Returns -1 if not found.
455
+ * For strings: both arguments must be strings.
456
+ * For arrays: uses deep equality.
457
+ */
458
+ declare const INDEX_OF: (v: RuntimeValue, search: RuntimeValue) => RuntimeValue;
439
459
  declare namespace datetime {
440
- export { TODAY, START_OF_YEAR, START_OF_WEEK, START_OF_QUARTER, START_OF_MONTH, IS_WEEKEND, IS_SAME_DAY, IS_LEAP_YEAR, GET_YEAR, GET_WEEKDAY, GET_QUARTER, GET_MONTH, GET_DAY_OF_YEAR, GET_DAY, END_OF_YEAR, END_OF_MONTH, DIFFERENCE_IN_YEARS, DIFFERENCE_IN_WEEKS, DIFFERENCE_IN_MONTHS, DIFFERENCE_IN_DAYS, DATE, ADD_YEARS, ADD_MONTHS, ADD_DAYS };
460
+ export { YEAR, WEEKDAY, TODAY, START_OF_YEAR, START_OF_WEEK, START_OF_QUARTER, START_OF_MONTH, QUARTER, MONTH, IS_WEEKEND, IS_SAME_DAY, IS_LEAP_YEAR, END_OF_YEAR, END_OF_MONTH, DIFFERENCE_IN_YEARS, DIFFERENCE_IN_WEEKS, DIFFERENCE_IN_MONTHS, DIFFERENCE_IN_DAYS, DAY_OF_YEAR, DAY, DATE, AGE, ADD_YEARS, ADD_MONTHS, ADD_DAYS };
441
461
  }
442
462
  /**
443
463
  * Date utility functions using Temporal.PlainDate and Temporal.PlainDateTime
@@ -455,27 +475,27 @@ declare const DATE: (year: RuntimeValue, month: RuntimeValue, day: RuntimeValue)
455
475
  /**
456
476
  * Get the year from a date or datetime
457
477
  */
458
- declare const GET_YEAR: (date: RuntimeValue) => RuntimeValue;
478
+ declare const YEAR: (date: RuntimeValue) => RuntimeValue;
459
479
  /**
460
480
  * Get the month from a date or datetime (1-based: 1 = January, 12 = December)
461
481
  */
462
- declare const GET_MONTH: (date: RuntimeValue) => RuntimeValue;
482
+ declare const MONTH: (date: RuntimeValue) => RuntimeValue;
463
483
  /**
464
484
  * Get the day of month from a date or datetime (1-31)
465
485
  */
466
- declare const GET_DAY: (date: RuntimeValue) => RuntimeValue;
486
+ declare const DAY: (date: RuntimeValue) => RuntimeValue;
467
487
  /**
468
488
  * Get the day of week from a date or datetime (1 = Monday, 7 = Sunday)
469
489
  */
470
- declare const GET_WEEKDAY: (date: RuntimeValue) => RuntimeValue;
490
+ declare const WEEKDAY: (date: RuntimeValue) => RuntimeValue;
471
491
  /**
472
492
  * Get the day of year (1-366) from a date or datetime
473
493
  */
474
- declare const GET_DAY_OF_YEAR: (date: RuntimeValue) => RuntimeValue;
494
+ declare const DAY_OF_YEAR: (date: RuntimeValue) => RuntimeValue;
475
495
  /**
476
496
  * Get the quarter (1-4) from a date or datetime
477
497
  */
478
- declare const GET_QUARTER: (date: RuntimeValue) => RuntimeValue;
498
+ declare const QUARTER: (date: RuntimeValue) => RuntimeValue;
479
499
  /**
480
500
  * Add days to a date or datetime, returning the same type
481
501
  */
@@ -542,6 +562,12 @@ declare const IS_WEEKEND: (date: RuntimeValue) => RuntimeValue;
542
562
  * Check if a date or datetime is in a leap year
543
563
  */
544
564
  declare const IS_LEAP_YEAR: (date: RuntimeValue) => RuntimeValue;
565
+ /**
566
+ * Calculate age in complete years from a birth date.
567
+ * Accepts PlainDate or PlainDateTime (only the date portion is used).
568
+ * Optional second argument specifies the reference date (defaults to today).
569
+ */
570
+ declare const AGE: (birthDate: RuntimeValue, ...rest: RuntimeValue[]) => RuntimeValue;
545
571
  declare namespace datetimefull {
546
572
  export { TO_TIME, TO_DATE, START_OF_DAY, NOW, END_OF_DAY, DATETIME, COMBINE };
547
573
  }
@@ -601,13 +627,9 @@ declare const EXP: (x: RuntimeValue) => RuntimeValue;
601
627
  */
602
628
  declare const CLAMP: (value: RuntimeValue, min: RuntimeValue, max: RuntimeValue) => RuntimeValue;
603
629
  declare namespace string {
604
- export { STR_UPPER, STR_TRIM, STR_STARTS_WITH, STR_SPLIT, STR_SLICE, STR_REPLACE, STR_REPEAT, STR_LOWER, STR_LEN, STR_INDEX_OF, STR_ENDS_WITH, STR_CONTAINS };
630
+ export { STR_UPPER, STR_TRIM, STR_STARTS_WITH, STR_SPLIT, STR_REPLACE, STR_REPEAT, STR_LOWER, STR_ENDS_WITH };
605
631
  }
606
632
  /**
607
- * Get the length of a string
608
- */
609
- declare const STR_LEN: (s: RuntimeValue) => RuntimeValue;
610
- /**
611
633
  * Convert string to uppercase
612
634
  */
613
635
  declare const STR_UPPER: (s: RuntimeValue) => RuntimeValue;
@@ -620,19 +642,6 @@ declare const STR_LOWER: (s: RuntimeValue) => RuntimeValue;
620
642
  */
621
643
  declare const STR_TRIM: (s: RuntimeValue) => RuntimeValue;
622
644
  /**
623
- * Extract a section of a string (0-based indices)
624
- */
625
- declare const STR_SLICE: (s: RuntimeValue, start: RuntimeValue, end?: RuntimeValue) => RuntimeValue;
626
- /**
627
- * Check if a string contains another string
628
- * Returns boolean
629
- */
630
- declare const STR_CONTAINS: (s: RuntimeValue, search: RuntimeValue) => RuntimeValue;
631
- /**
632
- * Find the first index of a substring (-1 if not found)
633
- */
634
- declare const STR_INDEX_OF: (s: RuntimeValue, search: RuntimeValue) => RuntimeValue;
635
- /**
636
645
  * Split a string by a separator into a string array
637
646
  */
638
647
  declare const STR_SPLIT: (s: RuntimeValue, sep: RuntimeValue) => RuntimeValue;
@@ -653,7 +662,7 @@ declare const STR_ENDS_WITH: (s: RuntimeValue, suffix: RuntimeValue) => RuntimeV
653
662
  */
654
663
  declare const STR_REPEAT: (s: RuntimeValue, count: RuntimeValue) => RuntimeValue;
655
664
  declare namespace time {
656
- export { TIME, NOW_TIME, IS_SAME_TIME, GET_SECOND, GET_MINUTE, GET_MILLISECOND, GET_HOUR, DIFFERENCE_IN_SECONDS, DIFFERENCE_IN_MINUTES, DIFFERENCE_IN_HOURS, ADD_SECONDS, ADD_MINUTES, ADD_HOURS };
665
+ export { TIME, SECOND, NOW_TIME, MINUTE, MILLISECOND, IS_SAME_TIME, HOUR, DIFFERENCE_IN_SECONDS, DIFFERENCE_IN_MINUTES, DIFFERENCE_IN_HOURS, ADD_SECONDS, ADD_MINUTES, ADD_HOURS };
657
666
  }
658
667
  /**
659
668
  * Time utility functions using Temporal.PlainTime and Temporal.PlainDateTime
@@ -671,19 +680,19 @@ declare const NOW_TIME: () => RuntimeValue;
671
680
  /**
672
681
  * Get the hour (0-23) from a time or datetime
673
682
  */
674
- declare const GET_HOUR: (t: RuntimeValue) => RuntimeValue;
683
+ declare const HOUR: (t: RuntimeValue) => RuntimeValue;
675
684
  /**
676
685
  * Get the minute (0-59) from a time or datetime
677
686
  */
678
- declare const GET_MINUTE: (t: RuntimeValue) => RuntimeValue;
687
+ declare const MINUTE: (t: RuntimeValue) => RuntimeValue;
679
688
  /**
680
689
  * Get the second (0-59) from a time or datetime
681
690
  */
682
- declare const GET_SECOND: (t: RuntimeValue) => RuntimeValue;
691
+ declare const SECOND: (t: RuntimeValue) => RuntimeValue;
683
692
  /**
684
693
  * Get the millisecond (0-999) from a time or datetime
685
694
  */
686
- declare const GET_MILLISECOND: (t: RuntimeValue) => RuntimeValue;
695
+ declare const MILLISECOND: (t: RuntimeValue) => RuntimeValue;
687
696
  /**
688
697
  * Add hours to a time or datetime.
689
698
  * PlainTime wraps around at midnight boundaries.
@@ -825,4 +834,4 @@ declare function visit<T>(node: ASTNode, visitor: Visitor<T>): T;
825
834
  * @returns The result of visiting the node
826
835
  */
827
836
  declare function visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T>>, defaultHandler: VisitorHandler<ASTNode, T>): T;
828
- export { visitPartial, visit, typeOf, time, string, parse, optimize, math, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isPlaceholder, isPipeExpression, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, generate, extractInputVariables, extractAssignedVariables, evaluateScope, evaluate, defaultContext, datetimefull, datetime, core, exports_ast as ast, assertTimeOrDateTime, assertTime, assertString, assertNumber, assertDateTime, assertDateOrDateTime, assertDate, assertBoolean, assertArray, array, VisitorHandler, Visitor, UnaryOp, StringLiteral, RuntimeValue, RangeExpression, Program, Placeholder, PipeExpression, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, ExecutionContext, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
837
+ export { visitPartial, visit, typeOf, toLineColumn, time, string, parse, optimize, math, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isPlaceholder, isPipeExpression, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, generate, extractInputVariables, extractAssignedVariables, evaluateScope, evaluate, defaultContext, datetimefull, datetime, core, exports_ast as ast, assertTimeOrDateTime, assertTime, assertString, assertNumber, assertDateTime, assertDateOrDateTime, assertDate, assertBoolean, assertArray, array, VisitorHandler, Visitor, UnaryOp, StringLiteral, RuntimeValue, RangeExpression, Program, Placeholder, PipeExpression, ParseError, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, ExecutionContext, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
package/dist/index.js CHANGED
@@ -413,6 +413,31 @@ function containsExternalReference(node, boundVars) {
413
413
  function containsVariableReference(node) {
414
414
  return containsExternalReference(node, new Set);
415
415
  }
416
+ // src/errors.ts
417
+ class ParseError extends Error {
418
+ start;
419
+ end;
420
+ name = "ParseError";
421
+ constructor(message, start, end) {
422
+ super(message);
423
+ this.start = start;
424
+ this.end = end;
425
+ }
426
+ }
427
+ function toLineColumn(source, offset) {
428
+ let line = 1;
429
+ let column = 1;
430
+ const clamped = Math.min(offset, source.length);
431
+ for (let i = 0;i < clamped; i++) {
432
+ if (source.charCodeAt(i) === 10) {
433
+ line++;
434
+ column = 1;
435
+ } else {
436
+ column++;
437
+ }
438
+ }
439
+ return { line, column };
440
+ }
416
441
  // src/lexer.ts
417
442
  var KEYWORDS = new Map([
418
443
  ["if", 28 /* If */],
@@ -594,7 +619,7 @@ function nextToken(cursor) {
594
619
  }
595
620
  return [18 /* DotDot */, start, cursor.pos];
596
621
  }
597
- throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
622
+ throw new ParseError(`Unexpected character '${String.fromCharCode(ch)}'`, start, cursor.pos);
598
623
  case 44:
599
624
  advance(cursor);
600
625
  return [25 /* Comma */, start, cursor.pos];
@@ -607,7 +632,7 @@ function nextToken(cursor) {
607
632
  advance(cursor);
608
633
  return [16 /* And */, start, cursor.pos];
609
634
  }
610
- throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
635
+ throw new ParseError(`Unexpected character '${String.fromCharCode(ch)}'`, start, cursor.pos);
611
636
  case 124:
612
637
  advance(cursor);
613
638
  if (peek(cursor) === 124) {
@@ -618,12 +643,13 @@ function nextToken(cursor) {
618
643
  advance(cursor);
619
644
  return [26 /* Pipe */, start, cursor.pos];
620
645
  }
621
- throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
646
+ throw new ParseError(`Unexpected character '${String.fromCharCode(ch)}'`, start, cursor.pos);
622
647
  case 63:
623
648
  advance(cursor);
624
649
  return [27 /* Question */, start, cursor.pos];
625
650
  default:
626
- throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
651
+ advance(cursor);
652
+ throw new ParseError(`Unexpected character '${String.fromCharCode(ch)}'`, start, cursor.pos);
627
653
  }
628
654
  }
629
655
  function lexString(cursor) {
@@ -640,7 +666,7 @@ function lexString(cursor) {
640
666
  cursor.pos++;
641
667
  }
642
668
  }
643
- throw new Error(`Unterminated string at position ${start}`);
669
+ throw new ParseError("Unterminated string", start, cursor.pos);
644
670
  }
645
671
  function lexNumber(cursor) {
646
672
  const start = cursor.pos;
@@ -1230,13 +1256,13 @@ function parse(source) {
1230
1256
  statements.push(parseExpression(state, 0));
1231
1257
  }
1232
1258
  if (statements.length === 0) {
1233
- throw new Error("Empty program");
1259
+ throw new ParseError("Empty program", 0, 0);
1234
1260
  }
1235
1261
  const annotated = attachComments(source, statements, statementOffsets, cursor.comments);
1236
1262
  if (annotated.statements.length === 1 && !annotated.programTrailing) {
1237
1263
  const stmt = annotated.statements[0];
1238
1264
  if (stmt === undefined) {
1239
- throw new Error("Unexpected empty statements array");
1265
+ throw new ParseError("Unexpected empty statements array", 0, 0);
1240
1266
  }
1241
1267
  return stmt;
1242
1268
  }
@@ -1346,7 +1372,7 @@ function parseExpression(state, minPrecedence) {
1346
1372
  advance2(state);
1347
1373
  const index = parseExpression(state, 0);
1348
1374
  if (peekKind(state) !== 23 /* RBracket */) {
1349
- throw new Error("Expected closing bracket");
1375
+ throw new ParseError("Expected closing bracket", state.currentToken[1], state.currentToken[2]);
1350
1376
  }
1351
1377
  advance2(state);
1352
1378
  left = indexAccess(left, index);
@@ -1377,7 +1403,7 @@ function parsePrefix(state) {
1377
1403
  advance2(state);
1378
1404
  const expr = parseExpression(state, 0);
1379
1405
  if (peekKind(state) !== 21 /* RParen */) {
1380
- throw new Error("Expected closing parenthesis");
1406
+ throw new ParseError("Expected closing parenthesis", state.currentToken[1], state.currentToken[2]);
1381
1407
  }
1382
1408
  advance2(state);
1383
1409
  return expr;
@@ -1393,7 +1419,7 @@ function parsePrefix(state) {
1393
1419
  }
1394
1420
  }
1395
1421
  if (peekKind(state) !== 23 /* RBracket */) {
1396
- throw new Error("Expected closing bracket");
1422
+ throw new ParseError("Expected closing bracket", state.currentToken[1], state.currentToken[2]);
1397
1423
  }
1398
1424
  advance2(state);
1399
1425
  return array(elements);
@@ -1402,12 +1428,12 @@ function parsePrefix(state) {
1402
1428
  advance2(state);
1403
1429
  const condition = parseExpression(state, 0);
1404
1430
  if (peekKind(state) !== 29 /* Then */) {
1405
- throw new Error('Expected "then" in if expression');
1431
+ throw new ParseError('Expected "then" in if expression', state.currentToken[1], state.currentToken[2]);
1406
1432
  }
1407
1433
  advance2(state);
1408
1434
  const consequent = parseExpression(state, 0);
1409
1435
  if (peekKind(state) !== 30 /* Else */) {
1410
- throw new Error('Expected "else" in if expression');
1436
+ throw new ParseError('Expected "else" in if expression', state.currentToken[1], state.currentToken[2]);
1411
1437
  }
1412
1438
  advance2(state);
1413
1439
  const alternate = parseExpression(state, 0);
@@ -1416,12 +1442,12 @@ function parsePrefix(state) {
1416
1442
  if (tokenKind === 31 /* For */) {
1417
1443
  advance2(state);
1418
1444
  if (peekKind(state) !== 1 /* Identifier */) {
1419
- throw new Error('Expected identifier after "for"');
1445
+ throw new ParseError('Expected identifier after "for"', state.currentToken[1], state.currentToken[2]);
1420
1446
  }
1421
1447
  const variableName = readText(state.cursor, state.currentToken);
1422
1448
  advance2(state);
1423
1449
  if (peekKind(state) !== 32 /* In */) {
1424
- throw new Error('Expected "in" in for expression');
1450
+ throw new ParseError('Expected "in" in for expression', state.currentToken[1], state.currentToken[2]);
1425
1451
  }
1426
1452
  advance2(state);
1427
1453
  const iterable = parseExpression(state, 0);
@@ -1434,19 +1460,19 @@ function parsePrefix(state) {
1434
1460
  if (peekKind(state) === 34 /* Into */) {
1435
1461
  advance2(state);
1436
1462
  if (peekKind(state) !== 1 /* Identifier */) {
1437
- throw new Error('Expected identifier after "into"');
1463
+ throw new ParseError('Expected identifier after "into"', state.currentToken[1], state.currentToken[2]);
1438
1464
  }
1439
1465
  const accName = readText(state.cursor, state.currentToken);
1440
1466
  advance2(state);
1441
1467
  if (peekKind(state) !== 24 /* Eq */) {
1442
- throw new Error('Expected "=" after accumulator name');
1468
+ throw new ParseError('Expected "=" after accumulator name', state.currentToken[1], state.currentToken[2]);
1443
1469
  }
1444
1470
  advance2(state);
1445
1471
  const initial = parseExpression(state, 0);
1446
1472
  accumulator = { name: accName, initial };
1447
1473
  }
1448
1474
  if (peekKind(state) !== 29 /* Then */) {
1449
- throw new Error('Expected "then" in for expression');
1475
+ throw new ParseError('Expected "then" in for expression', state.currentToken[1], state.currentToken[2]);
1450
1476
  }
1451
1477
  advance2(state);
1452
1478
  const body = parseExpression(state, 0);
@@ -1478,7 +1504,7 @@ function parsePrefix(state) {
1478
1504
  advance2(state);
1479
1505
  const args = parseFunctionArguments(state);
1480
1506
  if (peekKind(state) !== 21 /* RParen */) {
1481
- throw new Error("Expected closing parenthesis");
1507
+ throw new ParseError("Expected closing parenthesis", state.currentToken[1], state.currentToken[2]);
1482
1508
  }
1483
1509
  advance2(state);
1484
1510
  return functionCall(name, args);
@@ -1487,13 +1513,13 @@ function parsePrefix(state) {
1487
1513
  }
1488
1514
  const token = state.currentToken;
1489
1515
  const tokenText = readText(state.cursor, token);
1490
- throw new Error(`Unexpected token: ${tokenText}`);
1516
+ throw new ParseError(`Unexpected token: ${tokenText}`, token[1], token[2]);
1491
1517
  }
1492
1518
  function parseInfix(state, left, precedence) {
1493
1519
  const tokenKind = peekKind(state);
1494
1520
  if (tokenKind === 24 /* Eq */) {
1495
1521
  if (left.kind !== 2 /* Identifier */) {
1496
- throw new Error("Invalid assignment target");
1522
+ throw new ParseError("Invalid assignment target", state.currentToken[1], state.currentToken[2]);
1497
1523
  }
1498
1524
  const identName = left.name;
1499
1525
  advance2(state);
@@ -1509,17 +1535,17 @@ function parseInfix(state, left, precedence) {
1509
1535
  if (tokenKind === 26 /* Pipe */) {
1510
1536
  advance2(state);
1511
1537
  if (peekKind(state) !== 1 /* Identifier */) {
1512
- throw new Error("Expected function name after |>");
1538
+ throw new ParseError("Expected function name after |>", state.currentToken[1], state.currentToken[2]);
1513
1539
  }
1514
1540
  const name = readText(state.cursor, state.currentToken);
1515
1541
  advance2(state);
1516
1542
  if (peekKind(state) !== 20 /* LParen */) {
1517
- throw new Error("Expected ( after function name in pipe expression");
1543
+ throw new ParseError("Expected ( after function name in pipe expression", state.currentToken[1], state.currentToken[2]);
1518
1544
  }
1519
1545
  advance2(state);
1520
1546
  const args = parsePipeArguments(state);
1521
1547
  if (peekKind(state) !== 21 /* RParen */) {
1522
- throw new Error("Expected closing parenthesis");
1548
+ throw new ParseError("Expected closing parenthesis", state.currentToken[1], state.currentToken[2]);
1523
1549
  }
1524
1550
  advance2(state);
1525
1551
  let hasPlaceholder = false;
@@ -1530,7 +1556,7 @@ function parseInfix(state, left, precedence) {
1530
1556
  }
1531
1557
  }
1532
1558
  if (!hasPlaceholder) {
1533
- throw new Error("Pipe expression requires at least one ? placeholder");
1559
+ throw new ParseError("Pipe expression requires at least one ? placeholder", state.currentToken[1], state.currentToken[2]);
1534
1560
  }
1535
1561
  return pipeExpr(left, name, args);
1536
1562
  }
@@ -1541,7 +1567,7 @@ function parseInfix(state, left, precedence) {
1541
1567
  const right = parseExpression(state, isRightAssociative ? precedence : precedence + 1);
1542
1568
  return binaryOp(left, operator, right);
1543
1569
  }
1544
- throw new Error("Unexpected token in infix position");
1570
+ throw new ParseError("Unexpected token in infix position", state.currentToken[1], state.currentToken[2]);
1545
1571
  }
1546
1572
  function parseFunctionArguments(state) {
1547
1573
  if (peekKind(state) === 21 /* RParen */) {
@@ -2296,52 +2322,11 @@ __export(exports_array, {
2296
2322
  ARR_UNIQUE: () => ARR_UNIQUE,
2297
2323
  ARR_SUM: () => ARR_SUM,
2298
2324
  ARR_SORT: () => ARR_SORT,
2299
- ARR_SLICE: () => ARR_SLICE,
2300
- ARR_REVERSE: () => ARR_REVERSE,
2301
- ARR_PUSH: () => ARR_PUSH,
2302
2325
  ARR_MIN: () => ARR_MIN,
2303
2326
  ARR_MAX: () => ARR_MAX,
2304
- ARR_LEN: () => ARR_LEN,
2305
2327
  ARR_JOIN: () => ARR_JOIN,
2306
- ARR_FLAT: () => ARR_FLAT,
2307
- ARR_CONTAINS: () => ARR_CONTAINS
2328
+ ARR_FLAT: () => ARR_FLAT
2308
2329
  });
2309
- var ARR_LEN = (a) => {
2310
- assertArray(a, "ARR_LEN");
2311
- return a.length;
2312
- };
2313
- var ARR_PUSH = (a, value) => {
2314
- assertArray(a, "ARR_PUSH");
2315
- if (a.length > 0) {
2316
- const elemType = typeOf(a[0]);
2317
- const valType = typeOf(value);
2318
- if (elemType !== valType) {
2319
- throw new TypeError(`ARR_PUSH: cannot push ${valType} into array<${elemType}>`);
2320
- }
2321
- }
2322
- return [...a, value];
2323
- };
2324
- var ARR_SLICE = (a, start, end) => {
2325
- assertArray(a, "ARR_SLICE");
2326
- assertNumber(start, "ARR_SLICE", "start");
2327
- if (end !== undefined) {
2328
- assertNumber(end, "ARR_SLICE", "end");
2329
- return a.slice(start, end);
2330
- }
2331
- return a.slice(start);
2332
- };
2333
- var ARR_CONTAINS = (a, value) => {
2334
- assertArray(a, "ARR_CONTAINS");
2335
- for (const elem of a) {
2336
- if (deepEquals(elem, value))
2337
- return true;
2338
- }
2339
- return false;
2340
- };
2341
- var ARR_REVERSE = (a) => {
2342
- assertArray(a, "ARR_REVERSE");
2343
- return [...a].reverse();
2344
- };
2345
2330
  var ARR_SORT = (a) => {
2346
2331
  assertArray(a, "ARR_SORT");
2347
2332
  if (a.length <= 1)
@@ -2473,7 +2458,12 @@ var exports_core = {};
2473
2458
  __export(exports_core, {
2474
2459
  TYPE: () => TYPE,
2475
2460
  STR: () => STR,
2476
- NUM: () => NUM
2461
+ SLICE: () => SLICE,
2462
+ REVERSE: () => REVERSE,
2463
+ NUM: () => NUM,
2464
+ LEN: () => LEN,
2465
+ INDEX_OF: () => INDEX_OF,
2466
+ CONTAINS: () => CONTAINS
2477
2467
  });
2478
2468
  var STR = (v) => {
2479
2469
  if (typeof v === "string")
@@ -2504,31 +2494,81 @@ var NUM = (v) => {
2504
2494
  var TYPE = (v) => {
2505
2495
  return typeOf(v);
2506
2496
  };
2497
+ function assertStringOrArray(v, context) {
2498
+ if (typeof v !== "string" && !Array.isArray(v)) {
2499
+ throw new TypeError(`${context} expected string or array, got ${typeOf(v)}`);
2500
+ }
2501
+ }
2502
+ var LEN = (v) => {
2503
+ assertStringOrArray(v, "LEN");
2504
+ return v.length;
2505
+ };
2506
+ var SLICE = (v, start, end) => {
2507
+ assertStringOrArray(v, "SLICE");
2508
+ assertNumber(start, "SLICE", "start");
2509
+ if (end !== undefined) {
2510
+ assertNumber(end, "SLICE", "end");
2511
+ return v.slice(start, end);
2512
+ }
2513
+ return v.slice(start);
2514
+ };
2515
+ var CONTAINS = (v, search) => {
2516
+ assertStringOrArray(v, "CONTAINS");
2517
+ if (typeof v === "string") {
2518
+ assertString(search, "CONTAINS (search)");
2519
+ return v.includes(search);
2520
+ }
2521
+ for (const elem of v) {
2522
+ if (deepEquals(elem, search))
2523
+ return true;
2524
+ }
2525
+ return false;
2526
+ };
2527
+ var REVERSE = (v) => {
2528
+ assertStringOrArray(v, "REVERSE");
2529
+ if (typeof v === "string") {
2530
+ return Array.from(v).reverse().join("");
2531
+ }
2532
+ return [...v].reverse();
2533
+ };
2534
+ var INDEX_OF = (v, search) => {
2535
+ assertStringOrArray(v, "INDEX_OF");
2536
+ if (typeof v === "string") {
2537
+ assertString(search, "INDEX_OF (search)");
2538
+ return v.indexOf(search);
2539
+ }
2540
+ for (let i = 0;i < v.length; i++) {
2541
+ if (deepEquals(v[i], search))
2542
+ return i;
2543
+ }
2544
+ return -1;
2545
+ };
2507
2546
 
2508
2547
  // src/stdlib/datetime.ts
2509
2548
  var exports_datetime = {};
2510
2549
  __export(exports_datetime, {
2550
+ YEAR: () => YEAR,
2551
+ WEEKDAY: () => WEEKDAY,
2511
2552
  TODAY: () => TODAY,
2512
2553
  START_OF_YEAR: () => START_OF_YEAR,
2513
2554
  START_OF_WEEK: () => START_OF_WEEK,
2514
2555
  START_OF_QUARTER: () => START_OF_QUARTER,
2515
2556
  START_OF_MONTH: () => START_OF_MONTH,
2557
+ QUARTER: () => QUARTER,
2558
+ MONTH: () => MONTH,
2516
2559
  IS_WEEKEND: () => IS_WEEKEND,
2517
2560
  IS_SAME_DAY: () => IS_SAME_DAY,
2518
2561
  IS_LEAP_YEAR: () => IS_LEAP_YEAR,
2519
- GET_YEAR: () => GET_YEAR,
2520
- GET_WEEKDAY: () => GET_WEEKDAY,
2521
- GET_QUARTER: () => GET_QUARTER,
2522
- GET_MONTH: () => GET_MONTH,
2523
- GET_DAY_OF_YEAR: () => GET_DAY_OF_YEAR,
2524
- GET_DAY: () => GET_DAY,
2525
2562
  END_OF_YEAR: () => END_OF_YEAR,
2526
2563
  END_OF_MONTH: () => END_OF_MONTH,
2527
2564
  DIFFERENCE_IN_YEARS: () => DIFFERENCE_IN_YEARS,
2528
2565
  DIFFERENCE_IN_WEEKS: () => DIFFERENCE_IN_WEEKS,
2529
2566
  DIFFERENCE_IN_MONTHS: () => DIFFERENCE_IN_MONTHS,
2530
2567
  DIFFERENCE_IN_DAYS: () => DIFFERENCE_IN_DAYS,
2568
+ DAY_OF_YEAR: () => DAY_OF_YEAR,
2569
+ DAY: () => DAY,
2531
2570
  DATE: () => DATE,
2571
+ AGE: () => AGE,
2532
2572
  ADD_YEARS: () => ADD_YEARS,
2533
2573
  ADD_MONTHS: () => ADD_MONTHS,
2534
2574
  ADD_DAYS: () => ADD_DAYS
@@ -2540,28 +2580,28 @@ var DATE = (year, month, day) => {
2540
2580
  assertNumber(day, "DATE", "day");
2541
2581
  return new Temporal.PlainDate(year, month, day);
2542
2582
  };
2543
- var GET_YEAR = (date) => {
2544
- assertDateOrDateTime(date, "GET_YEAR");
2583
+ var YEAR = (date) => {
2584
+ assertDateOrDateTime(date, "YEAR");
2545
2585
  return date.year;
2546
2586
  };
2547
- var GET_MONTH = (date) => {
2548
- assertDateOrDateTime(date, "GET_MONTH");
2587
+ var MONTH = (date) => {
2588
+ assertDateOrDateTime(date, "MONTH");
2549
2589
  return date.month;
2550
2590
  };
2551
- var GET_DAY = (date) => {
2552
- assertDateOrDateTime(date, "GET_DAY");
2591
+ var DAY = (date) => {
2592
+ assertDateOrDateTime(date, "DAY");
2553
2593
  return date.day;
2554
2594
  };
2555
- var GET_WEEKDAY = (date) => {
2556
- assertDateOrDateTime(date, "GET_WEEKDAY");
2595
+ var WEEKDAY = (date) => {
2596
+ assertDateOrDateTime(date, "WEEKDAY");
2557
2597
  return date.dayOfWeek;
2558
2598
  };
2559
- var GET_DAY_OF_YEAR = (date) => {
2560
- assertDateOrDateTime(date, "GET_DAY_OF_YEAR");
2599
+ var DAY_OF_YEAR = (date) => {
2600
+ assertDateOrDateTime(date, "DAY_OF_YEAR");
2561
2601
  return date.dayOfYear;
2562
2602
  };
2563
- var GET_QUARTER = (date) => {
2564
- assertDateOrDateTime(date, "GET_QUARTER");
2603
+ var QUARTER = (date) => {
2604
+ assertDateOrDateTime(date, "QUARTER");
2565
2605
  return Math.ceil(date.month / 3);
2566
2606
  };
2567
2607
  var ADD_DAYS = (date, days) => {
@@ -2676,6 +2716,22 @@ var IS_LEAP_YEAR = (date) => {
2676
2716
  assertDateOrDateTime(date, "IS_LEAP_YEAR");
2677
2717
  return date.inLeapYear;
2678
2718
  };
2719
+ var AGE = (birthDate, ...rest) => {
2720
+ assertDateOrDateTime(birthDate, "AGE");
2721
+ const birth = birthDate instanceof Temporal.PlainDateTime ? birthDate.toPlainDate() : birthDate;
2722
+ let reference;
2723
+ if (rest.length > 0) {
2724
+ const ref = rest[0];
2725
+ assertDateOrDateTime(ref, "AGE");
2726
+ reference = ref instanceof Temporal.PlainDateTime ? ref.toPlainDate() : ref;
2727
+ } else {
2728
+ reference = Temporal.Now.plainDateISO();
2729
+ }
2730
+ if (Temporal.PlainDate.compare(birth, reference) > 0) {
2731
+ throw new RangeError("AGE requires birth date to be on or before reference date");
2732
+ }
2733
+ return birth.until(reference, { largestUnit: "year" }).years;
2734
+ };
2679
2735
 
2680
2736
  // src/stdlib/datetimefull.ts
2681
2737
  var exports_datetimefull = {};
@@ -2822,19 +2878,11 @@ __export(exports_string, {
2822
2878
  STR_TRIM: () => STR_TRIM,
2823
2879
  STR_STARTS_WITH: () => STR_STARTS_WITH,
2824
2880
  STR_SPLIT: () => STR_SPLIT,
2825
- STR_SLICE: () => STR_SLICE,
2826
2881
  STR_REPLACE: () => STR_REPLACE,
2827
2882
  STR_REPEAT: () => STR_REPEAT,
2828
2883
  STR_LOWER: () => STR_LOWER,
2829
- STR_LEN: () => STR_LEN,
2830
- STR_INDEX_OF: () => STR_INDEX_OF,
2831
- STR_ENDS_WITH: () => STR_ENDS_WITH,
2832
- STR_CONTAINS: () => STR_CONTAINS
2884
+ STR_ENDS_WITH: () => STR_ENDS_WITH
2833
2885
  });
2834
- var STR_LEN = (s) => {
2835
- assertString(s, "STR_LEN");
2836
- return s.length;
2837
- };
2838
2886
  var STR_UPPER = (s) => {
2839
2887
  assertString(s, "STR_UPPER");
2840
2888
  return s.toUpperCase();
@@ -2847,25 +2895,6 @@ var STR_TRIM = (s) => {
2847
2895
  assertString(s, "STR_TRIM");
2848
2896
  return s.trim();
2849
2897
  };
2850
- var STR_SLICE = (s, start, end) => {
2851
- assertString(s, "STR_SLICE");
2852
- assertNumber(start, "STR_SLICE", "start");
2853
- if (end !== undefined) {
2854
- assertNumber(end, "STR_SLICE", "end");
2855
- return s.slice(start, end);
2856
- }
2857
- return s.slice(start);
2858
- };
2859
- var STR_CONTAINS = (s, search) => {
2860
- assertString(s, "STR_CONTAINS");
2861
- assertString(search, "STR_CONTAINS");
2862
- return s.includes(search);
2863
- };
2864
- var STR_INDEX_OF = (s, search) => {
2865
- assertString(s, "STR_INDEX_OF");
2866
- assertString(search, "STR_INDEX_OF");
2867
- return s.indexOf(search);
2868
- };
2869
2898
  var STR_SPLIT = (s, sep) => {
2870
2899
  assertString(s, "STR_SPLIT");
2871
2900
  assertString(sep, "STR_SPLIT (separator)");
@@ -2900,12 +2929,12 @@ var STR_REPEAT = (s, count) => {
2900
2929
  var exports_time = {};
2901
2930
  __export(exports_time, {
2902
2931
  TIME: () => TIME,
2932
+ SECOND: () => SECOND,
2903
2933
  NOW_TIME: () => NOW_TIME,
2934
+ MINUTE: () => MINUTE,
2935
+ MILLISECOND: () => MILLISECOND,
2904
2936
  IS_SAME_TIME: () => IS_SAME_TIME,
2905
- GET_SECOND: () => GET_SECOND,
2906
- GET_MINUTE: () => GET_MINUTE,
2907
- GET_MILLISECOND: () => GET_MILLISECOND,
2908
- GET_HOUR: () => GET_HOUR,
2937
+ HOUR: () => HOUR,
2909
2938
  DIFFERENCE_IN_SECONDS: () => DIFFERENCE_IN_SECONDS,
2910
2939
  DIFFERENCE_IN_MINUTES: () => DIFFERENCE_IN_MINUTES,
2911
2940
  DIFFERENCE_IN_HOURS: () => DIFFERENCE_IN_HOURS,
@@ -2920,20 +2949,20 @@ var TIME = (hour, minute, second) => {
2920
2949
  return new Temporal.PlainTime(hour, minute, second);
2921
2950
  };
2922
2951
  var NOW_TIME = () => Temporal.Now.plainTimeISO();
2923
- var GET_HOUR = (t) => {
2924
- assertTimeOrDateTime(t, "GET_HOUR");
2952
+ var HOUR = (t) => {
2953
+ assertTimeOrDateTime(t, "HOUR");
2925
2954
  return t.hour;
2926
2955
  };
2927
- var GET_MINUTE = (t) => {
2928
- assertTimeOrDateTime(t, "GET_MINUTE");
2956
+ var MINUTE = (t) => {
2957
+ assertTimeOrDateTime(t, "MINUTE");
2929
2958
  return t.minute;
2930
2959
  };
2931
- var GET_SECOND = (t) => {
2932
- assertTimeOrDateTime(t, "GET_SECOND");
2960
+ var SECOND = (t) => {
2961
+ assertTimeOrDateTime(t, "SECOND");
2933
2962
  return t.second;
2934
2963
  };
2935
- var GET_MILLISECOND = (t) => {
2936
- assertTimeOrDateTime(t, "GET_MILLISECOND");
2964
+ var MILLISECOND = (t) => {
2965
+ assertTimeOrDateTime(t, "MILLISECOND");
2937
2966
  return t.millisecond;
2938
2967
  };
2939
2968
  var ADD_HOURS = (t, hours) => {
@@ -3019,6 +3048,7 @@ export {
3019
3048
  visitPartial,
3020
3049
  visit,
3021
3050
  typeOf,
3051
+ toLineColumn,
3022
3052
  exports_time as time,
3023
3053
  exports_string as string,
3024
3054
  parse,
@@ -3060,5 +3090,6 @@ export {
3060
3090
  assertBoolean,
3061
3091
  assertArray,
3062
3092
  exports_array as array,
3093
+ ParseError,
3063
3094
  NodeKind
3064
3095
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlewing",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "A minimal, high-performance multi-type expression language with lexer, parser, and interpreter. Optimized for browsers with type-safe execution.",
5
5
  "keywords": [
6
6
  "calculator",
@@ -51,6 +51,7 @@
51
51
  "test": "bun test",
52
52
  "test:coverage": "bun test --coverage",
53
53
  "test:watch": "bun test --watch",
54
+ "prepublishOnly": "cp ../../README.md ../../LICENSE .",
54
55
  "release": "bumpp --commit --push --tag"
55
56
  },
56
57
  "devDependencies": {