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 +97 -3
- package/dist/index.d.ts +62 -1
- package/dist/index.js +356 -100
- package/package.json +1 -1
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
|
-
- **
|
|
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** -
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 (
|
|
205
|
-
|
|
206
|
-
while (
|
|
207
|
-
|
|
208
|
-
ch
|
|
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
|
-
|
|
330
|
-
let
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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 (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
let
|
|
371
|
-
while (
|
|
372
|
-
|
|
373
|
-
ch
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
576
|
-
|
|
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 =
|
|
610
|
+
const leftNeedsParens = needsParens(leftNode, operator, true) || operator === "^" && isUnaryOp(leftNode);
|
|
599
611
|
const leftCode = leftNeedsParens ? `(${left})` : left;
|
|
600
|
-
const rightNeedsParens =
|
|
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
|
|
609
|
-
const argCode =
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
1024
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|