littlewing 0.3.1 → 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** - 106 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");
@@ -201,6 +221,23 @@ const regenerated = generate(tree); // → "2 + 3 * 4"
201
221
  parseSource(regenerated); // Same AST structure
202
222
  ```
203
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
+
204
241
  ### Classes
205
242
 
206
243
  #### `Lexer`
@@ -262,6 +299,7 @@ ast.subtract(left, right);
262
299
  ast.multiply(left, right);
263
300
  ast.divide(left, right);
264
301
  ast.modulo(left, right);
302
+ ast.exponentiate(left, right);
265
303
  ast.negate(argument);
266
304
  ast.assign("x", value);
267
305
  ast.functionCall("abs", [ast.number(-5)]);
@@ -278,11 +316,79 @@ import { defaultContext } from "littlewing";
278
316
  (abs, ceil, floor, round, sqrt, min, max);
279
317
  (sin, cos, tan, log, log10, exp);
280
318
 
281
- // Date functions
282
- (now, date);
319
+ // Timestamp functions
320
+ now(); // Current timestamp
321
+ timestamp(year, month, day); // Create timestamp from date components
283
322
 
284
323
  // Time conversion (returns milliseconds)
285
- (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)
334
+ ```
335
+
336
+ ## Advanced Features
337
+
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:**
343
+
344
+ ```typescript
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
350
+ ```
351
+
352
+ **With optimization:**
353
+
354
+ ```typescript
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!
360
+ ```
361
+
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:
382
+
383
+ ```typescript
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)
286
392
  ```
287
393
 
288
394
  ## Examples
@@ -293,7 +399,7 @@ import { defaultContext } from "littlewing";
293
399
  import { execute, defaultContext } from "littlewing";
294
400
 
295
401
  function calculate(expression: string): number {
296
- return execute(expression, defaultContext) as number;
402
+ return execute(expression, defaultContext);
297
403
  }
298
404
 
299
405
  calculate("2 + 2 * 3"); // → 8
@@ -316,18 +422,28 @@ const context = {
316
422
  };
317
423
 
318
424
  const compound = execute("principal * (1 + rate) ^ years", context);
425
+ // → 1102.5
319
426
  ```
320
427
 
321
- ### Date Arithmetic
428
+ ### Timestamp Arithmetic
322
429
 
323
430
  ```typescript
324
431
  import { execute, defaultContext } from "littlewing";
325
432
 
326
- // 5 days from now
327
- 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
328
436
 
329
- // Add 2 hours and 30 minutes
330
- const futureTime = execute("now() + hours(2) + minutes(30)", defaultContext);
437
+ // Complex time calculations
438
+ const result = execute("now() + weeks(2) + days(3) + hours(4)", defaultContext);
439
+
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);
331
447
  ```
332
448
 
333
449
  ### Custom Functions
@@ -339,7 +455,7 @@ const context = {
339
455
  functions: {
340
456
  fahrenheit: (celsius) => (celsius * 9) / 5 + 32,
341
457
  kilometers: (miles) => miles * 1.60934,
342
- factorial: (n) => (n <= 1 ? 1 : n * factorial(n - 1)),
458
+ factorial: (n) => (n <= 1 ? 1 : n * context.functions.factorial(n - 1)),
343
459
  },
344
460
  variables: {
345
461
  roomTemp: 20,
@@ -350,33 +466,54 @@ execute("fahrenheit(roomTemp)", context); // → 68
350
466
  execute("kilometers(5)", context); // → 8.0467
351
467
  ```
352
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
+
353
488
  ## Performance
354
489
 
355
490
  ### Algorithms
356
491
 
357
492
  - **Lexer**: O(n) single-pass tokenization
358
493
  - **Parser**: Optimal Pratt parsing with O(n) time complexity
359
- - **Executor**: O(n) tree-walk evaluation
494
+ - **Executor**: O(n) tree-walk evaluation with no type checking overhead
360
495
 
361
496
  ### Bundle Size
362
497
 
363
- - Raw: 21.06 KB
364
- - Gzipped: **4.26 KB**
498
+ - **4.19 KB gzipped** (19.60 KB raw)
365
499
  - Zero dependencies
500
+ - Includes optimizer for constant folding
501
+ - Fully tree-shakeable
366
502
 
367
503
  ### Test Coverage
368
504
 
369
- - 106 comprehensive tests
370
- - 95.99% code coverage
505
+ - **128 tests** with **98.61% line coverage**
506
+ - **98.52% function coverage**
371
507
  - All edge cases handled
508
+ - Type-safe execution guaranteed
372
509
 
373
510
  ## Type Safety
374
511
 
375
512
  - Strict TypeScript mode
376
513
  - Zero implicit `any` types
377
514
  - Complete type annotations
378
- - Runtime type validation
379
- - Type guards on all operations
515
+ - Single `RuntimeValue = number` type
516
+ - No runtime type checking overhead
380
517
 
381
518
  ## Error Handling
382
519
 
@@ -384,9 +521,9 @@ Clear, actionable error messages for:
384
521
 
385
522
  - Undefined variables: `"Undefined variable: x"`
386
523
  - Undefined functions: `"Undefined function: abs"`
387
- - Type mismatches: `"Cannot add string and number"`
388
524
  - Division by zero: `"Division by zero"`
389
- - Invalid assignments: `"Cannot assign string to variable"`
525
+ - Modulo by zero: `"Modulo by zero"`
526
+ - Syntax errors with position information
390
527
 
391
528
  ## Browser Support
392
529
 
@@ -399,6 +536,18 @@ Clear, actionable error messages for:
399
536
 
400
537
  Works with Node.js 18+ via ESM imports.
401
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
+
402
551
  ## License
403
552
 
404
553
  MIT
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.1",
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",