littlewing 0.3.0 → 0.4.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 +198 -55
- package/dist/index.d.ts +35 -60
- package/dist/index.js +97 -145
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@ A minimal, high-performance arithmetic expression language with a complete lexer
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🚀 **Minimal & Fast** - O(n) algorithms throughout (lexer, parser, executor)
|
|
8
|
-
- 📦 **Tiny Bundle** -
|
|
8
|
+
- 📦 **Tiny Bundle** - 4.19 KB gzipped, zero dependencies
|
|
9
9
|
- 🌐 **Browser Ready** - 100% ESM, no Node.js APIs
|
|
10
10
|
- 🔒 **Type-Safe** - Strict TypeScript with full type coverage
|
|
11
|
-
- ✅ **Thoroughly Tested** -
|
|
12
|
-
- 📐 **
|
|
11
|
+
- ✅ **Thoroughly Tested** - 128 tests, 98.61% line coverage
|
|
12
|
+
- 📐 **Pure Arithmetic** - Numbers-only, clean semantics
|
|
13
13
|
- 🎯 **Clean API** - Intuitive dual API (class-based + functional)
|
|
14
14
|
- 📝 **Well Documented** - Complete JSDoc and examples
|
|
15
15
|
|
|
@@ -58,21 +58,40 @@ execute("pi * 2", context); // → 6.28318
|
|
|
58
58
|
execute("maxValue - 25", context); // → 75
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
###
|
|
61
|
+
### Timestamp Arithmetic
|
|
62
|
+
|
|
63
|
+
Littlewing uses a numbers-only type system. Timestamps (milliseconds since Unix epoch) are just numbers, enabling clean date arithmetic:
|
|
62
64
|
|
|
63
65
|
```typescript
|
|
64
66
|
import { execute, defaultContext } from "littlewing";
|
|
65
67
|
|
|
66
|
-
//
|
|
67
|
-
execute("now()", defaultContext); // →
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Get current timestamp
|
|
69
|
+
execute("now()", defaultContext); // → 1704067200000 (number)
|
|
70
|
+
|
|
71
|
+
// Create timestamp from date components
|
|
72
|
+
execute("timestamp(2025, 10, 1)", defaultContext); // → timestamp for Oct 1, 2025
|
|
73
|
+
|
|
74
|
+
// Add time durations (all return milliseconds)
|
|
75
|
+
execute("now() + minutes(30)", defaultContext); // → timestamp 30 minutes from now
|
|
76
|
+
execute("now() + hours(2) + minutes(15)", defaultContext); // → 2h 15m from now
|
|
70
77
|
|
|
71
78
|
// Time conversion helpers
|
|
72
79
|
execute("seconds(30)", defaultContext); // → 30000 (milliseconds)
|
|
73
80
|
execute("minutes(5)", defaultContext); // → 300000 (milliseconds)
|
|
74
81
|
execute("hours(2)", defaultContext); // → 7200000 (milliseconds)
|
|
75
|
-
execute("days(
|
|
82
|
+
execute("days(7)", defaultContext); // → 604800000 (milliseconds)
|
|
83
|
+
execute("weeks(2)", defaultContext); // → 1209600000 (milliseconds)
|
|
84
|
+
|
|
85
|
+
// Extract components from timestamps
|
|
86
|
+
const timestamp = new Date("2024-06-15T14:30:00").getTime();
|
|
87
|
+
execute("year(t)", { ...defaultContext, variables: { t: timestamp } }); // → 2024
|
|
88
|
+
execute("month(t)", { ...defaultContext, variables: { t: timestamp } }); // → 6 (June)
|
|
89
|
+
execute("day(t)", { ...defaultContext, variables: { t: timestamp } }); // → 15
|
|
90
|
+
execute("hour(t)", { ...defaultContext, variables: { t: timestamp } }); // → 14
|
|
91
|
+
|
|
92
|
+
// Convert result back to Date when needed
|
|
93
|
+
const result = execute("now() + days(7)", defaultContext);
|
|
94
|
+
const futureDate = new Date(result); // JavaScript Date object
|
|
76
95
|
```
|
|
77
96
|
|
|
78
97
|
### Manual AST Construction
|
|
@@ -91,9 +110,10 @@ executor.execute(expr); // → 14
|
|
|
91
110
|
### Literals
|
|
92
111
|
|
|
93
112
|
```typescript
|
|
94
|
-
42; //
|
|
113
|
+
42; // integer
|
|
95
114
|
3.14; // floating point
|
|
96
|
-
|
|
115
|
+
1.5e6; // scientific notation (1500000)
|
|
116
|
+
2e-3; // negative exponent (0.002)
|
|
97
117
|
```
|
|
98
118
|
|
|
99
119
|
### Variables
|
|
@@ -140,7 +160,7 @@ Functions accept any number of arguments:
|
|
|
140
160
|
```typescript
|
|
141
161
|
abs(-5); // → 5
|
|
142
162
|
max(1, 5, 3); // → 5
|
|
143
|
-
|
|
163
|
+
timestamp(2025, 1, 1); // → timestamp
|
|
144
164
|
```
|
|
145
165
|
|
|
146
166
|
### Comments
|
|
@@ -165,9 +185,9 @@ execute("42"); // → 42
|
|
|
165
185
|
|
|
166
186
|
### Main Functions
|
|
167
187
|
|
|
168
|
-
#### `execute(source: string, context?: ExecutionContext):
|
|
188
|
+
#### `execute(source: string, context?: ExecutionContext): number`
|
|
169
189
|
|
|
170
|
-
Execute source code with an optional execution context.
|
|
190
|
+
Execute source code with an optional execution context. Always returns a number.
|
|
171
191
|
|
|
172
192
|
```typescript
|
|
173
193
|
execute("2 + 2");
|
|
@@ -183,6 +203,41 @@ const ast = parseSource("2 + 3 * 4");
|
|
|
183
203
|
// Returns: BinaryOp(+, NumberLiteral(2), BinaryOp(*, ...))
|
|
184
204
|
```
|
|
185
205
|
|
|
206
|
+
#### `generate(node: ASTNode): string`
|
|
207
|
+
|
|
208
|
+
Convert an AST node back to source code. Intelligently adds parentheses only when necessary to preserve semantics.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { generate, ast } from "littlewing";
|
|
212
|
+
|
|
213
|
+
// From AST builders
|
|
214
|
+
const expr = ast.multiply(ast.add(ast.number(2), ast.number(3)), ast.number(4));
|
|
215
|
+
generate(expr); // → "(2 + 3) * 4"
|
|
216
|
+
|
|
217
|
+
// Round-trip: parse → generate → parse
|
|
218
|
+
const code = "2 + 3 * 4";
|
|
219
|
+
const tree = parseSource(code);
|
|
220
|
+
const regenerated = generate(tree); // → "2 + 3 * 4"
|
|
221
|
+
parseSource(regenerated); // Same AST structure
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `optimize(node: ASTNode): ASTNode`
|
|
225
|
+
|
|
226
|
+
Optimize an AST by performing constant folding. Evaluates expressions with only literals at compile-time.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { optimize, parseSource } from "littlewing";
|
|
230
|
+
|
|
231
|
+
// Parse first, then optimize
|
|
232
|
+
const ast = parseSource("2 + 3 * 4");
|
|
233
|
+
const optimized = optimize(ast);
|
|
234
|
+
// Transforms BinaryOp tree to NumberLiteral(14)
|
|
235
|
+
|
|
236
|
+
// Useful for storing compact ASTs
|
|
237
|
+
const compactAst = optimize(parseSource("1e6 + 2e6"));
|
|
238
|
+
// → NumberLiteral(3000000)
|
|
239
|
+
```
|
|
240
|
+
|
|
186
241
|
### Classes
|
|
187
242
|
|
|
188
243
|
#### `Lexer`
|
|
@@ -219,6 +274,17 @@ const executor = new Executor(context);
|
|
|
219
274
|
const result = executor.execute(ast);
|
|
220
275
|
```
|
|
221
276
|
|
|
277
|
+
#### `CodeGenerator`
|
|
278
|
+
|
|
279
|
+
Convert AST nodes back to source code. Handles operator precedence and associativity automatically.
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { CodeGenerator } from "littlewing";
|
|
283
|
+
|
|
284
|
+
const generator = new CodeGenerator();
|
|
285
|
+
const code = generator.generate(ast);
|
|
286
|
+
```
|
|
287
|
+
|
|
222
288
|
### AST Builders
|
|
223
289
|
|
|
224
290
|
The `ast` namespace provides convenient functions for building AST nodes:
|
|
@@ -233,6 +299,7 @@ ast.subtract(left, right);
|
|
|
233
299
|
ast.multiply(left, right);
|
|
234
300
|
ast.divide(left, right);
|
|
235
301
|
ast.modulo(left, right);
|
|
302
|
+
ast.exponentiate(left, right);
|
|
236
303
|
ast.negate(argument);
|
|
237
304
|
ast.assign("x", value);
|
|
238
305
|
ast.functionCall("abs", [ast.number(-5)]);
|
|
@@ -249,42 +316,79 @@ import { defaultContext } from "littlewing";
|
|
|
249
316
|
(abs, ceil, floor, round, sqrt, min, max);
|
|
250
317
|
(sin, cos, tan, log, log10, exp);
|
|
251
318
|
|
|
252
|
-
//
|
|
253
|
-
(
|
|
319
|
+
// Timestamp functions
|
|
320
|
+
now(); // Current timestamp
|
|
321
|
+
timestamp(year, month, day); // Create timestamp from date components
|
|
254
322
|
|
|
255
323
|
// Time conversion (returns milliseconds)
|
|
256
|
-
(milliseconds, seconds, minutes, hours, days);
|
|
324
|
+
(milliseconds(n), seconds(n), minutes(n), hours(n), days(n), weeks(n));
|
|
325
|
+
|
|
326
|
+
// Timestamp component extractors
|
|
327
|
+
year(timestamp); // Extract year (e.g., 2024)
|
|
328
|
+
month(timestamp); // Extract month (1-12, 1 = January)
|
|
329
|
+
day(timestamp); // Extract day of month (1-31)
|
|
330
|
+
hour(timestamp); // Extract hour (0-23)
|
|
331
|
+
minute(timestamp); // Extract minute (0-59)
|
|
332
|
+
second(timestamp); // Extract second (0-59)
|
|
333
|
+
weekday(timestamp); // Extract day of week (0-6, 0 = Sunday)
|
|
257
334
|
```
|
|
258
335
|
|
|
259
|
-
##
|
|
336
|
+
## Advanced Features
|
|
260
337
|
|
|
261
|
-
###
|
|
338
|
+
### Constant Folding Optimization
|
|
339
|
+
|
|
340
|
+
The `optimize()` function performs constant folding, pre-calculating expressions with only literal values. This results in smaller ASTs and faster execution.
|
|
341
|
+
|
|
342
|
+
**Without optimization:**
|
|
262
343
|
|
|
263
344
|
```typescript
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
345
|
+
import { parseSource } from "littlewing";
|
|
346
|
+
|
|
347
|
+
const ast = parseSource("2 + 3 * 4");
|
|
348
|
+
// AST: BinaryOp(+, NumberLiteral(2), BinaryOp(*, NumberLiteral(3), NumberLiteral(4)))
|
|
349
|
+
// Size: 3 nodes
|
|
268
350
|
```
|
|
269
351
|
|
|
270
|
-
|
|
352
|
+
**With optimization:**
|
|
271
353
|
|
|
272
354
|
```typescript
|
|
273
|
-
|
|
355
|
+
import { optimize, parseSource } from "littlewing";
|
|
356
|
+
|
|
357
|
+
const ast = optimize(parseSource("2 + 3 * 4"));
|
|
358
|
+
// AST: NumberLiteral(14)
|
|
359
|
+
// Size: 1 node - 67% smaller!
|
|
274
360
|
```
|
|
275
361
|
|
|
276
|
-
|
|
362
|
+
**When to use:**
|
|
363
|
+
|
|
364
|
+
- **Storage:** Compact ASTs for databases or serialization
|
|
365
|
+
- **Performance:** Faster execution (no runtime calculation needed)
|
|
366
|
+
- **Network:** Smaller payload when transmitting ASTs
|
|
367
|
+
- **Caching:** Pre-calculate expensive expressions once
|
|
368
|
+
|
|
369
|
+
**What gets optimized:**
|
|
370
|
+
|
|
371
|
+
- ✅ Binary operations with literals: `2 + 3` → `5`
|
|
372
|
+
- ✅ Unary operations: `-5` → `-5`
|
|
373
|
+
- ✅ Nested expressions: `2 + 3 * 4` → `14`
|
|
374
|
+
- ✅ Scientific notation: `1e6 + 2e6` → `3000000`
|
|
375
|
+
- ✅ Partial optimization: `x = 2 + 3` → `x = 5`
|
|
376
|
+
- ❌ Variables: `x + 3` stays as-is (x is not a literal)
|
|
377
|
+
- ❌ Functions: `sqrt(16)` stays as-is (might have side effects)
|
|
378
|
+
|
|
379
|
+
### Scientific Notation
|
|
380
|
+
|
|
381
|
+
Littlewing supports scientific notation for large or small numbers:
|
|
277
382
|
|
|
278
383
|
```typescript
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
| Assignment;
|
|
384
|
+
execute("1.5e6"); // → 1500000
|
|
385
|
+
execute("2e10"); // → 20000000000
|
|
386
|
+
execute("3e-2"); // → 0.03
|
|
387
|
+
execute("4E+5"); // → 400000
|
|
388
|
+
|
|
389
|
+
// Works with optimization too
|
|
390
|
+
const ast = parseSource("1e6 * 2", { optimize: true });
|
|
391
|
+
// → NumberLiteral(2000000)
|
|
288
392
|
```
|
|
289
393
|
|
|
290
394
|
## Examples
|
|
@@ -295,7 +399,7 @@ type ASTNode =
|
|
|
295
399
|
import { execute, defaultContext } from "littlewing";
|
|
296
400
|
|
|
297
401
|
function calculate(expression: string): number {
|
|
298
|
-
return execute(expression, defaultContext)
|
|
402
|
+
return execute(expression, defaultContext);
|
|
299
403
|
}
|
|
300
404
|
|
|
301
405
|
calculate("2 + 2 * 3"); // → 8
|
|
@@ -318,18 +422,28 @@ const context = {
|
|
|
318
422
|
};
|
|
319
423
|
|
|
320
424
|
const compound = execute("principal * (1 + rate) ^ years", context);
|
|
425
|
+
// → 1102.5
|
|
321
426
|
```
|
|
322
427
|
|
|
323
|
-
###
|
|
428
|
+
### Timestamp Arithmetic
|
|
324
429
|
|
|
325
430
|
```typescript
|
|
326
431
|
import { execute, defaultContext } from "littlewing";
|
|
327
432
|
|
|
328
|
-
//
|
|
329
|
-
const deadline = execute("now() + days(
|
|
433
|
+
// Calculate deadline
|
|
434
|
+
const deadline = execute("now() + days(7)", defaultContext);
|
|
435
|
+
const deadlineDate = new Date(deadline); // Convert to Date
|
|
436
|
+
|
|
437
|
+
// Complex time calculations
|
|
438
|
+
const result = execute("now() + weeks(2) + days(3) + hours(4)", defaultContext);
|
|
330
439
|
|
|
331
|
-
//
|
|
332
|
-
const
|
|
440
|
+
// Time until event
|
|
441
|
+
const eventTime = new Date("2025-12-31").getTime();
|
|
442
|
+
const timeUntil = execute("event - now()", {
|
|
443
|
+
...defaultContext,
|
|
444
|
+
variables: { event: eventTime },
|
|
445
|
+
});
|
|
446
|
+
const daysUntil = timeUntil / (1000 * 60 * 60 * 24);
|
|
333
447
|
```
|
|
334
448
|
|
|
335
449
|
### Custom Functions
|
|
@@ -341,7 +455,7 @@ const context = {
|
|
|
341
455
|
functions: {
|
|
342
456
|
fahrenheit: (celsius) => (celsius * 9) / 5 + 32,
|
|
343
457
|
kilometers: (miles) => miles * 1.60934,
|
|
344
|
-
factorial: (n) => (n <= 1 ? 1 : n * factorial(n - 1)),
|
|
458
|
+
factorial: (n) => (n <= 1 ? 1 : n * context.functions.factorial(n - 1)),
|
|
345
459
|
},
|
|
346
460
|
variables: {
|
|
347
461
|
roomTemp: 20,
|
|
@@ -352,33 +466,54 @@ execute("fahrenheit(roomTemp)", context); // → 68
|
|
|
352
466
|
execute("kilometers(5)", context); // → 8.0467
|
|
353
467
|
```
|
|
354
468
|
|
|
469
|
+
### Scheduling System
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { execute, defaultContext } from "littlewing";
|
|
473
|
+
|
|
474
|
+
// Parse user's relative time expressions
|
|
475
|
+
const tasks = [
|
|
476
|
+
{ name: "Review PR", due: "now() + hours(2)" },
|
|
477
|
+
{ name: "Deploy", due: "now() + days(1)" },
|
|
478
|
+
{ name: "Meeting", due: "timestamp(2025, 10, 15, 14, 30, 0)" },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const dueTimes = tasks.map((task) => ({
|
|
482
|
+
name: task.name,
|
|
483
|
+
dueTimestamp: execute(task.due, defaultContext),
|
|
484
|
+
dueDate: new Date(execute(task.due, defaultContext)),
|
|
485
|
+
}));
|
|
486
|
+
```
|
|
487
|
+
|
|
355
488
|
## Performance
|
|
356
489
|
|
|
357
490
|
### Algorithms
|
|
358
491
|
|
|
359
492
|
- **Lexer**: O(n) single-pass tokenization
|
|
360
493
|
- **Parser**: Optimal Pratt parsing with O(n) time complexity
|
|
361
|
-
- **Executor**: O(n) tree-walk evaluation
|
|
494
|
+
- **Executor**: O(n) tree-walk evaluation with no type checking overhead
|
|
362
495
|
|
|
363
496
|
### Bundle Size
|
|
364
497
|
|
|
365
|
-
-
|
|
366
|
-
- Gzipped: **3.61 KB**
|
|
498
|
+
- **4.19 KB gzipped** (19.60 KB raw)
|
|
367
499
|
- Zero dependencies
|
|
500
|
+
- Includes optimizer for constant folding
|
|
501
|
+
- Fully tree-shakeable
|
|
368
502
|
|
|
369
503
|
### Test Coverage
|
|
370
504
|
|
|
371
|
-
-
|
|
372
|
-
-
|
|
505
|
+
- **128 tests** with **98.61% line coverage**
|
|
506
|
+
- **98.52% function coverage**
|
|
373
507
|
- All edge cases handled
|
|
508
|
+
- Type-safe execution guaranteed
|
|
374
509
|
|
|
375
510
|
## Type Safety
|
|
376
511
|
|
|
377
512
|
- Strict TypeScript mode
|
|
378
513
|
- Zero implicit `any` types
|
|
379
514
|
- Complete type annotations
|
|
380
|
-
-
|
|
381
|
-
-
|
|
515
|
+
- Single `RuntimeValue = number` type
|
|
516
|
+
- No runtime type checking overhead
|
|
382
517
|
|
|
383
518
|
## Error Handling
|
|
384
519
|
|
|
@@ -386,9 +521,9 @@ Clear, actionable error messages for:
|
|
|
386
521
|
|
|
387
522
|
- Undefined variables: `"Undefined variable: x"`
|
|
388
523
|
- Undefined functions: `"Undefined function: abs"`
|
|
389
|
-
- Type mismatches: `"Cannot add string and number"`
|
|
390
524
|
- Division by zero: `"Division by zero"`
|
|
391
|
-
-
|
|
525
|
+
- Modulo by zero: `"Modulo by zero"`
|
|
526
|
+
- Syntax errors with position information
|
|
392
527
|
|
|
393
528
|
## Browser Support
|
|
394
529
|
|
|
@@ -401,6 +536,18 @@ Clear, actionable error messages for:
|
|
|
401
536
|
|
|
402
537
|
Works with Node.js 18+ via ESM imports.
|
|
403
538
|
|
|
539
|
+
## Philosophy
|
|
540
|
+
|
|
541
|
+
Littlewing embraces a **numbers-only** type system for maximum simplicity and performance:
|
|
542
|
+
|
|
543
|
+
- **Pure arithmetic**: Every operation works on numbers
|
|
544
|
+
- **No type checking overhead**: Operators don't need runtime type discrimination
|
|
545
|
+
- **Timestamps as numbers**: Date arithmetic uses millisecond timestamps
|
|
546
|
+
- **Clean semantics**: No ambiguous operations like `Date + Date`
|
|
547
|
+
- **Flexibility**: Convert to/from JavaScript Dates at the boundaries
|
|
548
|
+
|
|
549
|
+
This design keeps the language minimal while remaining powerful enough for real-world use cases.
|
|
550
|
+
|
|
404
551
|
## License
|
|
405
552
|
|
|
406
553
|
MIT
|
|
@@ -408,7 +555,3 @@ MIT
|
|
|
408
555
|
## Contributing
|
|
409
556
|
|
|
410
557
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
411
|
-
|
|
412
|
-
---
|
|
413
|
-
|
|
414
|
-
Made with ❤️ by the littlewing team
|
package/dist/index.d.ts
CHANGED
|
@@ -2,24 +2,27 @@ declare namespace exports_ast {
|
|
|
2
2
|
export { unaryOp, subtract, number, negate, multiply, modulo, identifier, functionCall, exponentiate, divide, binaryOp, assign, add };
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
|
-
* Runtime value type -
|
|
5
|
+
* Runtime value type - only numbers
|
|
6
6
|
*/
|
|
7
|
-
type RuntimeValue = number
|
|
7
|
+
type RuntimeValue = number;
|
|
8
|
+
/**
|
|
9
|
+
* Binary operator types
|
|
10
|
+
*/
|
|
11
|
+
type Operator = "+" | "-" | "*" | "/" | "%" | "^";
|
|
8
12
|
/**
|
|
9
13
|
* Execution context providing global functions and variables
|
|
10
|
-
* Functions must accept any arguments and return a number
|
|
11
|
-
* Variables must be numbers
|
|
14
|
+
* Functions must accept any arguments and return a number
|
|
15
|
+
* Variables must be numbers
|
|
12
16
|
*/
|
|
13
17
|
interface ExecutionContext {
|
|
14
|
-
functions?: Record<string, (...args: any[]) => number
|
|
15
|
-
variables?: Record<string, number
|
|
18
|
+
functions?: Record<string, (...args: any[]) => number>;
|
|
19
|
+
variables?: Record<string, number>;
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
18
22
|
* Token types for lexer output
|
|
19
23
|
*/
|
|
20
24
|
declare enum TokenType {
|
|
21
25
|
NUMBER = "NUMBER",
|
|
22
|
-
STRING = "STRING",
|
|
23
26
|
IDENTIFIER = "IDENTIFIER",
|
|
24
27
|
PLUS = "PLUS",
|
|
25
28
|
MINUS = "MINUS",
|
|
@@ -44,7 +47,7 @@ interface Token {
|
|
|
44
47
|
/**
|
|
45
48
|
* AST Node - base type
|
|
46
49
|
*/
|
|
47
|
-
type ASTNode = Program | NumberLiteral |
|
|
50
|
+
type ASTNode = Program | NumberLiteral | Identifier | BinaryOp | UnaryOp | FunctionCall | Assignment;
|
|
48
51
|
/**
|
|
49
52
|
* Program node (multiple statements)
|
|
50
53
|
*/
|
|
@@ -60,13 +63,6 @@ interface NumberLiteral {
|
|
|
60
63
|
value: number;
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
* String literal ('hello', '2025-10-01')
|
|
64
|
-
*/
|
|
65
|
-
interface StringLiteral {
|
|
66
|
-
type: "StringLiteral";
|
|
67
|
-
value: string;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
66
|
* Identifier (variable or function name)
|
|
71
67
|
*/
|
|
72
68
|
interface Identifier {
|
|
@@ -79,7 +75,7 @@ interface Identifier {
|
|
|
79
75
|
interface BinaryOp {
|
|
80
76
|
type: "BinaryOp";
|
|
81
77
|
left: ASTNode;
|
|
82
|
-
operator:
|
|
78
|
+
operator: Operator;
|
|
83
79
|
right: ASTNode;
|
|
84
80
|
}
|
|
85
81
|
/**
|
|
@@ -110,7 +106,6 @@ interface Assignment {
|
|
|
110
106
|
* Type guard functions for discriminated union narrowing
|
|
111
107
|
*/
|
|
112
108
|
declare function isNumberLiteral(node: ASTNode): node is NumberLiteral;
|
|
113
|
-
declare function isStringLiteral(node: ASTNode): node is StringLiteral;
|
|
114
109
|
declare function isIdentifier(node: ASTNode): node is Identifier;
|
|
115
110
|
declare function isBinaryOp(node: ASTNode): node is BinaryOp;
|
|
116
111
|
declare function isUnaryOp(node: ASTNode): node is UnaryOp;
|
|
@@ -131,7 +126,7 @@ declare function identifier(name: string): Identifier;
|
|
|
131
126
|
/**
|
|
132
127
|
* Create a binary operation node
|
|
133
128
|
*/
|
|
134
|
-
declare function binaryOp(left: ASTNode, operator:
|
|
129
|
+
declare function binaryOp(left: ASTNode, operator: Operator, right: ASTNode): BinaryOp;
|
|
135
130
|
/**
|
|
136
131
|
* Create a unary operation node (unary minus)
|
|
137
132
|
*/
|
|
@@ -192,10 +187,6 @@ declare class CodeGenerator {
|
|
|
192
187
|
*/
|
|
193
188
|
private generateNumberLiteral;
|
|
194
189
|
/**
|
|
195
|
-
* Generate code for a string literal
|
|
196
|
-
*/
|
|
197
|
-
private generateStringLiteral;
|
|
198
|
-
/**
|
|
199
190
|
* Generate code for an identifier
|
|
200
191
|
*/
|
|
201
192
|
private generateIdentifier;
|
|
@@ -237,9 +228,12 @@ declare class CodeGenerator {
|
|
|
237
228
|
*/
|
|
238
229
|
declare function generate(node: ASTNode): string;
|
|
239
230
|
/**
|
|
240
|
-
* Default execution context with common Math functions and
|
|
231
|
+
* Default execution context with common Math functions and timestamp utilities
|
|
241
232
|
* Users can use this as-is or spread it into their own context
|
|
242
233
|
*
|
|
234
|
+
* All date-related functions work with timestamps (milliseconds since Unix epoch)
|
|
235
|
+
* to maintain the language's numbers-only type system.
|
|
236
|
+
*
|
|
243
237
|
* @example
|
|
244
238
|
* // Use as-is
|
|
245
239
|
* execute('abs(-5)', defaultContext)
|
|
@@ -250,6 +244,11 @@ declare function generate(node: ASTNode): string;
|
|
|
250
244
|
* ...defaultContext,
|
|
251
245
|
* variables: { customVar: 42 }
|
|
252
246
|
* })
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* // Work with timestamps
|
|
250
|
+
* const result = execute('now() + days(7)', defaultContext)
|
|
251
|
+
* const futureDate = new Date(result) // Convert back to Date if needed
|
|
253
252
|
*/
|
|
254
253
|
declare const defaultContext: ExecutionContext;
|
|
255
254
|
/**
|
|
@@ -272,10 +271,6 @@ declare class Executor {
|
|
|
272
271
|
*/
|
|
273
272
|
private executeNumberLiteral;
|
|
274
273
|
/**
|
|
275
|
-
* Execute a string literal
|
|
276
|
-
*/
|
|
277
|
-
private executeStringLiteral;
|
|
278
|
-
/**
|
|
279
274
|
* Execute an identifier (variable reference)
|
|
280
275
|
*/
|
|
281
276
|
private executeIdentifier;
|
|
@@ -295,34 +290,6 @@ declare class Executor {
|
|
|
295
290
|
* Execute a variable assignment
|
|
296
291
|
*/
|
|
297
292
|
private executeAssignment;
|
|
298
|
-
/**
|
|
299
|
-
* Add two values (handles Date + number, number + Date, Date + Date, number + number)
|
|
300
|
-
*/
|
|
301
|
-
private add;
|
|
302
|
-
/**
|
|
303
|
-
* Subtract two values (handles Date - number, number - Date, Date - Date, number - number)
|
|
304
|
-
*/
|
|
305
|
-
private subtract;
|
|
306
|
-
/**
|
|
307
|
-
* Multiply two values (number * number only)
|
|
308
|
-
*/
|
|
309
|
-
private multiply;
|
|
310
|
-
/**
|
|
311
|
-
* Divide two values (number / number only)
|
|
312
|
-
*/
|
|
313
|
-
private divide;
|
|
314
|
-
/**
|
|
315
|
-
* Modulo operation (number % number only)
|
|
316
|
-
*/
|
|
317
|
-
private modulo;
|
|
318
|
-
/**
|
|
319
|
-
* Exponentiation operation (number ^ number only)
|
|
320
|
-
*/
|
|
321
|
-
private exponentiate;
|
|
322
|
-
/**
|
|
323
|
-
* Get type name for error messages
|
|
324
|
-
*/
|
|
325
|
-
private typeOf;
|
|
326
293
|
}
|
|
327
294
|
/**
|
|
328
295
|
* Execute source code with given context
|
|
@@ -349,6 +316,7 @@ declare class Lexer {
|
|
|
349
316
|
private skipWhitespaceAndComments;
|
|
350
317
|
/**
|
|
351
318
|
* Read a number token
|
|
319
|
+
* Supports: integers (42), decimals (3.14), and scientific notation (1.5e6, 2e-3)
|
|
352
320
|
*/
|
|
353
321
|
private readNumber;
|
|
354
322
|
/**
|
|
@@ -356,10 +324,6 @@ declare class Lexer {
|
|
|
356
324
|
*/
|
|
357
325
|
private readIdentifier;
|
|
358
326
|
/**
|
|
359
|
-
* Read a string token
|
|
360
|
-
*/
|
|
361
|
-
private readString;
|
|
362
|
-
/**
|
|
363
327
|
* Get character at position
|
|
364
328
|
*/
|
|
365
329
|
private getCharAt;
|
|
@@ -381,6 +345,14 @@ declare class Lexer {
|
|
|
381
345
|
private isWhitespace;
|
|
382
346
|
}
|
|
383
347
|
/**
|
|
348
|
+
* Optimize an AST by performing constant folding
|
|
349
|
+
* Recursively evaluates expressions with only literal values
|
|
350
|
+
*
|
|
351
|
+
* @param node - The AST node to optimize
|
|
352
|
+
* @returns Optimized AST node
|
|
353
|
+
*/
|
|
354
|
+
declare function optimize(node: ASTNode): ASTNode;
|
|
355
|
+
/**
|
|
384
356
|
* Parser using Pratt parsing (top-down operator precedence)
|
|
385
357
|
*/
|
|
386
358
|
declare class Parser {
|
|
@@ -427,6 +399,9 @@ declare class Parser {
|
|
|
427
399
|
}
|
|
428
400
|
/**
|
|
429
401
|
* Parse source code string into AST
|
|
402
|
+
*
|
|
403
|
+
* @param source - The source code to parse
|
|
404
|
+
* @returns Parsed AST
|
|
430
405
|
*/
|
|
431
406
|
declare function parseSource(source: string): ASTNode;
|
|
432
|
-
export { parseSource,
|
|
407
|
+
export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isBinaryOp, isAssignment, generate, execute, defaultContext, exports_ast as ast, TokenType, Token, RuntimeValue, Parser, Lexer, Executor, ExecutionContext, CodeGenerator, ASTNode };
|
package/dist/index.js
CHANGED
|
@@ -93,7 +93,6 @@ function negate(argument) {
|
|
|
93
93
|
var TokenType;
|
|
94
94
|
((TokenType2) => {
|
|
95
95
|
TokenType2["NUMBER"] = "NUMBER";
|
|
96
|
-
TokenType2["STRING"] = "STRING";
|
|
97
96
|
TokenType2["IDENTIFIER"] = "IDENTIFIER";
|
|
98
97
|
TokenType2["PLUS"] = "PLUS";
|
|
99
98
|
TokenType2["MINUS"] = "MINUS";
|
|
@@ -110,9 +109,6 @@ var TokenType;
|
|
|
110
109
|
function isNumberLiteral(node) {
|
|
111
110
|
return node.type === "NumberLiteral";
|
|
112
111
|
}
|
|
113
|
-
function isStringLiteral(node) {
|
|
114
|
-
return node.type === "StringLiteral";
|
|
115
|
-
}
|
|
116
112
|
function isIdentifier(node) {
|
|
117
113
|
return node.type === "Identifier";
|
|
118
114
|
}
|
|
@@ -139,8 +135,6 @@ class CodeGenerator {
|
|
|
139
135
|
return this.generateProgram(node);
|
|
140
136
|
if (isNumberLiteral(node))
|
|
141
137
|
return this.generateNumberLiteral(node);
|
|
142
|
-
if (isStringLiteral(node))
|
|
143
|
-
return this.generateStringLiteral(node);
|
|
144
138
|
if (isIdentifier(node))
|
|
145
139
|
return this.generateIdentifier(node);
|
|
146
140
|
if (isBinaryOp(node))
|
|
@@ -159,9 +153,6 @@ class CodeGenerator {
|
|
|
159
153
|
generateNumberLiteral(node) {
|
|
160
154
|
return String(node.value);
|
|
161
155
|
}
|
|
162
|
-
generateStringLiteral(node) {
|
|
163
|
-
return `'${node.value}'`;
|
|
164
|
-
}
|
|
165
156
|
generateIdentifier(node) {
|
|
166
157
|
return node.name;
|
|
167
158
|
}
|
|
@@ -244,13 +235,21 @@ var defaultContext = {
|
|
|
244
235
|
log: Math.log,
|
|
245
236
|
log10: Math.log10,
|
|
246
237
|
exp: Math.exp,
|
|
247
|
-
now: () =>
|
|
248
|
-
|
|
238
|
+
now: () => Date.now(),
|
|
239
|
+
timestamp: (year, month, day, hour = 0, minute = 0, second = 0) => new Date(year, month - 1, day, hour, minute, second).getTime(),
|
|
249
240
|
milliseconds: (ms) => ms,
|
|
250
241
|
seconds: (s) => s * 1000,
|
|
251
242
|
minutes: (m) => m * 60 * 1000,
|
|
252
243
|
hours: (h) => h * 60 * 60 * 1000,
|
|
253
|
-
days: (d) => d * 24 * 60 * 60 * 1000
|
|
244
|
+
days: (d) => d * 24 * 60 * 60 * 1000,
|
|
245
|
+
weeks: (w) => w * 7 * 24 * 60 * 60 * 1000,
|
|
246
|
+
year: (timestamp) => new Date(timestamp).getFullYear(),
|
|
247
|
+
month: (timestamp) => new Date(timestamp).getMonth() + 1,
|
|
248
|
+
day: (timestamp) => new Date(timestamp).getDate(),
|
|
249
|
+
hour: (timestamp) => new Date(timestamp).getHours(),
|
|
250
|
+
minute: (timestamp) => new Date(timestamp).getMinutes(),
|
|
251
|
+
second: (timestamp) => new Date(timestamp).getSeconds(),
|
|
252
|
+
weekday: (timestamp) => new Date(timestamp).getDay()
|
|
254
253
|
}
|
|
255
254
|
};
|
|
256
255
|
// src/lexer.ts
|
|
@@ -281,9 +280,6 @@ class Lexer {
|
|
|
281
280
|
if (this.isDigit(char)) {
|
|
282
281
|
return this.readNumber();
|
|
283
282
|
}
|
|
284
|
-
if (char === "'") {
|
|
285
|
-
return this.readString();
|
|
286
|
-
}
|
|
287
283
|
if (this.isLetter(char) || char === "_") {
|
|
288
284
|
return this.readIdentifier();
|
|
289
285
|
}
|
|
@@ -345,13 +341,28 @@ class Lexer {
|
|
|
345
341
|
readNumber() {
|
|
346
342
|
const start = this.position;
|
|
347
343
|
let hasDecimal = false;
|
|
344
|
+
let hasExponent = false;
|
|
348
345
|
while (this.position < this.source.length) {
|
|
349
346
|
const char = this.getCharAt(this.position);
|
|
350
347
|
if (this.isDigit(char)) {
|
|
351
348
|
this.position++;
|
|
352
|
-
} else if (char === "." && !hasDecimal) {
|
|
349
|
+
} else if (char === "." && !hasDecimal && !hasExponent) {
|
|
353
350
|
hasDecimal = true;
|
|
354
351
|
this.position++;
|
|
352
|
+
} else if ((char === "e" || char === "E") && !hasExponent) {
|
|
353
|
+
hasExponent = true;
|
|
354
|
+
this.position++;
|
|
355
|
+
const nextChar = this.getCharAt(this.position);
|
|
356
|
+
if (nextChar === "+" || nextChar === "-") {
|
|
357
|
+
this.position++;
|
|
358
|
+
}
|
|
359
|
+
if (!this.isDigit(this.getCharAt(this.position))) {
|
|
360
|
+
throw new Error(`Invalid number: expected digit after exponent at position ${this.position}`);
|
|
361
|
+
}
|
|
362
|
+
while (this.position < this.source.length && this.isDigit(this.getCharAt(this.position))) {
|
|
363
|
+
this.position++;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
355
366
|
} else {
|
|
356
367
|
break;
|
|
357
368
|
}
|
|
@@ -372,47 +383,6 @@ class Lexer {
|
|
|
372
383
|
const name = this.source.slice(start, this.position);
|
|
373
384
|
return { type: "IDENTIFIER" /* IDENTIFIER */, value: name, position: start };
|
|
374
385
|
}
|
|
375
|
-
readString() {
|
|
376
|
-
const start = this.position;
|
|
377
|
-
this.position++;
|
|
378
|
-
let value = "";
|
|
379
|
-
while (this.position < this.source.length) {
|
|
380
|
-
const char = this.getCharAt(this.position);
|
|
381
|
-
if (char === "'") {
|
|
382
|
-
this.position++;
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
if (char === "\\" && this.position + 1 < this.source.length) {
|
|
386
|
-
this.position++;
|
|
387
|
-
const escaped = this.getCharAt(this.position);
|
|
388
|
-
switch (escaped) {
|
|
389
|
-
case "n":
|
|
390
|
-
value += `
|
|
391
|
-
`;
|
|
392
|
-
break;
|
|
393
|
-
case "t":
|
|
394
|
-
value += "\t";
|
|
395
|
-
break;
|
|
396
|
-
case "r":
|
|
397
|
-
value += "\r";
|
|
398
|
-
break;
|
|
399
|
-
case "'":
|
|
400
|
-
value += "'";
|
|
401
|
-
break;
|
|
402
|
-
case "\\":
|
|
403
|
-
value += "\\";
|
|
404
|
-
break;
|
|
405
|
-
default:
|
|
406
|
-
value += escaped;
|
|
407
|
-
}
|
|
408
|
-
this.position++;
|
|
409
|
-
} else {
|
|
410
|
-
value += char;
|
|
411
|
-
this.position++;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return { type: "STRING" /* STRING */, value, position: start };
|
|
415
|
-
}
|
|
416
386
|
getCharAt(pos) {
|
|
417
387
|
return pos < this.source.length ? this.source[pos] || "" : "";
|
|
418
388
|
}
|
|
@@ -521,13 +491,6 @@ class Parser {
|
|
|
521
491
|
value: token.value
|
|
522
492
|
};
|
|
523
493
|
}
|
|
524
|
-
if (token.type === "STRING" /* STRING */) {
|
|
525
|
-
this.advance();
|
|
526
|
-
return {
|
|
527
|
-
type: "StringLiteral",
|
|
528
|
-
value: token.value
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
494
|
if (token.type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
532
495
|
const name = token.value;
|
|
533
496
|
this.advance();
|
|
@@ -620,8 +583,6 @@ class Executor {
|
|
|
620
583
|
return this.executeProgram(node);
|
|
621
584
|
if (isNumberLiteral(node))
|
|
622
585
|
return this.executeNumberLiteral(node);
|
|
623
|
-
if (isStringLiteral(node))
|
|
624
|
-
return this.executeStringLiteral(node);
|
|
625
586
|
if (isIdentifier(node))
|
|
626
587
|
return this.executeIdentifier(node);
|
|
627
588
|
if (isBinaryOp(node))
|
|
@@ -635,7 +596,7 @@ class Executor {
|
|
|
635
596
|
throw new Error(`Unknown node type`);
|
|
636
597
|
}
|
|
637
598
|
executeProgram(node) {
|
|
638
|
-
let result;
|
|
599
|
+
let result = 0;
|
|
639
600
|
for (const statement of node.statements) {
|
|
640
601
|
result = this.execute(statement);
|
|
641
602
|
}
|
|
@@ -644,9 +605,6 @@ class Executor {
|
|
|
644
605
|
executeNumberLiteral(node) {
|
|
645
606
|
return node.value;
|
|
646
607
|
}
|
|
647
|
-
executeStringLiteral(node) {
|
|
648
|
-
return node.value;
|
|
649
|
-
}
|
|
650
608
|
executeIdentifier(node) {
|
|
651
609
|
const value = this.variables.get(node.name);
|
|
652
610
|
if (value === undefined) {
|
|
@@ -659,17 +617,23 @@ class Executor {
|
|
|
659
617
|
const right = this.execute(node.right);
|
|
660
618
|
switch (node.operator) {
|
|
661
619
|
case "+":
|
|
662
|
-
return
|
|
620
|
+
return left + right;
|
|
663
621
|
case "-":
|
|
664
|
-
return
|
|
622
|
+
return left - right;
|
|
665
623
|
case "*":
|
|
666
|
-
return
|
|
624
|
+
return left * right;
|
|
667
625
|
case "/":
|
|
668
|
-
|
|
626
|
+
if (right === 0) {
|
|
627
|
+
throw new Error("Division by zero");
|
|
628
|
+
}
|
|
629
|
+
return left / right;
|
|
669
630
|
case "%":
|
|
670
|
-
|
|
631
|
+
if (right === 0) {
|
|
632
|
+
throw new Error("Modulo by zero");
|
|
633
|
+
}
|
|
634
|
+
return left % right;
|
|
671
635
|
case "^":
|
|
672
|
-
return
|
|
636
|
+
return left ** right;
|
|
673
637
|
default:
|
|
674
638
|
throw new Error(`Unknown operator: ${node.operator}`);
|
|
675
639
|
}
|
|
@@ -677,13 +641,7 @@ class Executor {
|
|
|
677
641
|
executeUnaryOp(node) {
|
|
678
642
|
const arg = this.execute(node.argument);
|
|
679
643
|
if (node.operator === "-") {
|
|
680
|
-
|
|
681
|
-
return -arg;
|
|
682
|
-
}
|
|
683
|
-
if (arg instanceof Date) {
|
|
684
|
-
return new Date(-arg.getTime());
|
|
685
|
-
}
|
|
686
|
-
throw new Error(`Cannot negate ${typeof arg}`);
|
|
644
|
+
return -arg;
|
|
687
645
|
}
|
|
688
646
|
throw new Error(`Unknown unary operator: ${node.operator}`);
|
|
689
647
|
}
|
|
@@ -700,88 +658,82 @@ class Executor {
|
|
|
700
658
|
}
|
|
701
659
|
executeAssignment(node) {
|
|
702
660
|
const value = this.execute(node.value);
|
|
703
|
-
if (typeof value !== "number" && !(value instanceof Date)) {
|
|
704
|
-
throw new Error(`Cannot assign ${typeof value} to variable. Only numbers and Dates are allowed.`);
|
|
705
|
-
}
|
|
706
661
|
this.variables.set(node.name, value);
|
|
707
662
|
return value;
|
|
708
663
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
throw new Error(`Cannot add ${this.typeOf(left)} and ${this.typeOf(right)}`);
|
|
664
|
+
}
|
|
665
|
+
function execute(source, context) {
|
|
666
|
+
const ast = parseSource(source);
|
|
667
|
+
const executor = new Executor(context);
|
|
668
|
+
return executor.execute(ast);
|
|
669
|
+
}
|
|
670
|
+
// src/optimizer.ts
|
|
671
|
+
function optimize(node) {
|
|
672
|
+
if (isProgram(node)) {
|
|
673
|
+
return {
|
|
674
|
+
...node,
|
|
675
|
+
statements: node.statements.map((stmt) => optimize(stmt))
|
|
676
|
+
};
|
|
723
677
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return
|
|
678
|
+
if (isAssignment(node)) {
|
|
679
|
+
return {
|
|
680
|
+
...node,
|
|
681
|
+
value: optimize(node.value)
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (isBinaryOp(node)) {
|
|
685
|
+
const left = optimize(node.left);
|
|
686
|
+
const right = optimize(node.right);
|
|
687
|
+
if (isNumberLiteral(left) && isNumberLiteral(right)) {
|
|
688
|
+
const result = evaluateBinaryOp(node.operator, left.value, right.value);
|
|
689
|
+
return number(result);
|
|
736
690
|
}
|
|
737
|
-
|
|
691
|
+
return {
|
|
692
|
+
...node,
|
|
693
|
+
left,
|
|
694
|
+
right
|
|
695
|
+
};
|
|
738
696
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
697
|
+
if (isUnaryOp(node)) {
|
|
698
|
+
const argument = optimize(node.argument);
|
|
699
|
+
if (isNumberLiteral(argument)) {
|
|
700
|
+
return number(-argument.value);
|
|
742
701
|
}
|
|
743
|
-
|
|
702
|
+
return {
|
|
703
|
+
...node,
|
|
704
|
+
argument
|
|
705
|
+
};
|
|
744
706
|
}
|
|
745
|
-
|
|
746
|
-
|
|
707
|
+
return node;
|
|
708
|
+
}
|
|
709
|
+
function evaluateBinaryOp(operator, left, right) {
|
|
710
|
+
switch (operator) {
|
|
711
|
+
case "+":
|
|
712
|
+
return left + right;
|
|
713
|
+
case "-":
|
|
714
|
+
return left - right;
|
|
715
|
+
case "*":
|
|
716
|
+
return left * right;
|
|
717
|
+
case "/":
|
|
747
718
|
if (right === 0) {
|
|
748
|
-
throw new Error("Division by zero");
|
|
719
|
+
throw new Error("Division by zero in constant folding");
|
|
749
720
|
}
|
|
750
721
|
return left / right;
|
|
751
|
-
|
|
752
|
-
throw new Error(`Cannot divide ${this.typeOf(left)} by ${this.typeOf(right)}`);
|
|
753
|
-
}
|
|
754
|
-
modulo(left, right) {
|
|
755
|
-
if (typeof left === "number" && typeof right === "number") {
|
|
722
|
+
case "%":
|
|
756
723
|
if (right === 0) {
|
|
757
|
-
throw new Error("
|
|
724
|
+
throw new Error("Modulo by zero in constant folding");
|
|
758
725
|
}
|
|
759
726
|
return left % right;
|
|
760
|
-
|
|
761
|
-
throw new Error(`Cannot compute ${this.typeOf(left)} modulo ${this.typeOf(right)}`);
|
|
762
|
-
}
|
|
763
|
-
exponentiate(left, right) {
|
|
764
|
-
if (typeof left === "number" && typeof right === "number") {
|
|
727
|
+
case "^":
|
|
765
728
|
return left ** right;
|
|
766
|
-
|
|
767
|
-
|
|
729
|
+
default:
|
|
730
|
+
throw new Error(`Unknown operator: ${operator}`);
|
|
768
731
|
}
|
|
769
|
-
typeOf(value) {
|
|
770
|
-
if (value instanceof Date) {
|
|
771
|
-
return "Date";
|
|
772
|
-
}
|
|
773
|
-
return typeof value;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
function execute(source, context) {
|
|
777
|
-
const ast = parseSource(source);
|
|
778
|
-
const executor = new Executor(context);
|
|
779
|
-
return executor.execute(ast);
|
|
780
732
|
}
|
|
781
733
|
export {
|
|
782
734
|
parseSource,
|
|
735
|
+
optimize,
|
|
783
736
|
isUnaryOp,
|
|
784
|
-
isStringLiteral,
|
|
785
737
|
isProgram,
|
|
786
738
|
isNumberLiteral,
|
|
787
739
|
isIdentifier,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlewing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|