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 +97 -3
- package/dist/index.d.ts +96 -1
- package/dist/index.js +279 -6
- 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
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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",
|