littlewing 0.5.2 → 0.6.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 +145 -682
- package/dist/index.d.ts +36 -62
- package/dist/index.js +82 -513
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
# littlewing
|
|
2
2
|
|
|
3
|
-
A minimal, high-performance arithmetic expression language
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🚀 **Minimal & Fast** - O(n) algorithms throughout (lexer, parser, executor)
|
|
8
|
-
- 📦 **Small Bundle** - 6.89 KB gzipped, zero dependencies
|
|
9
|
-
- 🌐 **Browser Ready** - 100% ESM, no Node.js APIs
|
|
10
|
-
- 🔒 **Type-Safe** - Strict TypeScript with full type coverage
|
|
11
|
-
- ✅ **Thoroughly Tested** - 247 tests, 98.61% line coverage
|
|
12
|
-
- 📐 **Pure Arithmetic** - Numbers-only, clean semantics
|
|
13
|
-
- 🎯 **Clean API** - Intuitive dual API (class-based + functional)
|
|
14
|
-
- 📝 **Well Documented** - Complete JSDoc and examples
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install littlewing
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
### Basic Usage
|
|
3
|
+
A minimal, high-performance arithmetic expression language for JavaScript. Pure numbers, zero dependencies, built for the browser.
|
|
25
4
|
|
|
26
5
|
```typescript
|
|
27
6
|
import { execute, defaultContext } from "littlewing";
|
|
@@ -29,762 +8,246 @@ import { execute, defaultContext } from "littlewing";
|
|
|
29
8
|
// Simple arithmetic
|
|
30
9
|
execute("2 + 3 * 4"); // → 14
|
|
31
10
|
|
|
32
|
-
// Variables
|
|
33
|
-
execute("
|
|
34
|
-
|
|
35
|
-
// Functions from context
|
|
36
|
-
execute("abs(-42)", defaultContext); // → 42
|
|
37
|
-
execute("sqrt(16)", defaultContext); // → 4
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### With Custom Context
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
import { execute } from "littlewing";
|
|
44
|
-
|
|
45
|
-
const context = {
|
|
46
|
-
functions: {
|
|
47
|
-
double: (n) => n * 2,
|
|
48
|
-
triple: (n) => n * 3,
|
|
49
|
-
},
|
|
50
|
-
variables: {
|
|
51
|
-
pi: 3.14159,
|
|
52
|
-
maxValue: 100,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
execute("double(5)", context); // → 10
|
|
57
|
-
execute("pi * 2", context); // → 6.28318
|
|
58
|
-
execute("maxValue - 25", context); // → 75
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Timestamp Arithmetic
|
|
62
|
-
|
|
63
|
-
Littlewing uses a numbers-only type system. Timestamps (milliseconds since Unix epoch) are just numbers, enabling clean date arithmetic:
|
|
11
|
+
// Variables and functions
|
|
12
|
+
execute("radius = 5; area = 3.14159 * radius ^ 2", defaultContext); // → 78.54
|
|
64
13
|
|
|
65
|
-
|
|
66
|
-
|
|
14
|
+
// Date arithmetic with timestamps
|
|
15
|
+
execute("deadline = now() + days(7)", defaultContext); // → timestamp 7 days from now
|
|
67
16
|
|
|
68
|
-
//
|
|
69
|
-
execute("
|
|
70
|
-
|
|
71
|
-
//
|
|
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
|
|
77
|
-
|
|
78
|
-
// Time conversion helpers
|
|
79
|
-
execute("seconds(30)", defaultContext); // → 30000 (milliseconds)
|
|
80
|
-
execute("minutes(5)", defaultContext); // → 300000 (milliseconds)
|
|
81
|
-
execute("hours(2)", defaultContext); // → 7200000 (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
|
|
17
|
+
// Conditional logic
|
|
18
|
+
execute("score = 85; grade = score >= 90 ? 100 : 90", {
|
|
19
|
+
variables: { score: 85 },
|
|
20
|
+
}); // → 90
|
|
95
21
|
```
|
|
96
22
|
|
|
97
|
-
|
|
23
|
+
## Features
|
|
98
24
|
|
|
99
|
-
|
|
100
|
-
|
|
25
|
+
- **Numbers-only** - Every value is a number. Simple, predictable, fast.
|
|
26
|
+
- **Zero dependencies** - 5.20 KB gzipped, perfect for browser bundles
|
|
27
|
+
- **O(n) performance** - Linear time parsing and execution
|
|
28
|
+
- **Safe evaluation** - No eval(), no code generation, no security risks
|
|
29
|
+
- **Timestamp arithmetic** - Built-in date/time functions using numeric timestamps
|
|
30
|
+
- **Extensible** - Add custom functions and variables via context
|
|
31
|
+
- **Type-safe** - Full TypeScript support with strict types
|
|
101
32
|
|
|
102
|
-
|
|
33
|
+
## Installation
|
|
103
34
|
|
|
104
|
-
|
|
105
|
-
|
|
35
|
+
```bash
|
|
36
|
+
npm install littlewing
|
|
106
37
|
```
|
|
107
38
|
|
|
108
|
-
##
|
|
109
|
-
|
|
110
|
-
### Literals
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
42; // integer
|
|
114
|
-
3.14; // floating point
|
|
115
|
-
1.5e6; // scientific notation (1500000)
|
|
116
|
-
2e-3; // negative exponent (0.002)
|
|
117
|
-
```
|
|
39
|
+
## Quick Start
|
|
118
40
|
|
|
119
|
-
###
|
|
41
|
+
### Basic Usage
|
|
120
42
|
|
|
121
43
|
```typescript
|
|
122
|
-
|
|
123
|
-
y = x + 10;
|
|
124
|
-
z = x * y;
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Operators
|
|
44
|
+
import { execute } from "littlewing";
|
|
128
45
|
|
|
129
|
-
|
|
46
|
+
// Arithmetic expressions
|
|
47
|
+
execute("2 + 3 * 4"); // → 14
|
|
48
|
+
execute("10 ^ 2"); // → 100
|
|
49
|
+
execute("17 % 5"); // → 2
|
|
130
50
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
10 - 4; // subtraction
|
|
134
|
-
3 * 4; // multiplication
|
|
135
|
-
10 / 2; // division
|
|
136
|
-
10 % 3; // modulo
|
|
137
|
-
2 ^ 3; // exponentiation (power)
|
|
138
|
-
-5; // unary minus
|
|
139
|
-
```
|
|
51
|
+
// Variables
|
|
52
|
+
execute("x = 10; y = 20; x + y"); // → 30
|
|
140
53
|
|
|
141
|
-
|
|
54
|
+
// Comparisons (return 1 for true, 0 for false)
|
|
55
|
+
execute("5 > 3"); // → 1
|
|
56
|
+
execute("10 == 10"); // → 1
|
|
57
|
+
execute("2 != 2"); // → 0
|
|
142
58
|
|
|
143
|
-
|
|
59
|
+
// Logical operators
|
|
60
|
+
execute("1 && 1"); // → 1
|
|
61
|
+
execute("0 || 1"); // → 1
|
|
144
62
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
5 != 3; // not equal → 1
|
|
148
|
-
5 > 3; // greater than → 1
|
|
149
|
-
5 < 3; // less than → 0
|
|
150
|
-
5 >= 5; // greater than or equal → 1
|
|
151
|
-
5 <= 3; // less than or equal → 0
|
|
63
|
+
// Ternary conditionals
|
|
64
|
+
execute("age >= 18 ? 100 : 0", { variables: { age: 21 } }); // → 100
|
|
152
65
|
```
|
|
153
66
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Returns `1` for true, `0` for false. Treats `0` as false, any non-zero value as true:
|
|
67
|
+
### With Built-in Functions
|
|
157
68
|
|
|
158
69
|
```typescript
|
|
159
|
-
|
|
160
|
-
1 && 0; // logical AND → 0 (right is falsy)
|
|
161
|
-
0 || 1; // logical OR → 1 (right is truthy)
|
|
162
|
-
0 || 0; // logical OR → 0 (both falsy)
|
|
163
|
-
|
|
164
|
-
// Commonly used with comparisons
|
|
165
|
-
5 > 3 && 10 > 8; // → 1 (both conditions true)
|
|
166
|
-
5 < 3 || 10 > 8; // → 1 (second condition true)
|
|
167
|
-
age >= 18 && age <= 65; // age range check
|
|
168
|
-
isStudent || age >= 65; // student or senior discount
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### Ternary Operator
|
|
172
|
-
|
|
173
|
-
Conditional expression with `? :` syntax:
|
|
70
|
+
import { execute, defaultContext } from "littlewing";
|
|
174
71
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
72
|
+
// Math functions
|
|
73
|
+
execute("abs(-42)", defaultContext); // → 42
|
|
74
|
+
execute("sqrt(16)", defaultContext); // → 4
|
|
75
|
+
execute("max(3, 7, 2)", defaultContext); // → 7
|
|
179
76
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
```
|
|
77
|
+
// Current timestamp
|
|
78
|
+
execute("now()", defaultContext); // → 1704067200000
|
|
183
79
|
|
|
184
|
-
|
|
80
|
+
// Date arithmetic
|
|
81
|
+
execute("now() + hours(2)", defaultContext); // → timestamp 2 hours from now
|
|
82
|
+
execute("tomorrow = now() + days(1)", defaultContext); // → tomorrow's timestamp
|
|
185
83
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
84
|
+
// Extract date components
|
|
85
|
+
const ctx = { ...defaultContext, variables: { ts: Date.now() } };
|
|
86
|
+
execute("year(ts)", ctx); // → 2024
|
|
87
|
+
execute("month(ts)", ctx); // → 11
|
|
88
|
+
execute("day(ts)", ctx); // → 6
|
|
189
89
|
```
|
|
190
90
|
|
|
191
|
-
|
|
91
|
+
### Custom Functions and Variables
|
|
192
92
|
|
|
193
93
|
```typescript
|
|
194
|
-
|
|
195
|
-
execute("price * 2", { variables: { price: 50 } }); // → 100
|
|
196
|
-
execute("price * 2", {}); // Error: Undefined variable: price
|
|
197
|
-
|
|
198
|
-
// With ??=
|
|
199
|
-
execute("price ??= 100; price * 2", { variables: { price: 50 } }); // → 100 (uses existing)
|
|
200
|
-
execute("price ??= 100; price * 2", {}); // → 200 (uses default)
|
|
201
|
-
```
|
|
94
|
+
import { execute } from "littlewing";
|
|
202
95
|
|
|
203
|
-
|
|
96
|
+
const context = {
|
|
97
|
+
functions: {
|
|
98
|
+
// Custom functions must return numbers
|
|
99
|
+
fahrenheit: (celsius: number) => (celsius * 9) / 5 + 32,
|
|
100
|
+
discount: (price: number, percent: number) => price * (1 - percent / 100),
|
|
101
|
+
},
|
|
102
|
+
variables: {
|
|
103
|
+
pi: 3.14159,
|
|
104
|
+
taxRate: 0.08,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
204
107
|
|
|
205
|
-
|
|
206
|
-
execute("
|
|
207
|
-
execute("
|
|
108
|
+
execute("fahrenheit(20)", context); // → 68
|
|
109
|
+
execute("discount(100, 15)", context); // → 85
|
|
110
|
+
execute("100 * (1 + taxRate)", context); // → 108
|
|
208
111
|
```
|
|
209
112
|
|
|
210
|
-
###
|
|
211
|
-
|
|
212
|
-
From lowest to highest:
|
|
213
|
-
|
|
214
|
-
1. Assignment (`=`, `??=`) - Lowest
|
|
215
|
-
2. Ternary conditional (`? :`)
|
|
216
|
-
3. Logical OR (`||`)
|
|
217
|
-
4. Logical AND (`&&`)
|
|
218
|
-
5. Comparison (`==`, `!=`, `<`, `>`, `<=`, `>=`)
|
|
219
|
-
6. Addition, subtraction (`+`, `-`)
|
|
220
|
-
7. Multiplication, division, modulo (`*`, `/`, `%`)
|
|
221
|
-
8. Exponentiation (`^`)
|
|
222
|
-
9. Unary minus (`-`) - Highest
|
|
223
|
-
|
|
224
|
-
Parentheses override precedence:
|
|
113
|
+
### External Variables Override Script Defaults
|
|
225
114
|
|
|
226
115
|
```typescript
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
```
|
|
116
|
+
// Scripts can define default values
|
|
117
|
+
const formula = "multiplier = 2; value = 100; value * multiplier";
|
|
230
118
|
|
|
231
|
-
|
|
119
|
+
// Without external variables: uses script defaults
|
|
120
|
+
execute(formula); // → 200
|
|
232
121
|
|
|
233
|
-
|
|
122
|
+
// External variables override script assignments
|
|
123
|
+
execute(formula, { variables: { multiplier: 3 } }); // → 300
|
|
124
|
+
execute(formula, { variables: { value: 50 } }); // → 100
|
|
234
125
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
Single-line comments with `//`:
|
|
126
|
+
// Useful for configurable formulas
|
|
127
|
+
const pricing = `
|
|
128
|
+
basePrice = 100;
|
|
129
|
+
taxRate = 0.08;
|
|
130
|
+
discount = 0;
|
|
131
|
+
finalPrice = basePrice * (1 - discount) * (1 + taxRate)
|
|
132
|
+
`;
|
|
244
133
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
134
|
+
execute(pricing); // → 108 (uses all defaults)
|
|
135
|
+
execute(pricing, { variables: { discount: 0.1 } }); // → 97.2 (10% discount)
|
|
136
|
+
execute(pricing, { variables: { basePrice: 200, discount: 0.2 } }); // → 172.8
|
|
248
137
|
```
|
|
249
138
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
The last expression is always returned:
|
|
139
|
+
## Language Reference
|
|
253
140
|
|
|
254
|
-
|
|
255
|
-
execute("x = 5; x + 10"); // → 15
|
|
256
|
-
execute("42"); // → 42
|
|
257
|
-
```
|
|
141
|
+
For complete language documentation including all operators, functions, and examples, see [LANGUAGE.md](./LANGUAGE.md).
|
|
258
142
|
|
|
259
|
-
## API
|
|
143
|
+
## API
|
|
260
144
|
|
|
261
145
|
### Main Functions
|
|
262
146
|
|
|
263
147
|
#### `execute(source: string, context?: ExecutionContext): number`
|
|
264
148
|
|
|
265
|
-
Execute
|
|
149
|
+
Execute an expression and return the result.
|
|
266
150
|
|
|
267
151
|
```typescript
|
|
268
|
-
execute("2 + 2");
|
|
269
|
-
execute("abs(-5)", { functions: { abs: Math.abs } });
|
|
152
|
+
execute("2 + 2"); // → 4
|
|
153
|
+
execute("abs(-5)", { functions: { abs: Math.abs } }); // → 5
|
|
270
154
|
```
|
|
271
155
|
|
|
272
156
|
#### `parseSource(source: string): ASTNode`
|
|
273
157
|
|
|
274
|
-
Parse source
|
|
158
|
+
Parse source into an Abstract Syntax Tree without executing.
|
|
275
159
|
|
|
276
160
|
```typescript
|
|
277
161
|
const ast = parseSource("2 + 3 * 4");
|
|
278
|
-
//
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
#### `generate(node: ASTNode): string`
|
|
282
|
-
|
|
283
|
-
Convert an AST node back to source code. Intelligently adds parentheses only when necessary to preserve semantics.
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
|
-
import { generate, ast } from "littlewing";
|
|
287
|
-
|
|
288
|
-
// From AST builders
|
|
289
|
-
const expr = ast.multiply(ast.add(ast.number(2), ast.number(3)), ast.number(4));
|
|
290
|
-
generate(expr); // → "(2 + 3) * 4"
|
|
291
|
-
|
|
292
|
-
// Round-trip: parse → generate → parse
|
|
293
|
-
const code = "2 + 3 * 4";
|
|
294
|
-
const tree = parseSource(code);
|
|
295
|
-
const regenerated = generate(tree); // → "2 + 3 * 4"
|
|
296
|
-
parseSource(regenerated); // Same AST structure
|
|
162
|
+
// Use with Executor class or optimize() function
|
|
297
163
|
```
|
|
298
164
|
|
|
299
165
|
#### `optimize(node: ASTNode): ASTNode`
|
|
300
166
|
|
|
301
|
-
Optimize an AST by
|
|
167
|
+
Optimize an AST by folding constants. Safe for use with external variables.
|
|
302
168
|
|
|
303
169
|
```typescript
|
|
304
|
-
import { optimize, parseSource } from "littlewing";
|
|
305
|
-
|
|
306
|
-
// Parse first, then optimize
|
|
307
170
|
const ast = parseSource("2 + 3 * 4");
|
|
308
|
-
const optimized = optimize(ast);
|
|
309
|
-
// Transforms BinaryOp tree to NumberLiteral(14)
|
|
310
|
-
|
|
311
|
-
// Useful for storing compact ASTs
|
|
312
|
-
const compactAst = optimize(parseSource("1e6 + 2e6"));
|
|
313
|
-
// → NumberLiteral(3000000)
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
### Classes
|
|
317
|
-
|
|
318
|
-
#### `Lexer`
|
|
319
|
-
|
|
320
|
-
Tokenize source code into a token stream.
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
import { Lexer, TokenType } from "littlewing";
|
|
324
|
-
|
|
325
|
-
const lexer = new Lexer("x = 42");
|
|
326
|
-
const tokens = lexer.tokenize();
|
|
327
|
-
// → [Identifier('x'), Equals, Number(42), EOF]
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
#### `Parser`
|
|
331
|
-
|
|
332
|
-
Parse tokens into an AST.
|
|
333
|
-
|
|
334
|
-
```typescript
|
|
335
|
-
import { Parser } from "littlewing";
|
|
336
|
-
|
|
337
|
-
const parser = new Parser(tokens);
|
|
338
|
-
const ast = parser.parse();
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
#### `Executor`
|
|
342
|
-
|
|
343
|
-
Execute an AST with a given context.
|
|
344
|
-
|
|
345
|
-
```typescript
|
|
346
|
-
import { Executor } from "littlewing";
|
|
347
|
-
|
|
348
|
-
const executor = new Executor(context);
|
|
349
|
-
const result = executor.execute(ast);
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
#### `CodeGenerator`
|
|
353
|
-
|
|
354
|
-
Convert AST nodes back to source code. Handles operator precedence and associativity automatically.
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
import { CodeGenerator } from "littlewing";
|
|
358
|
-
|
|
359
|
-
const generator = new CodeGenerator();
|
|
360
|
-
const code = generator.generate(ast);
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### AST Builders
|
|
364
|
-
|
|
365
|
-
The `ast` namespace provides convenient functions for building AST nodes:
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
import { ast } from "littlewing";
|
|
369
|
-
|
|
370
|
-
// Literals and identifiers
|
|
371
|
-
ast.number(42);
|
|
372
|
-
ast.identifier("x");
|
|
373
|
-
|
|
374
|
-
// Arithmetic operators
|
|
375
|
-
ast.add(left, right);
|
|
376
|
-
ast.subtract(left, right);
|
|
377
|
-
ast.multiply(left, right);
|
|
378
|
-
ast.divide(left, right);
|
|
379
|
-
ast.modulo(left, right);
|
|
380
|
-
ast.exponentiate(left, right);
|
|
381
|
-
ast.negate(argument);
|
|
382
|
-
|
|
383
|
-
// Comparison operators
|
|
384
|
-
ast.equals(left, right); // ==
|
|
385
|
-
ast.notEquals(left, right); // !=
|
|
386
|
-
ast.lessThan(left, right); // <
|
|
387
|
-
ast.greaterThan(left, right); // >
|
|
388
|
-
ast.lessEqual(left, right); // <=
|
|
389
|
-
ast.greaterEqual(left, right); // >=
|
|
390
|
-
|
|
391
|
-
// Logical operators
|
|
392
|
-
ast.logicalAnd(left, right); // &&
|
|
393
|
-
ast.logicalOr(left, right); // ||
|
|
394
|
-
|
|
395
|
-
// Control flow
|
|
396
|
-
ast.conditional(condition, consequent, alternate); // ? :
|
|
397
|
-
|
|
398
|
-
// Assignment
|
|
399
|
-
ast.assign("x", value); // =
|
|
400
|
-
ast.nullishAssign("x", value); // ??=
|
|
401
|
-
|
|
402
|
-
// Functions
|
|
403
|
-
ast.functionCall("abs", [ast.number(-5)]);
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Default Context
|
|
407
|
-
|
|
408
|
-
The `defaultContext` provides a comprehensive set of built-in functions:
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
import { defaultContext } from "littlewing";
|
|
412
|
-
|
|
413
|
-
// Math functions
|
|
414
|
-
(abs, ceil, floor, round, sqrt, min, max);
|
|
415
|
-
(sin, cos, tan, log, log10, exp);
|
|
416
|
-
|
|
417
|
-
// Timestamp functions
|
|
418
|
-
now(); // Current timestamp
|
|
419
|
-
timestamp(year, month, day); // Create timestamp from date components
|
|
420
|
-
|
|
421
|
-
// Time conversion (returns milliseconds)
|
|
422
|
-
(milliseconds(n), seconds(n), minutes(n), hours(n), days(n), weeks(n));
|
|
423
|
-
|
|
424
|
-
// Timestamp component extractors
|
|
425
|
-
year(timestamp); // Extract year (e.g., 2024)
|
|
426
|
-
month(timestamp); // Extract month (1-12, 1 = January)
|
|
427
|
-
day(timestamp); // Extract day of month (1-31)
|
|
428
|
-
hour(timestamp); // Extract hour (0-23)
|
|
429
|
-
minute(timestamp); // Extract minute (0-59)
|
|
430
|
-
second(timestamp); // Extract second (0-59)
|
|
431
|
-
weekday(timestamp); // Extract day of week (0-6, 0 = Sunday)
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
## Advanced Features
|
|
435
|
-
|
|
436
|
-
### Advanced Optimization
|
|
437
|
-
|
|
438
|
-
The `optimize()` function implements a **production-grade, O(n) optimization algorithm** that achieves maximum AST compaction through constant propagation and dead code elimination.
|
|
439
|
-
|
|
440
|
-
#### Simple Example
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
import { optimize, parseSource } from "littlewing";
|
|
171
|
+
const optimized = optimize(ast); // → NumberLiteral(14)
|
|
444
172
|
|
|
445
|
-
//
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// Transitive constant propagation
|
|
450
|
-
const ast2 = optimize(parseSource("x = 5; y = x + 10; y * 2"));
|
|
451
|
-
// Result: NumberLiteral(30) - fully evaluated!
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
#### Complex Example
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
import { optimize, parseSource } from "littlewing";
|
|
458
|
-
|
|
459
|
-
const source = `
|
|
460
|
-
principal = 1000;
|
|
461
|
-
rate = 0.05;
|
|
462
|
-
years = 10;
|
|
463
|
-
n = 12;
|
|
464
|
-
base = 1 + (rate / n);
|
|
465
|
-
exponent = n * years;
|
|
466
|
-
result = principal * (base ^ exponent);
|
|
467
|
-
result
|
|
468
|
-
`;
|
|
469
|
-
|
|
470
|
-
const optimized = optimize(parseSource(source));
|
|
471
|
-
// Result: NumberLiteral(1647.0095406619717)
|
|
472
|
-
// Reduced from 8 statements (40+ nodes) to a single literal!
|
|
173
|
+
// Variables are NOT folded (can be overridden by context)
|
|
174
|
+
const ast2 = parseSource("x = 5; x + 10");
|
|
175
|
+
const opt2 = optimize(ast2); // Still has variable reference
|
|
473
176
|
```
|
|
474
177
|
|
|
475
|
-
####
|
|
476
|
-
|
|
477
|
-
The optimizer uses a three-phase algorithm inspired by compiler optimization theory:
|
|
478
|
-
|
|
479
|
-
1. **Program Analysis** (O(n))
|
|
480
|
-
- Builds dependency graph between variables
|
|
481
|
-
- Identifies constants and tainted expressions
|
|
482
|
-
- Performs topological sorting for evaluation order
|
|
483
|
-
|
|
484
|
-
2. **Constant Propagation** (O(n))
|
|
485
|
-
- Evaluates constants in dependency order
|
|
486
|
-
- Propagates values transitively (a = 5; b = a + 10 → b = 15)
|
|
487
|
-
- Replaces variable references with computed values
|
|
488
|
-
|
|
489
|
-
3. **Dead Code Elimination** (O(n))
|
|
490
|
-
- Removes unused assignments
|
|
491
|
-
- Eliminates fully-propagated variables
|
|
492
|
-
- Unwraps single-value programs
|
|
493
|
-
|
|
494
|
-
**Time complexity:** O(n) guaranteed - no iteration, single pass through AST
|
|
495
|
-
|
|
496
|
-
#### What Gets Optimized
|
|
497
|
-
|
|
498
|
-
✅ **Constant folding:** `2 + 3 * 4` → `14`
|
|
499
|
-
✅ **Variable propagation:** `x = 5; x + 10` → `15`
|
|
500
|
-
✅ **Transitive evaluation:** `a = 5; b = a + 10; b * 2` → `30`
|
|
501
|
-
✅ **Chained computations:** Multi-statement programs fully evaluated
|
|
502
|
-
✅ **Dead code elimination:** Unused variables removed
|
|
503
|
-
✅ **Scientific notation:** `1e6 + 2e6` → `3000000`
|
|
504
|
-
|
|
505
|
-
#### What Stays (Correctly)
|
|
506
|
-
|
|
507
|
-
❌ **External variables:** Variables from `ExecutionContext`
|
|
508
|
-
❌ **Function calls:** `sqrt(16)`, `now()` (runtime behavior)
|
|
509
|
-
❌ **Reassigned variables:** `x = 5; x = 10; x` (not constant)
|
|
510
|
-
❌ **Tainted expressions:** Depend on function calls or external values
|
|
511
|
-
|
|
512
|
-
#### When to Use
|
|
513
|
-
|
|
514
|
-
- **Storage:** Compact ASTs for databases (87% size reduction typical)
|
|
515
|
-
- **Performance:** Faster execution, pre-calculate once
|
|
516
|
-
- **Network:** Smaller payload for transmitted ASTs
|
|
517
|
-
- **Caching:** Store optimized expressions for repeated evaluation
|
|
518
|
-
- **Build tools:** Optimize configuration files at compile time
|
|
519
|
-
|
|
520
|
-
### Scientific Notation
|
|
178
|
+
#### `generate(node: ASTNode): string`
|
|
521
179
|
|
|
522
|
-
|
|
180
|
+
Convert AST back to source code.
|
|
523
181
|
|
|
524
182
|
```typescript
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
execute("3e-2"); // → 0.03
|
|
528
|
-
execute("4E+5"); // → 400000
|
|
529
|
-
|
|
530
|
-
// Works with optimization too
|
|
531
|
-
const ast = parseSource("1e6 * 2", { optimize: true });
|
|
532
|
-
// → NumberLiteral(2000000)
|
|
183
|
+
const ast = parseSource("2 + 3 * 4");
|
|
184
|
+
generate(ast); // → "2 + 3 * 4"
|
|
533
185
|
```
|
|
534
186
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
### Calculator
|
|
187
|
+
### ExecutionContext
|
|
538
188
|
|
|
539
189
|
```typescript
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return execute(expression, defaultContext);
|
|
190
|
+
interface ExecutionContext {
|
|
191
|
+
functions?: Record<string, (...args: any[]) => number>;
|
|
192
|
+
variables?: Record<string, number>;
|
|
544
193
|
}
|
|
545
|
-
|
|
546
|
-
calculate("2 + 2 * 3"); // → 8
|
|
547
|
-
calculate("(2 + 2) * 3"); // → 12
|
|
548
|
-
calculate("sqrt(16) + abs(-5)"); // → 9
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### Financial Calculations
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
import { execute } from "littlewing";
|
|
555
|
-
|
|
556
|
-
const context = {
|
|
557
|
-
functions: {},
|
|
558
|
-
variables: {
|
|
559
|
-
principal: 1000,
|
|
560
|
-
rate: 0.05,
|
|
561
|
-
years: 2,
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
const compound = execute("principal * (1 + rate) ^ years", context);
|
|
566
|
-
// → 1102.5
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### Timestamp Arithmetic
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
import { execute, defaultContext } from "littlewing";
|
|
573
|
-
|
|
574
|
-
// Calculate deadline
|
|
575
|
-
const deadline = execute("now() + days(7)", defaultContext);
|
|
576
|
-
const deadlineDate = new Date(deadline); // Convert to Date
|
|
577
|
-
|
|
578
|
-
// Complex time calculations
|
|
579
|
-
const result = execute("now() + weeks(2) + days(3) + hours(4)", defaultContext);
|
|
580
|
-
|
|
581
|
-
// Time until event
|
|
582
|
-
const eventTime = new Date("2025-12-31").getTime();
|
|
583
|
-
const timeUntil = execute("event - now()", {
|
|
584
|
-
...defaultContext,
|
|
585
|
-
variables: { event: eventTime },
|
|
586
|
-
});
|
|
587
|
-
const daysUntil = timeUntil / (1000 * 60 * 60 * 24);
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Custom Functions
|
|
591
|
-
|
|
592
|
-
```typescript
|
|
593
|
-
import { execute } from "littlewing";
|
|
594
|
-
|
|
595
|
-
const context = {
|
|
596
|
-
functions: {
|
|
597
|
-
fahrenheit: (celsius) => (celsius * 9) / 5 + 32,
|
|
598
|
-
kilometers: (miles) => miles * 1.60934,
|
|
599
|
-
factorial: (n) => (n <= 1 ? 1 : n * context.functions.factorial(n - 1)),
|
|
600
|
-
},
|
|
601
|
-
variables: {
|
|
602
|
-
roomTemp: 20,
|
|
603
|
-
},
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
execute("fahrenheit(roomTemp)", context); // → 68
|
|
607
|
-
execute("kilometers(5)", context); // → 8.0467
|
|
608
194
|
```
|
|
609
195
|
|
|
610
|
-
###
|
|
611
|
-
|
|
612
|
-
```typescript
|
|
613
|
-
import { execute } from "littlewing";
|
|
614
|
-
|
|
615
|
-
// Age-based discount system
|
|
616
|
-
const discountScript = `
|
|
617
|
-
age ??= 30;
|
|
618
|
-
isStudent ??= 0;
|
|
619
|
-
isPremium ??= 0;
|
|
196
|
+
### Default Context Functions
|
|
620
197
|
|
|
621
|
-
|
|
622
|
-
age < 18 ? 0.15 :
|
|
623
|
-
age >= 65 ? 0.15 :
|
|
624
|
-
isStudent ? 0.1 : 0;
|
|
198
|
+
The `defaultContext` includes these built-in functions:
|
|
625
199
|
|
|
626
|
-
|
|
627
|
-
`;
|
|
200
|
+
**Math:** `abs`, `ceil`, `floor`, `round`, `sqrt`, `min`, `max`, `sin`, `cos`, `tan`, `log`, `log10`, `exp`
|
|
628
201
|
|
|
629
|
-
|
|
630
|
-
execute(discountScript, { variables: { age: 16 } }); // → 0.15 (under 18)
|
|
631
|
-
execute(discountScript, { variables: { isPremium: 1 } }); // → 0.2 (premium)
|
|
632
|
-
execute(discountScript, { variables: { isStudent: 1 } }); // → 0.1 (student)
|
|
633
|
-
|
|
634
|
-
// Range validation
|
|
635
|
-
const validateAge = "age >= 18 && age <= 120";
|
|
636
|
-
execute(validateAge, { variables: { age: 25 } }); // → 1 (valid)
|
|
637
|
-
execute(validateAge, { variables: { age: 15 } }); // → 0 (too young)
|
|
638
|
-
execute(validateAge, { variables: { age: 150 } }); // → 0 (invalid)
|
|
639
|
-
|
|
640
|
-
// Complex business logic
|
|
641
|
-
const eligibilityScript = `
|
|
642
|
-
age ??= 0;
|
|
643
|
-
income ??= 0;
|
|
644
|
-
creditScore ??= 0;
|
|
645
|
-
|
|
646
|
-
hasGoodCredit = creditScore >= 700;
|
|
647
|
-
hasStableIncome = income >= 30000;
|
|
648
|
-
isAdult = age >= 18;
|
|
649
|
-
|
|
650
|
-
eligible = isAdult && hasGoodCredit && hasStableIncome;
|
|
651
|
-
eligible
|
|
652
|
-
`;
|
|
202
|
+
**Time:** `now`, `timestamp`, `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks`
|
|
653
203
|
|
|
654
|
-
|
|
655
|
-
variables: { age: 25, income: 45000, creditScore: 750 },
|
|
656
|
-
}); // → 1 (eligible)
|
|
657
|
-
```
|
|
204
|
+
**Date components:** `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`
|
|
658
205
|
|
|
659
|
-
|
|
206
|
+
## Use Cases
|
|
660
207
|
|
|
661
|
-
|
|
662
|
-
|
|
208
|
+
- **User-defined formulas** - Let users write safe arithmetic expressions
|
|
209
|
+
- **Business rules** - Express logic without eval() or new Function()
|
|
210
|
+
- **Financial calculators** - Compound interest, loan payments, etc.
|
|
211
|
+
- **Date arithmetic** - Deadlines, scheduling, time calculations
|
|
212
|
+
- **Game mechanics** - Damage formulas, score calculations
|
|
213
|
+
- **Configuration expressions** - Dynamic config values
|
|
214
|
+
- **Data transformations** - Process numeric data streams
|
|
663
215
|
|
|
664
|
-
|
|
665
|
-
// Defaults
|
|
666
|
-
basePrice ??= 100;
|
|
667
|
-
isPeakHour ??= 0;
|
|
668
|
-
isWeekend ??= 0;
|
|
669
|
-
quantity ??= 1;
|
|
670
|
-
isMember ??= 0;
|
|
216
|
+
## Why Littlewing?
|
|
671
217
|
|
|
672
|
-
|
|
673
|
-
surgeMultiplier = isPeakHour ? 1.5 : isWeekend ? 1.2 : 1.0;
|
|
218
|
+
### The Problem
|
|
674
219
|
|
|
675
|
-
|
|
676
|
-
volumeDiscount = quantity >= 10 ? 0.15 :
|
|
677
|
-
quantity >= 5 ? 0.1 :
|
|
678
|
-
quantity >= 3 ? 0.05 : 0;
|
|
220
|
+
Your app needs to evaluate user-provided formulas or dynamic expressions. Using `eval()` is a security risk. Writing a parser is complex. Embedding a full scripting language is overkill.
|
|
679
221
|
|
|
680
|
-
|
|
681
|
-
memberDiscount = isMember ? 0.1 : 0;
|
|
222
|
+
### The Solution
|
|
682
223
|
|
|
683
|
-
|
|
684
|
-
adjustedPrice = basePrice * surgeMultiplier;
|
|
685
|
-
afterVolumeDiscount = adjustedPrice * (1 - volumeDiscount);
|
|
686
|
-
finalPrice = afterVolumeDiscount * (1 - memberDiscount);
|
|
224
|
+
Littlewing provides just enough: arithmetic expressions with variables and functions. It's safe (no code execution), fast (linear time), and tiny (5KB gzipped).
|
|
687
225
|
|
|
688
|
-
|
|
689
|
-
`;
|
|
226
|
+
### What Makes It Different
|
|
690
227
|
|
|
691
|
-
|
|
692
|
-
|
|
228
|
+
1. **Numbers-only by design** - No string concatenation, no type coercion, no confusion
|
|
229
|
+
2. **External variables override** - Scripts have defaults, runtime provides overrides
|
|
230
|
+
3. **Timestamp arithmetic** - Dates are just numbers (milliseconds)
|
|
231
|
+
4. **Zero dependencies** - No bloat, no supply chain risks
|
|
232
|
+
5. **O(n) everything** - Predictable performance at any scale
|
|
693
233
|
|
|
694
|
-
|
|
695
|
-
execute(pricingFormula, {
|
|
696
|
-
variables: { isPeakHour: 1, quantity: 5, isMember: 1 },
|
|
697
|
-
}); // → 607.5
|
|
234
|
+
## Development
|
|
698
235
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}); // → 1020
|
|
703
|
-
```
|
|
236
|
+
```bash
|
|
237
|
+
# Install dependencies
|
|
238
|
+
bun install
|
|
704
239
|
|
|
705
|
-
|
|
240
|
+
# Run tests
|
|
241
|
+
bun test
|
|
706
242
|
|
|
707
|
-
|
|
708
|
-
|
|
243
|
+
# Build
|
|
244
|
+
bun run build
|
|
709
245
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
{ name: "Review PR", due: "now() + hours(2)" },
|
|
713
|
-
{ name: "Deploy", due: "now() + days(1)" },
|
|
714
|
-
{ name: "Meeting", due: "timestamp(2025, 10, 15, 14, 30, 0)" },
|
|
715
|
-
];
|
|
716
|
-
|
|
717
|
-
const dueTimes = tasks.map((task) => ({
|
|
718
|
-
name: task.name,
|
|
719
|
-
dueTimestamp: execute(task.due, defaultContext),
|
|
720
|
-
dueDate: new Date(execute(task.due, defaultContext)),
|
|
721
|
-
}));
|
|
246
|
+
# Develop with watch mode
|
|
247
|
+
bun run dev
|
|
722
248
|
```
|
|
723
249
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
### Algorithms
|
|
727
|
-
|
|
728
|
-
- **Lexer**: O(n) single-pass tokenization
|
|
729
|
-
- **Parser**: Optimal Pratt parsing with O(n) time complexity
|
|
730
|
-
- **Executor**: O(n) tree-walk evaluation with no type checking overhead
|
|
731
|
-
|
|
732
|
-
### Bundle Size
|
|
733
|
-
|
|
734
|
-
- **6.89 KB gzipped** (37.66 KB raw)
|
|
735
|
-
- Zero dependencies
|
|
736
|
-
- Includes production-grade O(n) optimizer
|
|
737
|
-
- Full feature set: arithmetic, comparisons, logical operators, ternary, assignments
|
|
738
|
-
- Fully tree-shakeable
|
|
739
|
-
|
|
740
|
-
### Test Coverage
|
|
741
|
-
|
|
742
|
-
- **247 tests** with **98.61% line coverage**
|
|
743
|
-
- **98.21% function coverage**
|
|
744
|
-
- Comprehensive coverage of all operators and features
|
|
745
|
-
- All edge cases handled
|
|
746
|
-
- Type-safe execution guaranteed
|
|
747
|
-
|
|
748
|
-
## Type Safety
|
|
749
|
-
|
|
750
|
-
- Strict TypeScript mode
|
|
751
|
-
- Zero implicit `any` types
|
|
752
|
-
- Complete type annotations
|
|
753
|
-
- Single `RuntimeValue = number` type
|
|
754
|
-
- No runtime type checking overhead
|
|
755
|
-
|
|
756
|
-
## Error Handling
|
|
757
|
-
|
|
758
|
-
Clear, actionable error messages for:
|
|
759
|
-
|
|
760
|
-
- Undefined variables: `"Undefined variable: x"`
|
|
761
|
-
- Undefined functions: `"Undefined function: abs"`
|
|
762
|
-
- Division by zero: `"Division by zero"`
|
|
763
|
-
- Modulo by zero: `"Modulo by zero"`
|
|
764
|
-
- Syntax errors with position information
|
|
765
|
-
|
|
766
|
-
## Browser Support
|
|
767
|
-
|
|
768
|
-
- ✅ All modern browsers (ES2023+)
|
|
769
|
-
- ✅ No polyfills required
|
|
770
|
-
- ✅ Tree-shakeable for optimal bundle sizes
|
|
771
|
-
- ✅ 100% ESM, no CommonJS
|
|
772
|
-
|
|
773
|
-
## Node.js Support
|
|
774
|
-
|
|
775
|
-
Works with Node.js 18+ via ESM imports.
|
|
776
|
-
|
|
777
|
-
## Philosophy
|
|
778
|
-
|
|
779
|
-
Littlewing embraces a **numbers-only** type system for maximum simplicity and performance:
|
|
780
|
-
|
|
781
|
-
- **Pure arithmetic**: Every operation works on numbers
|
|
782
|
-
- **No type checking overhead**: Operators don't need runtime type discrimination
|
|
783
|
-
- **Timestamps as numbers**: Date arithmetic uses millisecond timestamps
|
|
784
|
-
- **Clean semantics**: No ambiguous operations like `Date + Date`
|
|
785
|
-
- **Flexibility**: Convert to/from JavaScript Dates at the boundaries
|
|
786
|
-
|
|
787
|
-
This design keeps the language minimal while remaining powerful enough for real-world use cases.
|
|
250
|
+
For detailed development docs, see [CLAUDE.md](./CLAUDE.md).
|
|
788
251
|
|
|
789
252
|
## License
|
|
790
253
|
|
|
@@ -792,4 +255,4 @@ MIT
|
|
|
792
255
|
|
|
793
256
|
## Contributing
|
|
794
257
|
|
|
795
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
258
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|