littlewing 1.1.1 → 1.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/README.md CHANGED
@@ -25,11 +25,12 @@ evaluate("score = 85; grade = score >= 90 ? 100 : 90", {
25
25
  - **Numbers-only** - Every value is a number. Simple, predictable, fast.
26
26
  - **Zero dependencies** - 7.8 KB gzipped, perfect for browser bundles
27
27
  - **O(n) performance** - Linear time parsing and execution
28
- - **Safe evaluation** - No eval(), no code generation, no security risks
28
+ - **JIT compiler** - Optional compilation for 3-4x faster repeated execution
29
+ - **Safe evaluation** - Tree-walk interpreter requires no code generation
29
30
  - **Timestamp arithmetic** - Built-in date/time functions using numeric timestamps
30
31
  - **Extensible** - Add custom functions and variables via context
31
32
  - **Type-safe** - Full TypeScript support with strict types
32
- - **Excellent test coverage** - 502 tests with 99.45% function coverage, 98.57% line coverage
33
+ - **Excellent test coverage** - 607 tests with 99.45% function coverage, 98.57% line coverage
33
34
 
34
35
  ## Installation
35
36
 
@@ -174,6 +175,46 @@ evaluate(ast); // → 4
174
175
  evaluate(ast); // → 4 (no re-parsing)
175
176
  ```
176
177
 
178
+ #### `compile(input: string | ASTNode): CompiledExpression`
179
+
180
+ Compile source code or AST to a JavaScript function for faster repeated execution. Best for hot paths where the same expression is evaluated many times.
181
+
182
+ ```typescript
183
+ import { compile } from "littlewing";
184
+
185
+ // Compile once
186
+ const expr = compile("x * 2 + y");
187
+
188
+ // Execute many times with different contexts (5-10x faster than evaluate)
189
+ expr.execute({ variables: { x: 10, y: 5 } }); // → 25
190
+ expr.execute({ variables: { x: 20, y: 3 } }); // → 43
191
+ expr.execute({ variables: { x: 30, y: 1 } }); // → 61
192
+
193
+ // Inspect generated JavaScript code
194
+ console.log(expr.source); // Shows generated code for debugging
195
+ ```
196
+
197
+ **Performance (repeated execution after compilation):**
198
+
199
+ - Small scripts: **22.8x faster** (330.62 ns → 14.50 ns per execution)
200
+ - Medium scripts: **12.3x faster** (1.94 µs → 157.66 ns per execution)
201
+ - Large scripts: **99.9x faster** (9.13 µs → 91.39 ns per execution)
202
+
203
+ **Important:** Compilation has overhead. For one-time execution, use `evaluate()` instead:
204
+
205
+ - Small: `evaluate()` is 3.7x faster (1.43 µs vs 5.26 µs including compilation)
206
+ - Medium: `evaluate()` is 2.5x faster (7.13 µs vs 17.76 µs including compilation)
207
+ - Large: `evaluate()` is 1.7x faster (42.80 µs vs 71.73 µs including compilation)
208
+
209
+ **CompiledExpression interface:**
210
+
211
+ ```typescript
212
+ interface CompiledExpression {
213
+ execute(context?: ExecutionContext): number;
214
+ source: string; // Generated JavaScript code
215
+ }
216
+ ```
217
+
177
218
  #### `parse(source: string): ASTNode`
178
219
 
179
220
  Parse source into an Abstract Syntax Tree without evaluating. Useful for parse-once, execute-many scenarios.
@@ -415,7 +456,9 @@ The `defaultContext` includes these built-in functions:
415
456
 
416
457
  ## Performance Optimization
417
458
 
418
- For expressions that are executed multiple times with different contexts, parse once and reuse the AST:
459
+ ### Parse Once, Evaluate Many
460
+
461
+ For expressions executed multiple times, parse once and reuse the AST:
419
462
 
420
463
  ```typescript
421
464
  import { evaluate, parse } from "littlewing";
@@ -437,6 +480,57 @@ evaluate(formula, {
437
480
  // This avoids lexing and parsing overhead on every execution
438
481
  ```
439
482
 
483
+ ### JIT Compilation for Maximum Performance
484
+
485
+ For hot paths where the same expression is evaluated hundreds or thousands of times, use the JIT compiler:
486
+
487
+ ```typescript
488
+ import { compile } from "littlewing";
489
+
490
+ // Compile once (generates optimized JavaScript function)
491
+ const formula = compile("price * quantity * (1 - discount) * (1 + taxRate)");
492
+
493
+ // Execute many times with different contexts (5-10x faster than tree-walk interpreter)
494
+ formula.execute({
495
+ variables: { price: 10, quantity: 5, discount: 0.1, taxRate: 0.08 },
496
+ });
497
+ formula.execute({
498
+ variables: { price: 20, quantity: 3, discount: 0.15, taxRate: 0.08 },
499
+ });
500
+ formula.execute({
501
+ variables: { price: 15, quantity: 10, discount: 0.2, taxRate: 0.08 },
502
+ });
503
+ ```
504
+
505
+ **When to use JIT:**
506
+
507
+ - ✅ Same expression evaluated 10+ times with different variables
508
+ - ✅ Large scripts (>50 lines)
509
+ - ✅ Performance-critical loops
510
+ - ✅ Long-running applications that amortize compilation cost
511
+
512
+ **When to use tree-walk interpreter:**
513
+
514
+ - ✅ One-time or infrequent evaluation
515
+ - ✅ Small scripts (<10 lines)
516
+ - ✅ Compilation time matters more than execution speed
517
+
518
+ **Performance comparison:**
519
+
520
+ Execution time only (after compilation):
521
+
522
+ - Small scripts: JIT is **22.8x faster** (330.62 ns → 14.50 ns per execution)
523
+ - Medium scripts: JIT is **12.3x faster** (1.94 µs → 157.66 ns per execution)
524
+ - Large scripts: JIT is **99.9x faster** (9.13 µs → 91.39 ns per execution)
525
+
526
+ Full pipeline (including compilation overhead):
527
+
528
+ - Small scripts: `evaluate()` is **3.7x faster** (1.43 µs vs 5.26 µs)
529
+ - Medium scripts: `evaluate()` is **2.5x faster** (7.13 µs vs 17.76 µs)
530
+ - Large scripts: `evaluate()` is **1.7x faster** (42.80 µs vs 71.73 µs)
531
+
532
+ See [JIT_IMPLEMENTATION.md](./JIT_IMPLEMENTATION.md) for detailed performance analysis and usage patterns.
533
+
440
534
  ## Use Cases
441
535
 
442
536
  - **User-defined formulas** - Let users write safe arithmetic expressions
package/dist/index.d.ts CHANGED
@@ -207,6 +207,22 @@ declare function getNodeName(node: ASTNode): string;
207
207
  */
208
208
  declare function extractInputVariables(ast: ASTNode): string[];
209
209
  /**
210
+ * Extracts the names of all assigned variables from an AST.
211
+ *
212
+ * Walks the AST and collects the left-hand side name from every
213
+ * `Assignment` node. Returns deduplicated names in definition order.
214
+ *
215
+ * @param ast - The AST to analyze (can be a single statement or Program node)
216
+ * @returns Array of assigned variable names in definition order
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * const node = parse('totalTime = hours * 60; hoursRecovered = saved / 60')
221
+ * extractAssignedVariables(node) // ['totalTime', 'hoursRecovered']
222
+ * ```
223
+ */
224
+ declare function extractAssignedVariables(ast: ASTNode): string[];
225
+ /**
210
226
  * Generate source code from an AST node
211
227
  */
212
228
  declare function generate(node: ASTNode): string;
@@ -294,6 +310,85 @@ interface ExecutionContext {
294
310
  */
295
311
  declare function evaluate(input: string | ASTNode, context?: ExecutionContext): RuntimeValue;
296
312
  /**
313
+ * Evaluate source code or AST and return the full variable scope.
314
+ *
315
+ * Runs the same interpreter as `evaluate()` but instead of returning
316
+ * the last expression value, returns a record of all variables that
317
+ * exist in scope after execution (both script-assigned and context-provided).
318
+ *
319
+ * @param input - Either a source code string or an AST node
320
+ * @param context - Optional execution context with variables and functions
321
+ * @returns Record of all variable names to their values after execution
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const scope = evaluateScope('x = 10; y = x * 2')
326
+ * // scope === { x: 10, y: 20 }
327
+ * ```
328
+ */
329
+ declare function evaluateScope(input: string | ASTNode, context?: ExecutionContext): Record<string, number>;
330
+ /**
331
+ * Compiled expression that can be executed multiple times
332
+ * Provides faster repeated execution compared to tree-walk interpreter
333
+ */
334
+ interface CompiledExpression {
335
+ /**
336
+ * Execute the compiled expression with optional context
337
+ * @param context - Execution context with variables and functions
338
+ * @returns The evaluated result
339
+ */
340
+ execute: (context?: ExecutionContext) => RuntimeValue;
341
+ /**
342
+ * Generated JavaScript source code (for debugging and inspection)
343
+ */
344
+ source: string;
345
+ }
346
+ /**
347
+ * Compile source code or AST to a JavaScript function for faster repeated execution
348
+ *
349
+ * JIT compilation trades compilation time for execution speed:
350
+ * - First call: Parse + Optimize + Compile (~10-50µs depending on size)
351
+ * - Subsequent calls: Direct JavaScript execution (~1-5µs, 5-10x faster)
352
+ *
353
+ * The compiler automatically optimizes the AST before code generation:
354
+ * - Constant folding: `2 + 3` → `5` (safe constants only)
355
+ * - Dead code elimination: removes unused variable assignments
356
+ * - Expression simplification: `1 ? x : y` → `x`
357
+ *
358
+ * Optimization is safe because:
359
+ * - Constant folding errors (e.g., division by zero) propagate to compile-time
360
+ * - Variables that could be external are preserved (no unsafe DCE)
361
+ * - The optimizer respects ExecutionContext override semantics
362
+ *
363
+ * The compiler generates optimized JavaScript code:
364
+ * - Uses comma operator instead of IIFEs for better performance
365
+ * - Only initializes variables that are actually used
366
+ * - Pre-declares temporary variables for reuse
367
+ *
368
+ * Best for expressions that are evaluated many times with different contexts.
369
+ *
370
+ * Security: Uses `new Function()` to generate executable code. Safe for trusted
371
+ * input only. For untrusted input, use the tree-walk interpreter instead.
372
+ *
373
+ * @param input - Either a source code string or an AST node
374
+ * @returns Compiled expression that can be executed multiple times
375
+ * @throws {Error} If compilation fails or AST contains invalid operations (e.g., constant division by zero)
376
+ *
377
+ * @example
378
+ * ```typescript
379
+ * const expr = compile('x * 2 + y')
380
+ * expr.execute({ variables: { x: 10, y: 5 } }) // 25
381
+ * expr.execute({ variables: { x: 20, y: 3 } }) // 43
382
+ * ```
383
+ *
384
+ * @example Automatic optimization
385
+ * ```typescript
386
+ * const expr = compile('2 + 3 * 4') // Automatically optimized to 14
387
+ * expr.execute() // Fast execution of pre-folded constant
388
+ * ```
389
+ */
390
+ declare function compile(input: string | ASTNode): CompiledExpression;
391
+ /**
297
392
  * Optimize an AST using constant folding, expression simplification, and dead code elimination.
298
393
  *
299
394
  * This optimizer performs SAFE optimizations that preserve program semantics:
@@ -763,4 +858,4 @@ declare function visit<T>(node: ASTNode, visitor: Visitor<T>): T;
763
858
  * )
764
859
  */
765
860
  declare function visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T>>, defaultHandler: (node: ASTNode, recurse: (n: ASTNode) => T) => T): T;
766
- export { visitPartial, visit, parse, optimize, exports_math as math, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, humanize, generate, extractInputVariables, evaluate, defaultContext, exports_datetime as datetime, exports_ast as ast, Visitor, UnaryOp, RuntimeValue, Program, Operator, NumberLiteral, NodeKind, Identifier, HumanizeOptions, FunctionCall, ExecutionContext, ConditionalExpression, BinaryOp, Assignment, ASTNode };
861
+ export { visitPartial, visit, parse, optimize, exports_math as math, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, humanize, generate, extractInputVariables, extractAssignedVariables, evaluateScope, evaluate, defaultContext, exports_datetime as datetime, compile, exports_ast as ast, Visitor, UnaryOp, RuntimeValue, Program, Operator, NumberLiteral, NodeKind, Identifier, HumanizeOptions, FunctionCall, ExecutionContext, ConditionalExpression, CompiledExpression, BinaryOp, Assignment, ASTNode };
package/dist/index.js CHANGED
@@ -574,6 +574,26 @@ function extractInputVariables(ast) {
574
574
  }
575
575
  return Array.from(inputVars);
576
576
  }
577
+ function extractAssignedVariables(ast) {
578
+ const seen = new Set;
579
+ const names = [];
580
+ visitPartial(ast, {
581
+ Program: (n, recurse) => {
582
+ for (const statement of n[1]) {
583
+ recurse(statement);
584
+ }
585
+ },
586
+ Assignment: (n, recurse) => {
587
+ const name = n[1];
588
+ if (!seen.has(name)) {
589
+ seen.add(name);
590
+ names.push(name);
591
+ }
592
+ recurse(n[2]);
593
+ }
594
+ }, () => {});
595
+ return names;
596
+ }
577
597
  function containsVariableReference(node) {
578
598
  return collectAllIdentifiers(node).size > 0;
579
599
  }
@@ -1002,13 +1022,13 @@ function evaluateNode(node, context, variables, externalVariables) {
1002
1022
  Assignment: (n, recurse) => {
1003
1023
  const name = n[1];
1004
1024
  const valueNode = n[2];
1025
+ const value = recurse(valueNode);
1005
1026
  if (externalVariables.has(name)) {
1006
1027
  const externalValue = variables.get(name);
1007
1028
  if (externalValue !== undefined) {
1008
1029
  return externalValue;
1009
1030
  }
1010
1031
  }
1011
- const value = recurse(valueNode);
1012
1032
  variables.set(name, value);
1013
1033
  return value;
1014
1034
  },
@@ -1020,12 +1040,20 @@ function evaluateNode(node, context, variables, externalVariables) {
1020
1040
  }
1021
1041
  });
1022
1042
  }
1023
- function evaluate(input, context = {}) {
1043
+ function run(input, context = {}) {
1024
1044
  const node = typeof input === "string" ? parse(input) : input;
1025
1045
  const entries = Object.entries(context.variables || {});
1026
1046
  const variables = new Map(entries);
1027
1047
  const externalVariables = new Set(entries.map(([key]) => key));
1028
- return evaluateNode(node, context, variables, externalVariables);
1048
+ const value = evaluateNode(node, context, variables, externalVariables);
1049
+ return { value, variables };
1050
+ }
1051
+ function evaluate(input, context = {}) {
1052
+ return run(input, context).value;
1053
+ }
1054
+ function evaluateScope(input, context = {}) {
1055
+ const { variables } = run(input, context);
1056
+ return Object.fromEntries(variables);
1029
1057
  }
1030
1058
  // src/optimizer.ts
1031
1059
  function eliminateDeadCode(program2) {
@@ -1039,7 +1067,7 @@ function eliminateDeadCode(program2) {
1039
1067
  if (i === statements.length - 1) {
1040
1068
  keptIndices.push(i);
1041
1069
  const identifiers = collectAllIdentifiers(stmt);
1042
- for (const id of identifiers) {
1070
+ for (const id of Array.from(identifiers)) {
1043
1071
  liveVars.add(id);
1044
1072
  }
1045
1073
  continue;
@@ -1050,14 +1078,14 @@ function eliminateDeadCode(program2) {
1050
1078
  if (liveVars.has(name)) {
1051
1079
  keptIndices.push(i);
1052
1080
  const identifiers = collectAllIdentifiers(value);
1053
- for (const id of identifiers) {
1081
+ for (const id of Array.from(identifiers)) {
1054
1082
  liveVars.add(id);
1055
1083
  }
1056
1084
  }
1057
1085
  } else {
1058
1086
  keptIndices.push(i);
1059
1087
  const identifiers = collectAllIdentifiers(stmt);
1060
- for (const id of identifiers) {
1088
+ for (const id of Array.from(identifiers)) {
1061
1089
  liveVars.add(id);
1062
1090
  }
1063
1091
  }
@@ -1140,6 +1168,248 @@ function optimize(node) {
1140
1168
  }
1141
1169
  return folded;
1142
1170
  }
1171
+
1172
+ // src/jit.ts
1173
+ function compile(input) {
1174
+ const parsedAst = typeof input === "string" ? parse(input) : input;
1175
+ const ast = optimize(parsedAst);
1176
+ const jsCode = generateJavaScript(ast);
1177
+ const fullCode = `${HELPER_FUNCTIONS}
1178
+
1179
+ ${jsCode}`;
1180
+ const fn = new Function("__context", fullCode);
1181
+ return {
1182
+ execute: (context = {}) => {
1183
+ return fn(context);
1184
+ },
1185
+ source: fullCode
1186
+ };
1187
+ }
1188
+ var HELPER_FUNCTIONS = `
1189
+ // Helper function for throwing errors (avoids string concatenation overhead)
1190
+ function __throw(msg) { throw new Error(msg); }
1191
+
1192
+ // Temporary variables for optimized operations
1193
+ let __r; // For division/modulo right operand
1194
+ let __fn; // For function lookup
1195
+ let __val; // For assignment value
1196
+ `.trim();
1197
+ function generateJavaScript(node) {
1198
+ const lines = [];
1199
+ const assignedVars = collectAssignedVariables(node);
1200
+ const referencedVars = collectAllIdentifiers(node);
1201
+ const literalAssignments = collectLiteralAssignments(node);
1202
+ lines.push("// Context accessors");
1203
+ lines.push("const __ext = __context.variables || {};");
1204
+ lines.push("const __fns = __context.functions || {};");
1205
+ lines.push("const __extKeys = new Set(Object.keys(__ext));");
1206
+ lines.push("");
1207
+ const allVars = new Set([
1208
+ ...Array.from(assignedVars),
1209
+ ...Array.from(referencedVars)
1210
+ ]);
1211
+ if (allVars.size > 0) {
1212
+ lines.push("// Variable declarations");
1213
+ for (const name of Array.from(allVars)) {
1214
+ const literalValue = literalAssignments.get(name);
1215
+ if (literalValue !== undefined) {
1216
+ lines.push(`let ${name} = __ext.${name} ?? ${literalValue};`);
1217
+ } else {
1218
+ lines.push(`let ${name} = __ext.${name};`);
1219
+ }
1220
+ }
1221
+ lines.push("");
1222
+ }
1223
+ lines.push("// Execute program");
1224
+ const code = compileNode(node, literalAssignments);
1225
+ lines.push(`return ${code};`);
1226
+ return lines.join(`
1227
+ `);
1228
+ }
1229
+ function collectAssignedVariables(node) {
1230
+ const names = new Set;
1231
+ visit(node, {
1232
+ Program: (n, recurse) => {
1233
+ for (const stmt of n[1]) {
1234
+ recurse(stmt);
1235
+ }
1236
+ },
1237
+ NumberLiteral: () => {},
1238
+ Identifier: () => {},
1239
+ BinaryOp: (n, recurse) => {
1240
+ recurse(n[1]);
1241
+ recurse(n[3]);
1242
+ },
1243
+ UnaryOp: (n, recurse) => {
1244
+ recurse(n[2]);
1245
+ },
1246
+ FunctionCall: (n, recurse) => {
1247
+ for (const arg of n[2]) {
1248
+ recurse(arg);
1249
+ }
1250
+ },
1251
+ Assignment: (n, recurse) => {
1252
+ names.add(n[1]);
1253
+ recurse(n[2]);
1254
+ },
1255
+ ConditionalExpression: (n, recurse) => {
1256
+ recurse(n[1]);
1257
+ recurse(n[2]);
1258
+ recurse(n[3]);
1259
+ }
1260
+ });
1261
+ return names;
1262
+ }
1263
+ function collectLiteralAssignments(node) {
1264
+ const literals = new Map;
1265
+ const nonLiterals = new Set;
1266
+ visit(node, {
1267
+ Program: (n, recurse) => {
1268
+ for (const stmt of n[1]) {
1269
+ recurse(stmt);
1270
+ }
1271
+ },
1272
+ NumberLiteral: () => {},
1273
+ Identifier: () => {},
1274
+ BinaryOp: (n, recurse) => {
1275
+ recurse(n[1]);
1276
+ recurse(n[3]);
1277
+ },
1278
+ UnaryOp: (n, recurse) => {
1279
+ recurse(n[2]);
1280
+ },
1281
+ FunctionCall: (n, recurse) => {
1282
+ for (const arg of n[2]) {
1283
+ recurse(arg);
1284
+ }
1285
+ },
1286
+ Assignment: (n, recurse) => {
1287
+ const name = n[1];
1288
+ const value = n[2];
1289
+ if (literals.has(name) || nonLiterals.has(name)) {
1290
+ literals.delete(name);
1291
+ nonLiterals.add(name);
1292
+ return;
1293
+ }
1294
+ if (value[0] === 1 /* NumberLiteral */) {
1295
+ const num = value[1];
1296
+ if (Number.isNaN(num)) {
1297
+ literals.set(name, "NaN");
1298
+ } else if (!Number.isFinite(num)) {
1299
+ literals.set(name, num > 0 ? "Infinity" : "-Infinity");
1300
+ } else {
1301
+ literals.set(name, String(num));
1302
+ }
1303
+ } else {
1304
+ nonLiterals.add(name);
1305
+ recurse(value);
1306
+ }
1307
+ },
1308
+ ConditionalExpression: (n, recurse) => {
1309
+ recurse(n[1]);
1310
+ recurse(n[2]);
1311
+ recurse(n[3]);
1312
+ }
1313
+ });
1314
+ return literals;
1315
+ }
1316
+ function compileNode(node, literalAssignments = new Map) {
1317
+ const compileIdentifier = (name) => {
1318
+ if (literalAssignments.has(name)) {
1319
+ return name;
1320
+ }
1321
+ return `(${name} !== undefined ? ${name} : __throw('Undefined variable: ${name}'))`;
1322
+ };
1323
+ return visit(node, {
1324
+ Program: (n, recurse) => {
1325
+ const statements = n[1];
1326
+ if (statements.length === 0) {
1327
+ return "0";
1328
+ }
1329
+ if (statements.length === 1) {
1330
+ const stmt = statements[0];
1331
+ return stmt ? recurse(stmt) : "0";
1332
+ }
1333
+ const stmts = statements.map(recurse);
1334
+ return `(${stmts.join(", ")})`;
1335
+ },
1336
+ NumberLiteral: (n) => {
1337
+ const value = n[1];
1338
+ if (Number.isNaN(value))
1339
+ return "NaN";
1340
+ if (!Number.isFinite(value)) {
1341
+ return value > 0 ? "Infinity" : "-Infinity";
1342
+ }
1343
+ return String(value);
1344
+ },
1345
+ Identifier: (n) => {
1346
+ const name = n[1];
1347
+ return compileIdentifier(name);
1348
+ },
1349
+ BinaryOp: (n, recurse) => {
1350
+ const left = recurse(n[1]);
1351
+ const operator = n[2];
1352
+ const right = recurse(n[3]);
1353
+ switch (operator) {
1354
+ case "^":
1355
+ return `(${left} ** ${right})`;
1356
+ case "==":
1357
+ return `(${left} === ${right} ? 1 : 0)`;
1358
+ case "!=":
1359
+ return `(${left} !== ${right} ? 1 : 0)`;
1360
+ case "<":
1361
+ return `(${left} < ${right} ? 1 : 0)`;
1362
+ case ">":
1363
+ return `(${left} > ${right} ? 1 : 0)`;
1364
+ case "<=":
1365
+ return `(${left} <= ${right} ? 1 : 0)`;
1366
+ case ">=":
1367
+ return `(${left} >= ${right} ? 1 : 0)`;
1368
+ case "&&":
1369
+ return `(${left} !== 0 && ${right} !== 0 ? 1 : 0)`;
1370
+ case "||":
1371
+ return `(${left} !== 0 || ${right} !== 0 ? 1 : 0)`;
1372
+ case "/":
1373
+ return `((__r = ${right}) === 0 && __throw('Division by zero'), ${left} / __r)`;
1374
+ case "%":
1375
+ return `((__r = ${right}) === 0 && __throw('Modulo by zero'), ${left} % __r)`;
1376
+ default:
1377
+ return `(${left} ${operator} ${right})`;
1378
+ }
1379
+ },
1380
+ UnaryOp: (n, recurse) => {
1381
+ const operator = n[1];
1382
+ const arg = recurse(n[2]);
1383
+ if (operator === "-") {
1384
+ return `(-${arg})`;
1385
+ }
1386
+ if (operator === "!") {
1387
+ return `(${arg} === 0 ? 1 : 0)`;
1388
+ }
1389
+ throw new Error(`Unknown unary operator: ${operator}`);
1390
+ },
1391
+ FunctionCall: (n, recurse) => {
1392
+ const name = n[1];
1393
+ const args = n[2].map(recurse);
1394
+ const argsStr = args.length > 0 ? args.join(", ") : "";
1395
+ return `((__fn = __fns.${name}) === undefined ? __throw('Undefined function: ${name}') : typeof __fn !== 'function' ? __throw('${name} is not a function') : __fn(${argsStr}))`;
1396
+ },
1397
+ Assignment: (n, recurse) => {
1398
+ const name = n[1];
1399
+ if (literalAssignments.has(name)) {
1400
+ return name;
1401
+ }
1402
+ const value = recurse(n[2]);
1403
+ return `((__val = ${value}), (__extKeys.has('${name}') ? ${name} : (${name} = __val)))`;
1404
+ },
1405
+ ConditionalExpression: (n, recurse) => {
1406
+ const condition = recurse(n[1]);
1407
+ const consequent = recurse(n[2]);
1408
+ const alternate = recurse(n[3]);
1409
+ return `(${condition} !== 0 ? ${consequent} : ${alternate})`;
1410
+ }
1411
+ });
1412
+ }
1143
1413
  // src/stdlib/datetime.ts
1144
1414
  var exports_datetime = {};
1145
1415
  __export(exports_datetime, {
@@ -1357,9 +1627,12 @@ export {
1357
1627
  humanize,
1358
1628
  generate,
1359
1629
  extractInputVariables,
1630
+ extractAssignedVariables,
1631
+ evaluateScope,
1360
1632
  evaluate,
1361
1633
  defaultContext,
1362
1634
  exports_datetime as datetime,
1635
+ compile,
1363
1636
  exports_ast as ast,
1364
1637
  NodeKind
1365
1638
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlewing",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A minimal, high-performance arithmetic expression language with lexer, parser, and executor. Optimized for browsers with zero dependencies and type-safe execution.",
5
5
  "keywords": [
6
6
  "arithmetic",