littlewing 1.1.0 → 1.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/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
@@ -294,6 +294,67 @@ interface ExecutionContext {
294
294
  */
295
295
  declare function evaluate(input: string | ASTNode, context?: ExecutionContext): RuntimeValue;
296
296
  /**
297
+ * Compiled expression that can be executed multiple times
298
+ * Provides faster repeated execution compared to tree-walk interpreter
299
+ */
300
+ interface CompiledExpression {
301
+ /**
302
+ * Execute the compiled expression with optional context
303
+ * @param context - Execution context with variables and functions
304
+ * @returns The evaluated result
305
+ */
306
+ execute: (context?: ExecutionContext) => RuntimeValue;
307
+ /**
308
+ * Generated JavaScript source code (for debugging and inspection)
309
+ */
310
+ source: string;
311
+ }
312
+ /**
313
+ * Compile source code or AST to a JavaScript function for faster repeated execution
314
+ *
315
+ * JIT compilation trades compilation time for execution speed:
316
+ * - First call: Parse + Optimize + Compile (~10-50µs depending on size)
317
+ * - Subsequent calls: Direct JavaScript execution (~1-5µs, 5-10x faster)
318
+ *
319
+ * The compiler automatically optimizes the AST before code generation:
320
+ * - Constant folding: `2 + 3` → `5` (safe constants only)
321
+ * - Dead code elimination: removes unused variable assignments
322
+ * - Expression simplification: `1 ? x : y` → `x`
323
+ *
324
+ * Optimization is safe because:
325
+ * - Constant folding errors (e.g., division by zero) propagate to compile-time
326
+ * - Variables that could be external are preserved (no unsafe DCE)
327
+ * - The optimizer respects ExecutionContext override semantics
328
+ *
329
+ * The compiler generates optimized JavaScript code:
330
+ * - Uses comma operator instead of IIFEs for better performance
331
+ * - Only initializes variables that are actually used
332
+ * - Pre-declares temporary variables for reuse
333
+ *
334
+ * Best for expressions that are evaluated many times with different contexts.
335
+ *
336
+ * Security: Uses `new Function()` to generate executable code. Safe for trusted
337
+ * input only. For untrusted input, use the tree-walk interpreter instead.
338
+ *
339
+ * @param input - Either a source code string or an AST node
340
+ * @returns Compiled expression that can be executed multiple times
341
+ * @throws {Error} If compilation fails or AST contains invalid operations (e.g., constant division by zero)
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * const expr = compile('x * 2 + y')
346
+ * expr.execute({ variables: { x: 10, y: 5 } }) // 25
347
+ * expr.execute({ variables: { x: 20, y: 3 } }) // 43
348
+ * ```
349
+ *
350
+ * @example Automatic optimization
351
+ * ```typescript
352
+ * const expr = compile('2 + 3 * 4') // Automatically optimized to 14
353
+ * expr.execute() // Fast execution of pre-folded constant
354
+ * ```
355
+ */
356
+ declare function compile(input: string | ASTNode): CompiledExpression;
357
+ /**
297
358
  * Optimize an AST using constant folding, expression simplification, and dead code elimination.
298
359
  *
299
360
  * This optimizer performs SAFE optimizations that preserve program semantics:
@@ -763,4 +824,4 @@ declare function visit<T>(node: ASTNode, visitor: Visitor<T>): T;
763
824
  * )
764
825
  */
765
826
  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 };
827
+ 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, 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
@@ -194,18 +194,23 @@ function advance(cursor) {
194
194
  return char;
195
195
  }
196
196
  function skipWhitespace(cursor) {
197
- let c = peek(cursor);
198
- while (c === 32 || c === 10 || c === 13 || c === 9) {
199
- advance(cursor);
200
- c = peek(cursor);
197
+ while (cursor.pos < cursor.len) {
198
+ const c = cursor.source.charCodeAt(cursor.pos);
199
+ if (c === 32 || c === 10 || c === 13 || c === 9) {
200
+ cursor.pos++;
201
+ } else {
202
+ break;
203
+ }
201
204
  }
202
205
  }
203
206
  function skipComment(cursor) {
204
- if (peek(cursor) === 47 && peek(cursor, 1) === 47) {
205
- let ch = peek(cursor);
206
- while (ch !== 10 && ch !== 13 && ch !== 0) {
207
- advance(cursor);
208
- ch = peek(cursor);
207
+ if (cursor.pos < cursor.len - 1 && cursor.source.charCodeAt(cursor.pos) === 47 && cursor.source.charCodeAt(cursor.pos + 1) === 47) {
208
+ cursor.pos += 2;
209
+ while (cursor.pos < cursor.len) {
210
+ const ch = cursor.source.charCodeAt(cursor.pos);
211
+ if (ch === 10 || ch === 13)
212
+ break;
213
+ cursor.pos++;
209
214
  }
210
215
  }
211
216
  }
@@ -326,53 +331,68 @@ function nextToken(cursor) {
326
331
  }
327
332
  function lexNumber(cursor) {
328
333
  const start = cursor.pos;
329
- let hasDecimal = false;
330
- let hasExponent = false;
331
- let ch = peek(cursor);
332
- if (ch === 46) {
333
- hasDecimal = true;
334
- advance(cursor);
335
- ch = peek(cursor);
334
+ const { source, len } = cursor;
335
+ let pos = cursor.pos;
336
+ if (source.charCodeAt(pos) === 46) {
337
+ pos++;
338
+ while (pos < len && source.charCodeAt(pos) >= 48 && source.charCodeAt(pos) <= 57) {
339
+ pos++;
340
+ }
341
+ const ch = pos < len ? source.charCodeAt(pos) : 0;
342
+ if (ch === 101 || ch === 69) {
343
+ pos = lexExponent(source, len, pos);
344
+ }
345
+ cursor.pos = pos;
346
+ return [0 /* Number */, start, pos];
336
347
  }
337
- while (ch !== 0) {
338
- if (isDigit(ch)) {
339
- advance(cursor);
340
- ch = peek(cursor);
341
- } else if (ch === 46 && !hasDecimal && !hasExponent) {
342
- hasDecimal = true;
343
- advance(cursor);
344
- ch = peek(cursor);
345
- } else if ((ch === 101 || ch === 69) && !hasExponent) {
346
- hasExponent = true;
347
- advance(cursor);
348
- ch = peek(cursor);
349
- if (ch === 43 || ch === 45) {
350
- advance(cursor);
351
- ch = peek(cursor);
352
- }
353
- if (!isDigit(ch)) {
354
- throw new Error(`Invalid number: expected digit after exponent at position ${cursor.pos}`);
355
- }
356
- while (isDigit(ch)) {
357
- advance(cursor);
358
- ch = peek(cursor);
359
- }
360
- break;
361
- } else {
362
- break;
348
+ while (pos < len && source.charCodeAt(pos) >= 48 && source.charCodeAt(pos) <= 57) {
349
+ pos++;
350
+ }
351
+ if (pos < len && source.charCodeAt(pos) === 46) {
352
+ pos++;
353
+ while (pos < len && source.charCodeAt(pos) >= 48 && source.charCodeAt(pos) <= 57) {
354
+ pos++;
363
355
  }
364
356
  }
365
- return [0 /* Number */, start, cursor.pos];
357
+ if (pos < len) {
358
+ const ch = source.charCodeAt(pos);
359
+ if (ch === 101 || ch === 69) {
360
+ pos = lexExponent(source, len, pos);
361
+ }
362
+ }
363
+ cursor.pos = pos;
364
+ return [0 /* Number */, start, pos];
365
+ }
366
+ function lexExponent(source, len, pos) {
367
+ pos++;
368
+ if (pos < len) {
369
+ const next = source.charCodeAt(pos);
370
+ if (next === 43 || next === 45) {
371
+ pos++;
372
+ }
373
+ }
374
+ if (pos >= len || !isDigit(source.charCodeAt(pos))) {
375
+ throw new Error(`Invalid number: expected digit after exponent at position ${pos}`);
376
+ }
377
+ while (pos < len && isDigit(source.charCodeAt(pos))) {
378
+ pos++;
379
+ }
380
+ return pos;
366
381
  }
367
382
  function lexIdentifier(cursor) {
368
383
  const start = cursor.pos;
369
- advance(cursor);
370
- let ch = peek(cursor);
371
- while (isAlpha(ch) || isDigit(ch) || ch === 95) {
372
- advance(cursor);
373
- ch = peek(cursor);
384
+ const { source, len } = cursor;
385
+ let pos = cursor.pos + 1;
386
+ while (pos < len) {
387
+ const ch = source.charCodeAt(pos);
388
+ if (ch >= 65 && ch <= 90 || ch >= 97 && ch <= 122 || ch >= 48 && ch <= 57 || ch === 95) {
389
+ pos++;
390
+ } else {
391
+ break;
392
+ }
374
393
  }
375
- return [1 /* Identifier */, start, cursor.pos];
394
+ cursor.pos = pos;
395
+ return [1 /* Identifier */, start, pos];
376
396
  }
377
397
 
378
398
  // src/visitor.ts
@@ -383,7 +403,8 @@ function visit(node, visitor) {
383
403
  }
384
404
  function visitPartial(node, visitor, defaultHandler) {
385
405
  const recurse = (n) => visitPartial(n, visitor, defaultHandler);
386
- switch (node[0]) {
406
+ const kind = node[0];
407
+ switch (kind) {
387
408
  case 0 /* Program */:
388
409
  return visitor.Program ? visitor.Program(node, recurse) : defaultHandler(node, recurse);
389
410
  case 1 /* NumberLiteral */:
@@ -557,25 +578,16 @@ function containsVariableReference(node) {
557
578
  return collectAllIdentifiers(node).size > 0;
558
579
  }
559
580
  // src/codegen.ts
560
- function needsParensLeft(node, operator) {
561
- if (!isBinaryOp(node))
562
- return false;
563
- const nodePrecedence = getOperatorPrecedence(node[2]);
564
- const operatorPrecedence = getOperatorPrecedence(operator);
565
- if (operator === "^") {
566
- return nodePrecedence <= operatorPrecedence;
567
- }
568
- return nodePrecedence < operatorPrecedence;
569
- }
570
- function needsParensRight(node, operator) {
581
+ function needsParens(node, operator, isLeft) {
571
582
  if (!isBinaryOp(node))
572
583
  return false;
573
584
  const nodePrecedence = getOperatorPrecedence(node[2]);
574
585
  const operatorPrecedence = getOperatorPrecedence(operator);
575
- if (operator === "^") {
576
- return nodePrecedence < operatorPrecedence;
586
+ const isRightAssociative = operator === "^";
587
+ if (isRightAssociative) {
588
+ return isLeft ? nodePrecedence <= operatorPrecedence : nodePrecedence < operatorPrecedence;
577
589
  }
578
- return nodePrecedence <= operatorPrecedence;
590
+ return isLeft ? nodePrecedence < operatorPrecedence : nodePrecedence <= operatorPrecedence;
579
591
  }
580
592
  function generate(node) {
581
593
  return visit(node, {
@@ -595,9 +607,9 @@ function generate(node) {
595
607
  const rightNode = n[3];
596
608
  const left = recurse(leftNode);
597
609
  const right = recurse(rightNode);
598
- const leftNeedsParens = needsParensLeft(leftNode, operator) || operator === "^" && isUnaryOp(leftNode);
610
+ const leftNeedsParens = needsParens(leftNode, operator, true) || operator === "^" && isUnaryOp(leftNode);
599
611
  const leftCode = leftNeedsParens ? `(${left})` : left;
600
- const rightNeedsParens = needsParensRight(rightNode, operator);
612
+ const rightNeedsParens = needsParens(rightNode, operator, false);
601
613
  const rightCode = rightNeedsParens ? `(${right})` : right;
602
614
  return `${leftCode} ${operator} ${rightCode}`;
603
615
  },
@@ -605,8 +617,8 @@ function generate(node) {
605
617
  const operator = n[1];
606
618
  const argumentNode = n[2];
607
619
  const arg = recurse(argumentNode);
608
- const needsParens = isBinaryOp(argumentNode) || isAssignment(argumentNode);
609
- const argCode = needsParens ? `(${arg})` : arg;
620
+ const needsParens2 = isBinaryOp(argumentNode) || isAssignment(argumentNode);
621
+ const argCode = needsParens2 ? `(${arg})` : arg;
610
622
  return `${operator}${argCode}`;
611
623
  },
612
624
  FunctionCall: (n, recurse) => {
@@ -781,14 +793,8 @@ function humanizeNode(node, operatorPhrases, functionPhrases, options) {
781
793
  });
782
794
  }
783
795
  function humanize(node, options = {}) {
784
- const operatorPhrases = {
785
- ...DEFAULT_OPERATOR_PHRASES,
786
- ...options.operatorPhrases
787
- };
788
- const functionPhrases = {
789
- ...DEFAULT_FUNCTION_PHRASES,
790
- ...options.functionPhrases
791
- };
796
+ const operatorPhrases = options.operatorPhrases ? { ...DEFAULT_OPERATOR_PHRASES, ...options.operatorPhrases } : DEFAULT_OPERATOR_PHRASES;
797
+ const functionPhrases = options.functionPhrases ? { ...DEFAULT_FUNCTION_PHRASES, ...options.functionPhrases } : DEFAULT_FUNCTION_PHRASES;
792
798
  return humanizeNode(node, operatorPhrases, functionPhrases, options);
793
799
  }
794
800
  // src/parser.ts
@@ -815,7 +821,7 @@ function parse(source) {
815
821
  currentToken: nextToken(cursor)
816
822
  };
817
823
  const statements = [];
818
- while (peekKind(state) !== 23 /* Eof */) {
824
+ while (state.currentToken[0] !== 23 /* Eof */) {
819
825
  statements.push(parseExpression(state, 0));
820
826
  }
821
827
  if (statements.length === 0) {
@@ -833,7 +839,8 @@ function parse(source) {
833
839
  function parseExpression(state, minPrecedence) {
834
840
  let left = parsePrefix(state);
835
841
  while (true) {
836
- const precedence = getTokenPrecedence(peekKind(state));
842
+ const kind = state.currentToken[0];
843
+ const precedence = getTokenPrecedence(kind);
837
844
  if (precedence === 0 || precedence < minPrecedence) {
838
845
  break;
839
846
  }
@@ -845,12 +852,12 @@ function parsePrefix(state) {
845
852
  const tokenKind = peekKind(state);
846
853
  if (tokenKind === 3 /* Minus */) {
847
854
  advance2(state);
848
- const argument = parseExpression(state, getUnaryPrecedence());
855
+ const argument = parseExpression(state, UNARY_PRECEDENCE);
849
856
  return unaryOp("-", argument);
850
857
  }
851
858
  if (tokenKind === 8 /* Bang */) {
852
859
  advance2(state);
853
- const argument = parseExpression(state, getUnaryPrecedence());
860
+ const argument = parseExpression(state, UNARY_PRECEDENCE);
854
861
  return unaryOp("!", argument);
855
862
  }
856
863
  if (tokenKind === 17 /* LParen */) {
@@ -909,7 +916,7 @@ function parseInfix(state, left, precedence) {
909
916
  return conditional(left, consequent, alternate);
910
917
  }
911
918
  if (isBinaryOperator(tokenKind)) {
912
- const operator = getOperatorFromToken(state.currentToken, state.cursor);
919
+ const operator = readText(state.cursor, state.currentToken);
913
920
  advance2(state);
914
921
  const isRightAssociative = operator === "^";
915
922
  const right = parseExpression(state, isRightAssociative ? precedence : precedence + 1);
@@ -918,10 +925,10 @@ function parseInfix(state, left, precedence) {
918
925
  throw new Error("Unexpected token in infix position");
919
926
  }
920
927
  function parseFunctionArguments(state) {
921
- const args = [];
922
928
  if (peekKind(state) === 18 /* RParen */) {
923
- return args;
929
+ return [];
924
930
  }
931
+ const args = [];
925
932
  args.push(parseExpression(state, 0));
926
933
  while (peekKind(state) === 20 /* Comma */) {
927
934
  advance2(state);
@@ -929,15 +936,10 @@ function parseFunctionArguments(state) {
929
936
  }
930
937
  return args;
931
938
  }
932
- function getUnaryPrecedence() {
933
- return 7;
934
- }
939
+ var UNARY_PRECEDENCE = 7;
935
940
  function isBinaryOperator(kind) {
936
941
  return BINARY_OPERATOR_TOKENS.has(kind);
937
942
  }
938
- function getOperatorFromToken(token, cursor) {
939
- return readText(cursor, token);
940
- }
941
943
  function peekKind(state) {
942
944
  return state.currentToken[0];
943
945
  }
@@ -1000,13 +1002,13 @@ function evaluateNode(node, context, variables, externalVariables) {
1000
1002
  Assignment: (n, recurse) => {
1001
1003
  const name = n[1];
1002
1004
  const valueNode = n[2];
1005
+ const value = recurse(valueNode);
1003
1006
  if (externalVariables.has(name)) {
1004
1007
  const externalValue = variables.get(name);
1005
1008
  if (externalValue !== undefined) {
1006
1009
  return externalValue;
1007
1010
  }
1008
1011
  }
1009
- const value = recurse(valueNode);
1010
1012
  variables.set(name, value);
1011
1013
  return value;
1012
1014
  },
@@ -1020,23 +1022,24 @@ function evaluateNode(node, context, variables, externalVariables) {
1020
1022
  }
1021
1023
  function evaluate(input, context = {}) {
1022
1024
  const node = typeof input === "string" ? parse(input) : input;
1023
- const variables = new Map(Object.entries(context.variables || {}));
1024
- const externalVariables = new Set(Object.keys(context.variables || {}));
1025
+ const entries = Object.entries(context.variables || {});
1026
+ const variables = new Map(entries);
1027
+ const externalVariables = new Set(entries.map(([key]) => key));
1025
1028
  return evaluateNode(node, context, variables, externalVariables);
1026
1029
  }
1027
1030
  // src/optimizer.ts
1028
1031
  function eliminateDeadCode(program2) {
1029
1032
  const statements = program2[1];
1030
1033
  const liveVars = new Set;
1031
- const keptStatements = [];
1034
+ const keptIndices = [];
1032
1035
  for (let i = statements.length - 1;i >= 0; i--) {
1033
1036
  const stmt = statements[i];
1034
1037
  if (!stmt)
1035
1038
  continue;
1036
1039
  if (i === statements.length - 1) {
1037
- keptStatements.push(stmt);
1040
+ keptIndices.push(i);
1038
1041
  const identifiers = collectAllIdentifiers(stmt);
1039
- for (const id of identifiers) {
1042
+ for (const id of Array.from(identifiers)) {
1040
1043
  liveVars.add(id);
1041
1044
  }
1042
1045
  continue;
@@ -1045,21 +1048,31 @@ function eliminateDeadCode(program2) {
1045
1048
  const name = stmt[1];
1046
1049
  const value = stmt[2];
1047
1050
  if (liveVars.has(name)) {
1048
- keptStatements.push(stmt);
1051
+ keptIndices.push(i);
1049
1052
  const identifiers = collectAllIdentifiers(value);
1050
- for (const id of identifiers) {
1053
+ for (const id of Array.from(identifiers)) {
1051
1054
  liveVars.add(id);
1052
1055
  }
1053
1056
  }
1054
1057
  } else {
1055
- keptStatements.push(stmt);
1058
+ keptIndices.push(i);
1056
1059
  const identifiers = collectAllIdentifiers(stmt);
1057
- for (const id of identifiers) {
1060
+ for (const id of Array.from(identifiers)) {
1058
1061
  liveVars.add(id);
1059
1062
  }
1060
1063
  }
1061
1064
  }
1062
- return program(keptStatements.reverse());
1065
+ const keptStatements = [];
1066
+ for (let i = keptIndices.length - 1;i >= 0; i--) {
1067
+ const idx = keptIndices[i];
1068
+ if (idx !== undefined) {
1069
+ const stmt = statements[idx];
1070
+ if (stmt) {
1071
+ keptStatements.push(stmt);
1072
+ }
1073
+ }
1074
+ }
1075
+ return program(keptStatements);
1063
1076
  }
1064
1077
  function optimize(node) {
1065
1078
  const folded = visit(node, {
@@ -1127,6 +1140,248 @@ function optimize(node) {
1127
1140
  }
1128
1141
  return folded;
1129
1142
  }
1143
+
1144
+ // src/jit.ts
1145
+ function compile(input) {
1146
+ const parsedAst = typeof input === "string" ? parse(input) : input;
1147
+ const ast = optimize(parsedAst);
1148
+ const jsCode = generateJavaScript(ast);
1149
+ const fullCode = `${HELPER_FUNCTIONS}
1150
+
1151
+ ${jsCode}`;
1152
+ const fn = new Function("__context", fullCode);
1153
+ return {
1154
+ execute: (context = {}) => {
1155
+ return fn(context);
1156
+ },
1157
+ source: fullCode
1158
+ };
1159
+ }
1160
+ var HELPER_FUNCTIONS = `
1161
+ // Helper function for throwing errors (avoids string concatenation overhead)
1162
+ function __throw(msg) { throw new Error(msg); }
1163
+
1164
+ // Temporary variables for optimized operations
1165
+ let __r; // For division/modulo right operand
1166
+ let __fn; // For function lookup
1167
+ let __val; // For assignment value
1168
+ `.trim();
1169
+ function generateJavaScript(node) {
1170
+ const lines = [];
1171
+ const assignedVars = collectAssignedVariables(node);
1172
+ const referencedVars = collectAllIdentifiers(node);
1173
+ const literalAssignments = collectLiteralAssignments(node);
1174
+ lines.push("// Context accessors");
1175
+ lines.push("const __ext = __context.variables || {};");
1176
+ lines.push("const __fns = __context.functions || {};");
1177
+ lines.push("const __extKeys = new Set(Object.keys(__ext));");
1178
+ lines.push("");
1179
+ const allVars = new Set([
1180
+ ...Array.from(assignedVars),
1181
+ ...Array.from(referencedVars)
1182
+ ]);
1183
+ if (allVars.size > 0) {
1184
+ lines.push("// Variable declarations");
1185
+ for (const name of Array.from(allVars)) {
1186
+ const literalValue = literalAssignments.get(name);
1187
+ if (literalValue !== undefined) {
1188
+ lines.push(`let ${name} = __ext.${name} ?? ${literalValue};`);
1189
+ } else {
1190
+ lines.push(`let ${name} = __ext.${name};`);
1191
+ }
1192
+ }
1193
+ lines.push("");
1194
+ }
1195
+ lines.push("// Execute program");
1196
+ const code = compileNode(node, literalAssignments);
1197
+ lines.push(`return ${code};`);
1198
+ return lines.join(`
1199
+ `);
1200
+ }
1201
+ function collectAssignedVariables(node) {
1202
+ const names = new Set;
1203
+ visit(node, {
1204
+ Program: (n, recurse) => {
1205
+ for (const stmt of n[1]) {
1206
+ recurse(stmt);
1207
+ }
1208
+ },
1209
+ NumberLiteral: () => {},
1210
+ Identifier: () => {},
1211
+ BinaryOp: (n, recurse) => {
1212
+ recurse(n[1]);
1213
+ recurse(n[3]);
1214
+ },
1215
+ UnaryOp: (n, recurse) => {
1216
+ recurse(n[2]);
1217
+ },
1218
+ FunctionCall: (n, recurse) => {
1219
+ for (const arg of n[2]) {
1220
+ recurse(arg);
1221
+ }
1222
+ },
1223
+ Assignment: (n, recurse) => {
1224
+ names.add(n[1]);
1225
+ recurse(n[2]);
1226
+ },
1227
+ ConditionalExpression: (n, recurse) => {
1228
+ recurse(n[1]);
1229
+ recurse(n[2]);
1230
+ recurse(n[3]);
1231
+ }
1232
+ });
1233
+ return names;
1234
+ }
1235
+ function collectLiteralAssignments(node) {
1236
+ const literals = new Map;
1237
+ const nonLiterals = new Set;
1238
+ visit(node, {
1239
+ Program: (n, recurse) => {
1240
+ for (const stmt of n[1]) {
1241
+ recurse(stmt);
1242
+ }
1243
+ },
1244
+ NumberLiteral: () => {},
1245
+ Identifier: () => {},
1246
+ BinaryOp: (n, recurse) => {
1247
+ recurse(n[1]);
1248
+ recurse(n[3]);
1249
+ },
1250
+ UnaryOp: (n, recurse) => {
1251
+ recurse(n[2]);
1252
+ },
1253
+ FunctionCall: (n, recurse) => {
1254
+ for (const arg of n[2]) {
1255
+ recurse(arg);
1256
+ }
1257
+ },
1258
+ Assignment: (n, recurse) => {
1259
+ const name = n[1];
1260
+ const value = n[2];
1261
+ if (literals.has(name) || nonLiterals.has(name)) {
1262
+ literals.delete(name);
1263
+ nonLiterals.add(name);
1264
+ return;
1265
+ }
1266
+ if (value[0] === 1 /* NumberLiteral */) {
1267
+ const num = value[1];
1268
+ if (Number.isNaN(num)) {
1269
+ literals.set(name, "NaN");
1270
+ } else if (!Number.isFinite(num)) {
1271
+ literals.set(name, num > 0 ? "Infinity" : "-Infinity");
1272
+ } else {
1273
+ literals.set(name, String(num));
1274
+ }
1275
+ } else {
1276
+ nonLiterals.add(name);
1277
+ recurse(value);
1278
+ }
1279
+ },
1280
+ ConditionalExpression: (n, recurse) => {
1281
+ recurse(n[1]);
1282
+ recurse(n[2]);
1283
+ recurse(n[3]);
1284
+ }
1285
+ });
1286
+ return literals;
1287
+ }
1288
+ function compileNode(node, literalAssignments = new Map) {
1289
+ const compileIdentifier = (name) => {
1290
+ if (literalAssignments.has(name)) {
1291
+ return name;
1292
+ }
1293
+ return `(${name} !== undefined ? ${name} : __throw('Undefined variable: ${name}'))`;
1294
+ };
1295
+ return visit(node, {
1296
+ Program: (n, recurse) => {
1297
+ const statements = n[1];
1298
+ if (statements.length === 0) {
1299
+ return "0";
1300
+ }
1301
+ if (statements.length === 1) {
1302
+ const stmt = statements[0];
1303
+ return stmt ? recurse(stmt) : "0";
1304
+ }
1305
+ const stmts = statements.map(recurse);
1306
+ return `(${stmts.join(", ")})`;
1307
+ },
1308
+ NumberLiteral: (n) => {
1309
+ const value = n[1];
1310
+ if (Number.isNaN(value))
1311
+ return "NaN";
1312
+ if (!Number.isFinite(value)) {
1313
+ return value > 0 ? "Infinity" : "-Infinity";
1314
+ }
1315
+ return String(value);
1316
+ },
1317
+ Identifier: (n) => {
1318
+ const name = n[1];
1319
+ return compileIdentifier(name);
1320
+ },
1321
+ BinaryOp: (n, recurse) => {
1322
+ const left = recurse(n[1]);
1323
+ const operator = n[2];
1324
+ const right = recurse(n[3]);
1325
+ switch (operator) {
1326
+ case "^":
1327
+ return `(${left} ** ${right})`;
1328
+ case "==":
1329
+ return `(${left} === ${right} ? 1 : 0)`;
1330
+ case "!=":
1331
+ return `(${left} !== ${right} ? 1 : 0)`;
1332
+ case "<":
1333
+ return `(${left} < ${right} ? 1 : 0)`;
1334
+ case ">":
1335
+ return `(${left} > ${right} ? 1 : 0)`;
1336
+ case "<=":
1337
+ return `(${left} <= ${right} ? 1 : 0)`;
1338
+ case ">=":
1339
+ return `(${left} >= ${right} ? 1 : 0)`;
1340
+ case "&&":
1341
+ return `(${left} !== 0 && ${right} !== 0 ? 1 : 0)`;
1342
+ case "||":
1343
+ return `(${left} !== 0 || ${right} !== 0 ? 1 : 0)`;
1344
+ case "/":
1345
+ return `((__r = ${right}) === 0 && __throw('Division by zero'), ${left} / __r)`;
1346
+ case "%":
1347
+ return `((__r = ${right}) === 0 && __throw('Modulo by zero'), ${left} % __r)`;
1348
+ default:
1349
+ return `(${left} ${operator} ${right})`;
1350
+ }
1351
+ },
1352
+ UnaryOp: (n, recurse) => {
1353
+ const operator = n[1];
1354
+ const arg = recurse(n[2]);
1355
+ if (operator === "-") {
1356
+ return `(-${arg})`;
1357
+ }
1358
+ if (operator === "!") {
1359
+ return `(${arg} === 0 ? 1 : 0)`;
1360
+ }
1361
+ throw new Error(`Unknown unary operator: ${operator}`);
1362
+ },
1363
+ FunctionCall: (n, recurse) => {
1364
+ const name = n[1];
1365
+ const args = n[2].map(recurse);
1366
+ const argsStr = args.length > 0 ? args.join(", ") : "";
1367
+ return `((__fn = __fns.${name}) === undefined ? __throw('Undefined function: ${name}') : typeof __fn !== 'function' ? __throw('${name} is not a function') : __fn(${argsStr}))`;
1368
+ },
1369
+ Assignment: (n, recurse) => {
1370
+ const name = n[1];
1371
+ if (literalAssignments.has(name)) {
1372
+ return name;
1373
+ }
1374
+ const value = recurse(n[2]);
1375
+ return `((__val = ${value}), (__extKeys.has('${name}') ? ${name} : (${name} = __val)))`;
1376
+ },
1377
+ ConditionalExpression: (n, recurse) => {
1378
+ const condition = recurse(n[1]);
1379
+ const consequent = recurse(n[2]);
1380
+ const alternate = recurse(n[3]);
1381
+ return `(${condition} !== 0 ? ${consequent} : ${alternate})`;
1382
+ }
1383
+ });
1384
+ }
1130
1385
  // src/stdlib/datetime.ts
1131
1386
  var exports_datetime = {};
1132
1387
  __export(exports_datetime, {
@@ -1347,6 +1602,7 @@ export {
1347
1602
  evaluate,
1348
1603
  defaultContext,
1349
1604
  exports_datetime as datetime,
1605
+ compile,
1350
1606
  exports_ast as ast,
1351
1607
  NodeKind
1352
1608
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlewing",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",