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 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** - 3.61 KB gzipped, zero dependencies
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** - 71 tests, 97.66% coverage
12
- - 📐 **Math Expressions** - Numbers, dates, operators, functions, variables
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
- ### Date Support
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
- // Dates as first-class citizens
67
- execute("now()", defaultContext); // → Date object (current time)
68
- execute("date('2025-10-01')", defaultContext); // → Date object
69
- execute("now() + minutes(30)", defaultContext); // Date 30 minutes from now
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(1)", defaultContext); // → 86400000 (milliseconds)
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; // number
113
+ 42; // integer
95
114
  3.14; // floating point
96
- ("hello"); // string (function arguments only)
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
- date("2025-01-01"); // → Date object
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): RuntimeValue`
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
- // Date functions
253
- (now, date);
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
- ## Type Definitions
336
+ ## Advanced Features
260
337
 
261
- ### ExecutionContext
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
- interface ExecutionContext {
265
- functions?: Record<string, (...args: any[]) => number | Date>;
266
- variables?: Record<string, number | Date>;
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
- ### RuntimeValue
352
+ **With optimization:**
271
353
 
272
354
  ```typescript
273
- type RuntimeValue = number | Date | unknown;
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
- ### ASTNode
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
- type ASTNode =
280
- | Program
281
- | NumberLiteral
282
- | StringLiteral
283
- | Identifier
284
- | BinaryOp
285
- | UnaryOp
286
- | FunctionCall
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) as number;
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
- ### Date Arithmetic
428
+ ### Timestamp Arithmetic
324
429
 
325
430
  ```typescript
326
431
  import { execute, defaultContext } from "littlewing";
327
432
 
328
- // 5 days from now
329
- const deadline = execute("now() + days(5)", defaultContext);
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
- // Add 2 hours and 30 minutes
332
- const futureTime = execute("now() + hours(2) + minutes(30)", defaultContext);
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
- - Raw: 16.70 KB
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
- - 71 comprehensive tests
372
- - 97.66% code coverage
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
- - Runtime type validation
381
- - Type guards on all operations
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
- - Invalid assignments: `"Cannot assign string to variable"`
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 - can be a number, Date, or unknown (for function results)
5
+ * Runtime value type - only numbers
6
6
  */
7
- type RuntimeValue = number | Date | unknown;
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 or Date
11
- * Variables must be numbers or Dates
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 | Date>;
15
- variables?: Record<string, number | Date>;
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 | StringLiteral | Identifier | BinaryOp | UnaryOp | FunctionCall | Assignment;
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: "+" | "-" | "*" | "/" | "%" | "^", right: ASTNode): BinaryOp;
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 date utilities
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, isUnaryOp, isStringLiteral, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isBinaryOp, isAssignment, generate, execute, defaultContext, exports_ast as ast, TokenType, Token, RuntimeValue, Parser, Lexer, Executor, ExecutionContext, CodeGenerator, ASTNode };
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: () => new Date,
248
- date: (...args) => new Date(...args),
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 this.add(left, right);
620
+ return left + right;
663
621
  case "-":
664
- return this.subtract(left, right);
622
+ return left - right;
665
623
  case "*":
666
- return this.multiply(left, right);
624
+ return left * right;
667
625
  case "/":
668
- return this.divide(left, right);
626
+ if (right === 0) {
627
+ throw new Error("Division by zero");
628
+ }
629
+ return left / right;
669
630
  case "%":
670
- return this.modulo(left, right);
631
+ if (right === 0) {
632
+ throw new Error("Modulo by zero");
633
+ }
634
+ return left % right;
671
635
  case "^":
672
- return this.exponentiate(left, right);
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
- if (typeof arg === "number") {
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
- add(left, right) {
710
- if (typeof left === "number" && typeof right === "number") {
711
- return left + right;
712
- }
713
- if (left instanceof Date && typeof right === "number") {
714
- return new Date(left.getTime() + right);
715
- }
716
- if (typeof left === "number" && right instanceof Date) {
717
- return new Date(right.getTime() + left);
718
- }
719
- if (left instanceof Date && right instanceof Date) {
720
- return new Date(left.getTime() + right.getTime());
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
- subtract(left, right) {
725
- if (typeof left === "number" && typeof right === "number") {
726
- return left - right;
727
- }
728
- if (left instanceof Date && typeof right === "number") {
729
- return new Date(left.getTime() - right);
730
- }
731
- if (typeof left === "number" && right instanceof Date) {
732
- return new Date(left - right.getTime());
733
- }
734
- if (left instanceof Date && right instanceof Date) {
735
- return left.getTime() - right.getTime();
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
- throw new Error(`Cannot subtract ${this.typeOf(right)} from ${this.typeOf(left)}`);
691
+ return {
692
+ ...node,
693
+ left,
694
+ right
695
+ };
738
696
  }
739
- multiply(left, right) {
740
- if (typeof left === "number" && typeof right === "number") {
741
- return left * right;
697
+ if (isUnaryOp(node)) {
698
+ const argument = optimize(node.argument);
699
+ if (isNumberLiteral(argument)) {
700
+ return number(-argument.value);
742
701
  }
743
- throw new Error(`Cannot multiply ${this.typeOf(left)} and ${this.typeOf(right)}`);
702
+ return {
703
+ ...node,
704
+ argument
705
+ };
744
706
  }
745
- divide(left, right) {
746
- if (typeof left === "number" && typeof right === "number") {
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("Division by zero");
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
- throw new Error(`Cannot exponentiate ${this.typeOf(left)} by ${this.typeOf(right)}`);
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.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",