littlewing 2.0.0 → 2.1.1
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/LICENSE +21 -0
- package/README.md +400 -0
- package/dist/index.d.ts +62 -23
- package/dist/index.js +390 -134
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gabriel Vaquer <brielov@icloud.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# littlewing
|
|
2
|
+
|
|
3
|
+
A minimal, high-performance multi-type expression language for JavaScript. Seven types, zero compromise, built for the browser.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { evaluate, defaultContext } from "littlewing";
|
|
7
|
+
|
|
8
|
+
// Arithmetic
|
|
9
|
+
evaluate("2 + 3 * 4"); // → 14
|
|
10
|
+
|
|
11
|
+
// Strings
|
|
12
|
+
evaluate('"hello" + " world"'); // → "hello world"
|
|
13
|
+
|
|
14
|
+
// Variables and conditionals
|
|
15
|
+
evaluate('price = 100; if price > 50 then "expensive" else "cheap"'); // → "expensive"
|
|
16
|
+
|
|
17
|
+
// Date arithmetic
|
|
18
|
+
evaluate("DIFFERENCE_IN_DAYS(TODAY(), DATE(2025, 12, 31))", defaultContext);
|
|
19
|
+
|
|
20
|
+
// Array comprehensions
|
|
21
|
+
evaluate("for x in 1..=5 then x ^ 2"); // → [1, 4, 9, 16, 25]
|
|
22
|
+
|
|
23
|
+
// Reduce with accumulator
|
|
24
|
+
evaluate("for x in [1, 2, 3, 4] into sum = 0 then sum + x"); // → 10
|
|
25
|
+
|
|
26
|
+
// Pipe operator — chain values through functions
|
|
27
|
+
evaluate("-5 |> ABS(?) |> STR(?)", defaultContext); // → "5"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Seven types** — Numbers, strings, booleans, dates (`Temporal.PlainDate`), times (`Temporal.PlainTime`), datetimes (`Temporal.PlainDateTime`), and homogeneous arrays
|
|
33
|
+
- **No implicit coercion** — Explicit type conversion via `STR()`, `NUM()`, etc.
|
|
34
|
+
- **Strict boolean logic** — `!`, `&&`, `||`, and `if` conditions require booleans
|
|
35
|
+
- **Control flow** — `if/then/else` expressions and `for/in/then` comprehensions with optional `when` guard and `into` accumulator
|
|
36
|
+
- **Bracket indexing** — `arr[0]`, `str[-1]`, with chaining (`matrix[0][1]`)
|
|
37
|
+
- **Pipe operator** — `x |> FUN(?) |> OTHER(?, 1)` chains values through function calls
|
|
38
|
+
- **Range expressions** — `1..5` (exclusive), `1..=5` (inclusive)
|
|
39
|
+
- **Deep equality** — `[1, 2] == [1, 2]` → `true`; cross-type `==` → `false`
|
|
40
|
+
- **85 built-in functions** — Math, string, array, date, time, and datetime operations
|
|
41
|
+
- **O(n) performance** — Linear time parsing and execution
|
|
42
|
+
- **Safe evaluation** — Tree-walk interpreter, no code generation
|
|
43
|
+
- **Extensible** — Add custom functions and variables via context
|
|
44
|
+
- **Type-safe** — Full TypeScript support with strict types
|
|
45
|
+
- **Zero runtime dependencies** — Requires global `Temporal` API (native or polyfill)
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install littlewing
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Basic Usage
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { evaluate } from "littlewing";
|
|
59
|
+
|
|
60
|
+
// Arithmetic
|
|
61
|
+
evaluate("2 + 3 * 4"); // → 14
|
|
62
|
+
evaluate("2 ^ 10"); // → 1024
|
|
63
|
+
|
|
64
|
+
// Strings
|
|
65
|
+
evaluate('"hello" + " world"'); // → "hello world"
|
|
66
|
+
|
|
67
|
+
// Booleans (comparisons return boolean, not 1/0)
|
|
68
|
+
evaluate("5 > 3"); // → true
|
|
69
|
+
evaluate("!(5 > 10)"); // → true
|
|
70
|
+
|
|
71
|
+
// Variables
|
|
72
|
+
evaluate("x = 10; y = 20; x + y"); // → 30
|
|
73
|
+
|
|
74
|
+
// Conditionals (condition must be boolean, else is required)
|
|
75
|
+
evaluate('age = 21; if age >= 18 then "adult" else "minor"'); // → "adult"
|
|
76
|
+
|
|
77
|
+
// Arrays and indexing
|
|
78
|
+
evaluate("[10, 20, 30][-1]"); // → 30
|
|
79
|
+
evaluate("[1, 2] + [3, 4]"); // → [1, 2, 3, 4]
|
|
80
|
+
|
|
81
|
+
// Ranges
|
|
82
|
+
evaluate("1..=5"); // → [1, 2, 3, 4, 5]
|
|
83
|
+
|
|
84
|
+
// For comprehensions (map, filter, reduce)
|
|
85
|
+
evaluate("for x in 1..=5 then x * 2"); // → [2, 4, 6, 8, 10]
|
|
86
|
+
evaluate("for x in 1..=10 when x % 2 == 0 then x"); // → [2, 4, 6, 8, 10]
|
|
87
|
+
evaluate("for x in [1, 2, 3] into sum = 0 then sum + x"); // → 6
|
|
88
|
+
|
|
89
|
+
// Pipe operator
|
|
90
|
+
evaluate("-42 |> ABS(?)", defaultContext); // → 42
|
|
91
|
+
evaluate("150 |> CLAMP(?, 0, 100)", defaultContext); // → 100
|
|
92
|
+
evaluate("-3 |> ABS(?) |> STR(?)", defaultContext); // → "3"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### With Built-in Functions
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { evaluate, defaultContext } from "littlewing";
|
|
99
|
+
|
|
100
|
+
// Math
|
|
101
|
+
evaluate("ABS(-42)", defaultContext); // → 42
|
|
102
|
+
evaluate("ROUND(3.7)", defaultContext); // → 4
|
|
103
|
+
|
|
104
|
+
// Type conversion
|
|
105
|
+
evaluate('NUM("42")', defaultContext); // → 42
|
|
106
|
+
evaluate("STR(42)", defaultContext); // → "42"
|
|
107
|
+
|
|
108
|
+
// String functions
|
|
109
|
+
evaluate('STR_UPPER("hello")', defaultContext); // → "HELLO"
|
|
110
|
+
evaluate('STR_SPLIT("a,b,c", ",")', defaultContext); // → ["a", "b", "c"]
|
|
111
|
+
|
|
112
|
+
// Array functions
|
|
113
|
+
evaluate("ARR_SORT([3, 1, 2])", defaultContext); // → [1, 2, 3]
|
|
114
|
+
evaluate("ARR_SUM([10, 20, 30])", defaultContext); // → 60
|
|
115
|
+
evaluate('ARR_JOIN(["a", "b", "c"], "-")', defaultContext); // → "a-b-c"
|
|
116
|
+
|
|
117
|
+
// Date functions
|
|
118
|
+
evaluate("TODAY()", defaultContext); // → Temporal.PlainDate
|
|
119
|
+
evaluate("ADD_DAYS(TODAY(), 7)", defaultContext); // → 7 days from now
|
|
120
|
+
evaluate("IS_WEEKEND(TODAY())", defaultContext); // → true or false
|
|
121
|
+
|
|
122
|
+
// Time functions
|
|
123
|
+
evaluate("TIME(14, 30, 0)", defaultContext); // → Temporal.PlainTime
|
|
124
|
+
evaluate("ADD_HOURS(TIME(10, 0, 0), 3)", defaultContext); // → 13:00:00
|
|
125
|
+
|
|
126
|
+
// DateTime functions
|
|
127
|
+
evaluate("NOW()", defaultContext); // → Temporal.PlainDateTime
|
|
128
|
+
evaluate("TO_DATE(NOW())", defaultContext); // → today's date
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Custom Functions and Variables
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { evaluate, assertNumber, assertString } from "littlewing";
|
|
135
|
+
|
|
136
|
+
const context = {
|
|
137
|
+
functions: {
|
|
138
|
+
FAHRENHEIT: (celsius) => {
|
|
139
|
+
assertNumber(celsius, "FAHRENHEIT");
|
|
140
|
+
return (celsius * 9) / 5 + 32;
|
|
141
|
+
},
|
|
142
|
+
DISCOUNT: (price, percent) => {
|
|
143
|
+
assertNumber(price, "DISCOUNT", "price");
|
|
144
|
+
assertNumber(percent, "DISCOUNT", "percent");
|
|
145
|
+
return price * (1 - percent / 100);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
variables: {
|
|
149
|
+
pi: 3.14159,
|
|
150
|
+
taxRate: 0.08,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
evaluate("FAHRENHEIT(20)", context); // → 68
|
|
155
|
+
evaluate("DISCOUNT(100, 15)", context); // → 85
|
|
156
|
+
evaluate("100 * (1 + taxRate)", context); // → 108
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The assertion helpers (`assertNumber`, `assertString`, `assertBoolean`, `assertArray`, `assertDate`, `assertTime`, `assertDateTime`, `assertDateOrDateTime`, `assertTimeOrDateTime`) are the same ones used by the built-in standard library. They throw `TypeError` with consistent messages on type mismatch.
|
|
160
|
+
|
|
161
|
+
### External Variables Override Script Defaults
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const formula = "multiplier = 2; value = 100; value * multiplier";
|
|
165
|
+
|
|
166
|
+
evaluate(formula); // → 200
|
|
167
|
+
evaluate(formula, { variables: { multiplier: 3 } }); // → 300
|
|
168
|
+
evaluate(formula, { variables: { value: 50 } }); // → 100
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Language Reference
|
|
172
|
+
|
|
173
|
+
For complete language documentation including all operators, control flow, and built-in functions, see [LANGUAGE.md](./LANGUAGE.md).
|
|
174
|
+
|
|
175
|
+
## API
|
|
176
|
+
|
|
177
|
+
### Main Functions
|
|
178
|
+
|
|
179
|
+
#### `evaluate(input: string | ASTNode, context?: ExecutionContext): RuntimeValue`
|
|
180
|
+
|
|
181
|
+
Evaluate an expression or AST and return the result.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
evaluate("2 + 2"); // → 4
|
|
185
|
+
|
|
186
|
+
// Evaluate pre-parsed AST (parse once, evaluate many)
|
|
187
|
+
const ast = parse("price * quantity");
|
|
188
|
+
evaluate(ast, { variables: { price: 10, quantity: 5 } }); // → 50
|
|
189
|
+
evaluate(ast, { variables: { price: 20, quantity: 3 } }); // → 60
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### `evaluateScope(input: string | ASTNode, context?: ExecutionContext): Record<string, RuntimeValue>`
|
|
193
|
+
|
|
194
|
+
Evaluate and return all assigned variables as a record.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
evaluateScope("x = 10; y = x * 2"); // → { x: 10, y: 20 }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `parse(source: string): ASTNode`
|
|
201
|
+
|
|
202
|
+
Parse source into an Abstract Syntax Tree without evaluating.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const ast = parse("2 + 3 * 4");
|
|
206
|
+
evaluate(ast); // → 14
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `generate(node: ASTNode): string`
|
|
210
|
+
|
|
211
|
+
Convert AST back to source code (preserves comments).
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
generate(parse("2 + 3 * 4")); // → "2 + 3 * 4"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `optimize(node: ASTNode, externalVariables?: ReadonlySet<string>): ASTNode`
|
|
218
|
+
|
|
219
|
+
Optimize an AST with constant folding, constant propagation, and dead code elimination.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const ast = parse("2 + 3 * 4");
|
|
223
|
+
optimize(ast); // → NumberLiteral(14)
|
|
224
|
+
|
|
225
|
+
// With external variables: propagates internal constants while preserving external ones
|
|
226
|
+
const ast2 = parse("x = 5; y = 10; x + y");
|
|
227
|
+
optimize(ast2, new Set(["x"])); // Propagates y=10, keeps x as external
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### `extractInputVariables(ast: ASTNode): string[]`
|
|
231
|
+
|
|
232
|
+
Extract variable names assigned to constant values (useful for building UIs with input controls).
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const ast = parse("price = 100; tax = price * 0.08");
|
|
236
|
+
extractInputVariables(ast); // → ["price"]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### AST Visitor Pattern
|
|
240
|
+
|
|
241
|
+
#### `visit<T>(node: ASTNode, visitor: Visitor<T>): T`
|
|
242
|
+
|
|
243
|
+
Exhaustively visit every node in an AST. All 16 node types must be handled.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { visit, parse } from "littlewing";
|
|
247
|
+
|
|
248
|
+
const count = visit(parse("2 + 3"), {
|
|
249
|
+
Program: (n, recurse) => n.statements.reduce((s, stmt) => s + recurse(stmt), 0),
|
|
250
|
+
NumberLiteral: () => 1,
|
|
251
|
+
StringLiteral: () => 1,
|
|
252
|
+
BooleanLiteral: () => 1,
|
|
253
|
+
ArrayLiteral: (n, recurse) => 1 + n.elements.reduce((s, el) => s + recurse(el), 0),
|
|
254
|
+
Identifier: () => 1,
|
|
255
|
+
BinaryOp: (n, recurse) => 1 + recurse(n.left) + recurse(n.right),
|
|
256
|
+
UnaryOp: (n, recurse) => 1 + recurse(n.argument),
|
|
257
|
+
Assignment: (n, recurse) => 1 + recurse(n.value),
|
|
258
|
+
FunctionCall: (n, recurse) => 1 + n.args.reduce((s, arg) => s + recurse(arg), 0),
|
|
259
|
+
IfExpression: (n, recurse) =>
|
|
260
|
+
1 + recurse(n.condition) + recurse(n.consequent) + recurse(n.alternate),
|
|
261
|
+
ForExpression: (n, recurse) =>
|
|
262
|
+
1 + recurse(n.iterable) + (n.guard ? recurse(n.guard) : 0) + recurse(n.body),
|
|
263
|
+
IndexAccess: (n, recurse) => 1 + recurse(n.object) + recurse(n.index),
|
|
264
|
+
RangeExpression: (n, recurse) => 1 + recurse(n.start) + recurse(n.end),
|
|
265
|
+
PipeExpression: (n, recurse) =>
|
|
266
|
+
1 + recurse(n.value) + n.args.reduce((s, arg) => s + recurse(arg), 0),
|
|
267
|
+
Placeholder: () => 1,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `visitPartial<T>(node, visitor, defaultHandler): T`
|
|
272
|
+
|
|
273
|
+
Visit only specific node types with a fallback for unhandled types.
|
|
274
|
+
|
|
275
|
+
### AST Builder Functions
|
|
276
|
+
|
|
277
|
+
The `ast` namespace provides builder functions for constructing AST nodes:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { ast, generate } from "littlewing";
|
|
281
|
+
|
|
282
|
+
generate(ast.add(ast.number(2), ast.number(3))); // → "2 + 3"
|
|
283
|
+
generate(ast.ifExpr(ast.boolean(true), ast.number(1), ast.number(0))); // → "if true then 1 else 0"
|
|
284
|
+
generate(
|
|
285
|
+
ast.forExpr(
|
|
286
|
+
"x",
|
|
287
|
+
ast.identifier("arr"),
|
|
288
|
+
null,
|
|
289
|
+
null,
|
|
290
|
+
ast.multiply(ast.identifier("x"), ast.number(2)),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
// → "for x in arr then x * 2"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Available builders:**
|
|
297
|
+
|
|
298
|
+
- Core: `program()`, `number()`, `string()`, `boolean()`, `array()`, `identifier()`, `binaryOp()`, `unaryOp()`, `functionCall()`, `assign()`, `ifExpr()`, `forExpr()`, `indexAccess()`, `rangeExpr()`, `pipeExpr()`, `placeholder()`
|
|
299
|
+
- Arithmetic: `add()`, `subtract()`, `multiply()`, `divide()`, `modulo()`, `exponentiate()`, `negate()`
|
|
300
|
+
- Comparison: `equals()`, `notEquals()`, `lessThan()`, `greaterThan()`, `lessEqual()`, `greaterEqual()`
|
|
301
|
+
- Logical: `logicalAnd()`, `logicalOr()`, `logicalNot()`
|
|
302
|
+
|
|
303
|
+
### ExecutionContext
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
interface ExecutionContext {
|
|
307
|
+
functions?: Record<string, (...args: RuntimeValue[]) => RuntimeValue>;
|
|
308
|
+
variables?: Record<string, RuntimeValue>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type RuntimeValue =
|
|
312
|
+
| number
|
|
313
|
+
| string
|
|
314
|
+
| boolean
|
|
315
|
+
| Temporal.PlainDate
|
|
316
|
+
| Temporal.PlainTime
|
|
317
|
+
| Temporal.PlainDateTime
|
|
318
|
+
| readonly RuntimeValue[];
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Default Context Functions
|
|
322
|
+
|
|
323
|
+
The `defaultContext` includes **85 built-in functions**:
|
|
324
|
+
|
|
325
|
+
**Type Conversion (3):** `STR`, `NUM`, `TYPE`
|
|
326
|
+
|
|
327
|
+
**Math (14):** `ABS`, `CEIL`, `FLOOR`, `ROUND`, `SQRT`, `MIN`, `MAX`, `CLAMP`, `SIN`, `COS`, `TAN`, `LOG`, `LOG10`, `EXP`
|
|
328
|
+
|
|
329
|
+
**String (12):** `STR_LEN`, `STR_UPPER`, `STR_LOWER`, `STR_TRIM`, `STR_SLICE`, `STR_CONTAINS`, `STR_INDEX_OF`, `STR_SPLIT`, `STR_REPLACE`, `STR_STARTS_WITH`, `STR_ENDS_WITH`, `STR_REPEAT`
|
|
330
|
+
|
|
331
|
+
**Array (12):** `ARR_LEN`, `ARR_PUSH`, `ARR_SLICE`, `ARR_CONTAINS`, `ARR_REVERSE`, `ARR_SORT`, `ARR_UNIQUE`, `ARR_FLAT`, `ARR_JOIN`, `ARR_SUM`, `ARR_MIN`, `ARR_MAX`
|
|
332
|
+
|
|
333
|
+
**Date (24):** `TODAY`, `DATE`, `GET_YEAR`, `GET_MONTH`, `GET_DAY`, `GET_WEEKDAY`, `GET_DAY_OF_YEAR`, `GET_QUARTER`, `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS`, `DIFFERENCE_IN_DAYS`, `DIFFERENCE_IN_WEEKS`, `DIFFERENCE_IN_MONTHS`, `DIFFERENCE_IN_YEARS`, `START_OF_MONTH`, `END_OF_MONTH`, `START_OF_YEAR`, `END_OF_YEAR`, `START_OF_WEEK`, `START_OF_QUARTER`, `IS_SAME_DAY`, `IS_WEEKEND`, `IS_LEAP_YEAR`
|
|
334
|
+
|
|
335
|
+
**Time (13):** `TIME`, `NOW_TIME`, `GET_HOUR`, `GET_MINUTE`, `GET_SECOND`, `GET_MILLISECOND`, `ADD_HOURS`, `ADD_MINUTES`, `ADD_SECONDS`, `DIFFERENCE_IN_HOURS`, `DIFFERENCE_IN_MINUTES`, `DIFFERENCE_IN_SECONDS`, `IS_SAME_TIME`
|
|
336
|
+
|
|
337
|
+
**DateTime (7):** `DATETIME`, `NOW`, `TO_DATE`, `TO_TIME`, `COMBINE`, `START_OF_DAY`, `END_OF_DAY`
|
|
338
|
+
|
|
339
|
+
**Temporal type support:** Date functions accept both `PlainDate` and `PlainDateTime` (preserving type). Time functions accept both `PlainTime` and `PlainDateTime`. Difference functions require both arguments to be the same type.
|
|
340
|
+
|
|
341
|
+
## Performance Optimization
|
|
342
|
+
|
|
343
|
+
### Parse Once, Evaluate Many
|
|
344
|
+
|
|
345
|
+
For expressions executed multiple times, parse once and reuse the AST:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { evaluate, parse } from "littlewing";
|
|
349
|
+
|
|
350
|
+
const formula = parse("price * quantity * (1 - discount)");
|
|
351
|
+
|
|
352
|
+
evaluate(formula, { variables: { price: 10, quantity: 5, discount: 0.1 } }); // → 45
|
|
353
|
+
evaluate(formula, { variables: { price: 20, quantity: 3, discount: 0.15 } }); // → 51
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Use Cases
|
|
357
|
+
|
|
358
|
+
- **User-defined formulas** — Let users write safe expressions
|
|
359
|
+
- **Business rules** — Express logic without `eval()` or `new Function()`
|
|
360
|
+
- **Financial calculators** — Compound interest, loan payments, etc.
|
|
361
|
+
- **Date arithmetic** — Deadlines, scheduling, date calculations
|
|
362
|
+
- **Data transformations** — Map, filter, and reduce arrays
|
|
363
|
+
- **Configuration expressions** — Dynamic config values
|
|
364
|
+
|
|
365
|
+
## Why Littlewing?
|
|
366
|
+
|
|
367
|
+
### The Problem
|
|
368
|
+
|
|
369
|
+
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.
|
|
370
|
+
|
|
371
|
+
### The Solution
|
|
372
|
+
|
|
373
|
+
Littlewing provides just enough: expressions with multiple types, variables, and functions. It's safe (no code execution), fast (linear time), and type-safe (no implicit coercion).
|
|
374
|
+
|
|
375
|
+
### What Makes It Different
|
|
376
|
+
|
|
377
|
+
1. **Multi-type with strict semantics** — Seven types, no implicit coercion, no surprises
|
|
378
|
+
2. **External variables override** — Scripts have defaults, runtime provides overrides
|
|
379
|
+
3. **Full Temporal support** — First-class `PlainDate`, `PlainTime`, and `PlainDateTime`
|
|
380
|
+
4. **Deep equality** — Arrays and dates compare by value
|
|
381
|
+
5. **O(n) everything** — Predictable performance at any scale
|
|
382
|
+
|
|
383
|
+
## Development
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
bun install # Install dependencies
|
|
387
|
+
bun test # Run tests
|
|
388
|
+
bun run build # Build
|
|
389
|
+
bun run --cwd packages/littlewing dev # Watch mode
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
For detailed development docs, see [CLAUDE.md](./CLAUDE.md).
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
MIT
|
|
397
|
+
|
|
398
|
+
## Contributing
|
|
399
|
+
|
|
400
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare namespace exports_ast {
|
|
2
|
-
export { unaryOp, subtract, string, rangeExpr, program, number, notEquals, negate, multiply, modulo, logicalOr, logicalNot, logicalAnd, lessThan, lessEqual, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, indexAccess, ifExpr, identifier, greaterThan, greaterEqual, getNodeName, functionCall, forExpr, exponentiate, equals, divide, boolean, binaryOp, assign, array, add, UnaryOp, StringLiteral, RangeExpression, Program, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
|
|
2
|
+
export { unaryOp, subtract, string, rangeExpr, program, placeholder, pipeExpr, number, notEquals, negate, multiply, modulo, logicalOr, logicalNot, logicalAnd, lessThan, lessEqual, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isPlaceholder, isPipeExpression, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, indexAccess, ifExpr, identifier, greaterThan, greaterEqual, getNodeName, functionCall, forExpr, exponentiate, equals, divide, boolean, binaryOp, assign, array, add, UnaryOp, StringLiteral, RangeExpression, Program, Placeholder, PipeExpression, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Binary operator types
|
|
@@ -30,7 +30,9 @@ declare const enum NodeKind {
|
|
|
30
30
|
ArrayLiteral = 10,
|
|
31
31
|
ForExpression = 11,
|
|
32
32
|
IndexAccess = 12,
|
|
33
|
-
RangeExpression = 13
|
|
33
|
+
RangeExpression = 13,
|
|
34
|
+
PipeExpression = 14,
|
|
35
|
+
Placeholder = 15
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Program node (multiple statements)
|
|
@@ -151,9 +153,25 @@ interface RangeExpression extends ASTNodeBase {
|
|
|
151
153
|
readonly inclusive: boolean;
|
|
152
154
|
}
|
|
153
155
|
/**
|
|
156
|
+
* Pipe expression (value |> FUN(?, arg))
|
|
157
|
+
* Chains a value through a function call where ? marks the insertion point
|
|
158
|
+
*/
|
|
159
|
+
interface PipeExpression extends ASTNodeBase {
|
|
160
|
+
readonly kind: NodeKind.PipeExpression;
|
|
161
|
+
readonly value: ASTNode;
|
|
162
|
+
readonly name: string;
|
|
163
|
+
readonly args: readonly ASTNode[];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Placeholder (?) used inside pipe expression arguments
|
|
167
|
+
*/
|
|
168
|
+
interface Placeholder extends ASTNodeBase {
|
|
169
|
+
readonly kind: NodeKind.Placeholder;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
154
172
|
* AST Node - discriminated union of all node types
|
|
155
173
|
*/
|
|
156
|
-
type ASTNode = Program | NumberLiteral | StringLiteral | BooleanLiteral | ArrayLiteral | Identifier2 | BinaryOp | UnaryOp | FunctionCall | Assignment | IfExpression | ForExpression | IndexAccess | RangeExpression;
|
|
174
|
+
type ASTNode = Program | NumberLiteral | StringLiteral | BooleanLiteral | ArrayLiteral | Identifier2 | BinaryOp | UnaryOp | FunctionCall | Assignment | IfExpression | ForExpression | IndexAccess | RangeExpression | PipeExpression | Placeholder;
|
|
157
175
|
/**
|
|
158
176
|
* Type guard functions for discriminated union narrowing
|
|
159
177
|
*/
|
|
@@ -171,6 +189,8 @@ declare function isIfExpression(node: ASTNode): node is IfExpression;
|
|
|
171
189
|
declare function isForExpression(node: ASTNode): node is ForExpression;
|
|
172
190
|
declare function isIndexAccess(node: ASTNode): node is IndexAccess;
|
|
173
191
|
declare function isRangeExpression(node: ASTNode): node is RangeExpression;
|
|
192
|
+
declare function isPipeExpression(node: ASTNode): node is PipeExpression;
|
|
193
|
+
declare function isPlaceholder(node: ASTNode): node is Placeholder;
|
|
174
194
|
/**
|
|
175
195
|
* Builder functions for creating AST nodes manually
|
|
176
196
|
*/
|
|
@@ -234,6 +254,14 @@ declare function indexAccess(object: ASTNode, index: ASTNode): IndexAccess;
|
|
|
234
254
|
*/
|
|
235
255
|
declare function rangeExpr(start: ASTNode, end: ASTNode, inclusive: boolean): RangeExpression;
|
|
236
256
|
/**
|
|
257
|
+
* Create a pipe expression node (value |> FUN(?, arg))
|
|
258
|
+
*/
|
|
259
|
+
declare function pipeExpr(value: ASTNode, name: string, args: readonly ASTNode[]): PipeExpression;
|
|
260
|
+
/**
|
|
261
|
+
* Create a placeholder node (?) for use in pipe expression arguments
|
|
262
|
+
*/
|
|
263
|
+
declare function placeholder(): Placeholder;
|
|
264
|
+
/**
|
|
237
265
|
* Convenience functions for common operations
|
|
238
266
|
*/
|
|
239
267
|
declare function add(left: ASTNode, right: ASTNode): BinaryOp;
|
|
@@ -737,30 +765,41 @@ declare function assertTimeOrDateTime(v: RuntimeValue, context: string): asserts
|
|
|
737
765
|
*/
|
|
738
766
|
declare function assertArray(v: RuntimeValue, context: string): asserts v is readonly RuntimeValue[];
|
|
739
767
|
/**
|
|
768
|
+
* A single visitor handler: receives a narrowed node and a recurse function.
|
|
769
|
+
*
|
|
770
|
+
* @template N The specific AST node type this handler accepts
|
|
771
|
+
* @template T The return type shared across all handlers
|
|
772
|
+
*/
|
|
773
|
+
type VisitorHandler<
|
|
774
|
+
N,
|
|
775
|
+
T
|
|
776
|
+
> = (node: N, recurse: (n: ASTNode) => T) => T;
|
|
777
|
+
/**
|
|
740
778
|
* Type-safe visitor pattern for AST traversal.
|
|
741
779
|
*
|
|
742
|
-
* A visitor is an object with handler
|
|
743
|
-
* Each handler receives
|
|
744
|
-
*
|
|
745
|
-
* - A recurse function to visit child nodes with the same visitor
|
|
780
|
+
* A visitor is an object with one handler per AST node type.
|
|
781
|
+
* Each handler receives the narrowed node and a `recurse` function
|
|
782
|
+
* for visiting child nodes with the same visitor.
|
|
746
783
|
*
|
|
747
784
|
* @template T The return type of visitor handlers
|
|
748
785
|
*/
|
|
749
786
|
type Visitor<T> = {
|
|
750
|
-
Program:
|
|
751
|
-
NumberLiteral:
|
|
752
|
-
StringLiteral:
|
|
753
|
-
BooleanLiteral:
|
|
754
|
-
ArrayLiteral:
|
|
755
|
-
Identifier2:
|
|
756
|
-
BinaryOp:
|
|
757
|
-
UnaryOp:
|
|
758
|
-
FunctionCall:
|
|
759
|
-
Assignment:
|
|
760
|
-
IfExpression:
|
|
761
|
-
ForExpression:
|
|
762
|
-
IndexAccess:
|
|
763
|
-
RangeExpression:
|
|
787
|
+
Program: VisitorHandler<Program, T>;
|
|
788
|
+
NumberLiteral: VisitorHandler<NumberLiteral, T>;
|
|
789
|
+
StringLiteral: VisitorHandler<StringLiteral, T>;
|
|
790
|
+
BooleanLiteral: VisitorHandler<BooleanLiteral, T>;
|
|
791
|
+
ArrayLiteral: VisitorHandler<ArrayLiteral, T>;
|
|
792
|
+
Identifier2: VisitorHandler<Identifier2, T>;
|
|
793
|
+
BinaryOp: VisitorHandler<BinaryOp, T>;
|
|
794
|
+
UnaryOp: VisitorHandler<UnaryOp, T>;
|
|
795
|
+
FunctionCall: VisitorHandler<FunctionCall, T>;
|
|
796
|
+
Assignment: VisitorHandler<Assignment, T>;
|
|
797
|
+
IfExpression: VisitorHandler<IfExpression, T>;
|
|
798
|
+
ForExpression: VisitorHandler<ForExpression, T>;
|
|
799
|
+
IndexAccess: VisitorHandler<IndexAccess, T>;
|
|
800
|
+
RangeExpression: VisitorHandler<RangeExpression, T>;
|
|
801
|
+
PipeExpression: VisitorHandler<PipeExpression, T>;
|
|
802
|
+
Placeholder: VisitorHandler<Placeholder, T>;
|
|
764
803
|
};
|
|
765
804
|
/**
|
|
766
805
|
* Visit an AST node using a visitor object with type-specific handlers.
|
|
@@ -785,5 +824,5 @@ declare function visit<T>(node: ASTNode, visitor: Visitor<T>): T;
|
|
|
785
824
|
* @param defaultHandler Handler for unhandled node types
|
|
786
825
|
* @returns The result of visiting the node
|
|
787
826
|
*/
|
|
788
|
-
declare function visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T>>, defaultHandler:
|
|
789
|
-
export { visitPartial, visit, typeOf, time, string, parse, optimize, math, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, generate, extractInputVariables, extractAssignedVariables, evaluateScope, evaluate, defaultContext, datetimefull, datetime, core, exports_ast as ast, assertTimeOrDateTime, assertTime, assertString, assertNumber, assertDateTime, assertDateOrDateTime, assertDate, assertBoolean, assertArray, array, Visitor, UnaryOp, StringLiteral, RuntimeValue, RangeExpression, Program, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, ExecutionContext, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
|
|
827
|
+
declare function visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T>>, defaultHandler: VisitorHandler<ASTNode, T>): T;
|
|
828
|
+
export { visitPartial, visit, typeOf, time, string, parse, optimize, math, isUnaryOp, isStringLiteral, isRangeExpression, isProgram, isPlaceholder, isPipeExpression, isNumberLiteral, isIndexAccess, isIfExpression, isIdentifier, isFunctionCall, isForExpression, isBooleanLiteral, isBinaryOp, isAssignment, isArrayLiteral, generate, extractInputVariables, extractAssignedVariables, evaluateScope, evaluate, defaultContext, datetimefull, datetime, core, exports_ast as ast, assertTimeOrDateTime, assertTime, assertString, assertNumber, assertDateTime, assertDateOrDateTime, assertDate, assertBoolean, assertArray, array, VisitorHandler, Visitor, UnaryOp, StringLiteral, RuntimeValue, RangeExpression, Program, Placeholder, PipeExpression, Operator, NumberLiteral, NodeKind, IndexAccess, IfExpression, Identifier2 as Identifier, FunctionCall, ForExpression, ExecutionContext, BooleanLiteral, BinaryOp, Assignment, ArrayLiteral, ASTNodeBase, ASTNode };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
2
6
|
var __export = (target, all) => {
|
|
3
7
|
for (var name in all)
|
|
4
8
|
__defProp(target, name, {
|
|
5
9
|
get: all[name],
|
|
6
10
|
enumerable: true,
|
|
7
11
|
configurable: true,
|
|
8
|
-
set: (
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
9
13
|
});
|
|
10
14
|
};
|
|
11
15
|
|
|
@@ -17,6 +21,8 @@ __export(exports_ast, {
|
|
|
17
21
|
string: () => string,
|
|
18
22
|
rangeExpr: () => rangeExpr,
|
|
19
23
|
program: () => program,
|
|
24
|
+
placeholder: () => placeholder,
|
|
25
|
+
pipeExpr: () => pipeExpr,
|
|
20
26
|
number: () => number,
|
|
21
27
|
notEquals: () => notEquals,
|
|
22
28
|
negate: () => negate,
|
|
@@ -31,6 +37,8 @@ __export(exports_ast, {
|
|
|
31
37
|
isStringLiteral: () => isStringLiteral,
|
|
32
38
|
isRangeExpression: () => isRangeExpression,
|
|
33
39
|
isProgram: () => isProgram,
|
|
40
|
+
isPlaceholder: () => isPlaceholder,
|
|
41
|
+
isPipeExpression: () => isPipeExpression,
|
|
34
42
|
isNumberLiteral: () => isNumberLiteral,
|
|
35
43
|
isIndexAccess: () => isIndexAccess,
|
|
36
44
|
isIfExpression: () => isIfExpression,
|
|
@@ -75,6 +83,8 @@ var NodeKind;
|
|
|
75
83
|
NodeKind2[NodeKind2["ForExpression"] = 11] = "ForExpression";
|
|
76
84
|
NodeKind2[NodeKind2["IndexAccess"] = 12] = "IndexAccess";
|
|
77
85
|
NodeKind2[NodeKind2["RangeExpression"] = 13] = "RangeExpression";
|
|
86
|
+
NodeKind2[NodeKind2["PipeExpression"] = 14] = "PipeExpression";
|
|
87
|
+
NodeKind2[NodeKind2["Placeholder"] = 15] = "Placeholder";
|
|
78
88
|
})(NodeKind ||= {});
|
|
79
89
|
function isProgram(node) {
|
|
80
90
|
return node.kind === 0 /* Program */;
|
|
@@ -118,6 +128,12 @@ function isIndexAccess(node) {
|
|
|
118
128
|
function isRangeExpression(node) {
|
|
119
129
|
return node.kind === 13 /* RangeExpression */;
|
|
120
130
|
}
|
|
131
|
+
function isPipeExpression(node) {
|
|
132
|
+
return node.kind === 14 /* PipeExpression */;
|
|
133
|
+
}
|
|
134
|
+
function isPlaceholder(node) {
|
|
135
|
+
return node.kind === 15 /* Placeholder */;
|
|
136
|
+
}
|
|
121
137
|
function program(statements) {
|
|
122
138
|
return { kind: 0 /* Program */, statements };
|
|
123
139
|
}
|
|
@@ -172,6 +188,12 @@ function indexAccess(object, index) {
|
|
|
172
188
|
function rangeExpr(start, end, inclusive) {
|
|
173
189
|
return { kind: 13 /* RangeExpression */, start, end, inclusive };
|
|
174
190
|
}
|
|
191
|
+
function pipeExpr(value, name, args) {
|
|
192
|
+
return { kind: 14 /* PipeExpression */, value, name, args };
|
|
193
|
+
}
|
|
194
|
+
function placeholder() {
|
|
195
|
+
return { kind: 15 /* Placeholder */ };
|
|
196
|
+
}
|
|
175
197
|
function add(left, right) {
|
|
176
198
|
return binaryOp(left, "+", right);
|
|
177
199
|
}
|
|
@@ -250,20 +272,156 @@ function getNodeName(node) {
|
|
|
250
272
|
return "IndexAccess";
|
|
251
273
|
case 13 /* RangeExpression */:
|
|
252
274
|
return "RangeExpression";
|
|
275
|
+
case 14 /* PipeExpression */:
|
|
276
|
+
return "PipeExpression";
|
|
277
|
+
case 15 /* Placeholder */:
|
|
278
|
+
return "Placeholder";
|
|
253
279
|
default:
|
|
254
280
|
throw new Error(`Unknown node kind: ${node.kind}`);
|
|
255
281
|
}
|
|
256
282
|
}
|
|
257
283
|
|
|
284
|
+
// src/visitor.ts
|
|
285
|
+
function visit(node, visitor) {
|
|
286
|
+
return visitPartial(node, visitor, (node2) => {
|
|
287
|
+
throw new Error(`No handler for node type: ${getNodeName(node2)}`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function visitPartial(node, visitor, defaultHandler) {
|
|
291
|
+
const recurse = (n) => visitPartial(n, visitor, defaultHandler);
|
|
292
|
+
switch (node.kind) {
|
|
293
|
+
case 0 /* Program */:
|
|
294
|
+
return visitor.Program ? visitor.Program(node, recurse) : defaultHandler(node, recurse);
|
|
295
|
+
case 1 /* NumberLiteral */:
|
|
296
|
+
return visitor.NumberLiteral ? visitor.NumberLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
297
|
+
case 8 /* StringLiteral */:
|
|
298
|
+
return visitor.StringLiteral ? visitor.StringLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
299
|
+
case 9 /* BooleanLiteral */:
|
|
300
|
+
return visitor.BooleanLiteral ? visitor.BooleanLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
301
|
+
case 10 /* ArrayLiteral */:
|
|
302
|
+
return visitor.ArrayLiteral ? visitor.ArrayLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
303
|
+
case 2 /* Identifier */:
|
|
304
|
+
return visitor.Identifier ? visitor.Identifier(node, recurse) : defaultHandler(node, recurse);
|
|
305
|
+
case 3 /* BinaryOp */:
|
|
306
|
+
return visitor.BinaryOp ? visitor.BinaryOp(node, recurse) : defaultHandler(node, recurse);
|
|
307
|
+
case 4 /* UnaryOp */:
|
|
308
|
+
return visitor.UnaryOp ? visitor.UnaryOp(node, recurse) : defaultHandler(node, recurse);
|
|
309
|
+
case 5 /* FunctionCall */:
|
|
310
|
+
return visitor.FunctionCall ? visitor.FunctionCall(node, recurse) : defaultHandler(node, recurse);
|
|
311
|
+
case 6 /* Assignment */:
|
|
312
|
+
return visitor.Assignment ? visitor.Assignment(node, recurse) : defaultHandler(node, recurse);
|
|
313
|
+
case 7 /* IfExpression */:
|
|
314
|
+
return visitor.IfExpression ? visitor.IfExpression(node, recurse) : defaultHandler(node, recurse);
|
|
315
|
+
case 11 /* ForExpression */:
|
|
316
|
+
return visitor.ForExpression ? visitor.ForExpression(node, recurse) : defaultHandler(node, recurse);
|
|
317
|
+
case 12 /* IndexAccess */:
|
|
318
|
+
return visitor.IndexAccess ? visitor.IndexAccess(node, recurse) : defaultHandler(node, recurse);
|
|
319
|
+
case 13 /* RangeExpression */:
|
|
320
|
+
return visitor.RangeExpression ? visitor.RangeExpression(node, recurse) : defaultHandler(node, recurse);
|
|
321
|
+
case 14 /* PipeExpression */:
|
|
322
|
+
return visitor.PipeExpression ? visitor.PipeExpression(node, recurse) : defaultHandler(node, recurse);
|
|
323
|
+
case 15 /* Placeholder */:
|
|
324
|
+
return visitor.Placeholder ? visitor.Placeholder(node, recurse) : defaultHandler(node, recurse);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/analyzer.ts
|
|
329
|
+
function extractInputVariables(ast) {
|
|
330
|
+
const inputVars = new Set;
|
|
331
|
+
const statements = isProgram(ast) ? ast.statements : [ast];
|
|
332
|
+
for (const statement of statements) {
|
|
333
|
+
if (isAssignment(statement)) {
|
|
334
|
+
if (!containsVariableReference(statement.value)) {
|
|
335
|
+
inputVars.add(statement.name);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return Array.from(inputVars);
|
|
340
|
+
}
|
|
341
|
+
function extractAssignedVariables(ast) {
|
|
342
|
+
const seen = new Set;
|
|
343
|
+
const names = [];
|
|
344
|
+
visitPartial(ast, {
|
|
345
|
+
Program: (n, recurse) => {
|
|
346
|
+
for (const statement of n.statements) {
|
|
347
|
+
recurse(statement);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
Assignment: (n, recurse) => {
|
|
351
|
+
if (!seen.has(n.name)) {
|
|
352
|
+
seen.add(n.name);
|
|
353
|
+
names.push(n.name);
|
|
354
|
+
}
|
|
355
|
+
recurse(n.value);
|
|
356
|
+
},
|
|
357
|
+
IfExpression: (n, recurse) => {
|
|
358
|
+
recurse(n.condition);
|
|
359
|
+
recurse(n.consequent);
|
|
360
|
+
recurse(n.alternate);
|
|
361
|
+
},
|
|
362
|
+
ForExpression: (n, recurse) => {
|
|
363
|
+
recurse(n.iterable);
|
|
364
|
+
if (n.guard)
|
|
365
|
+
recurse(n.guard);
|
|
366
|
+
if (n.accumulator)
|
|
367
|
+
recurse(n.accumulator.initial);
|
|
368
|
+
recurse(n.body);
|
|
369
|
+
},
|
|
370
|
+
IndexAccess: (n, recurse) => {
|
|
371
|
+
recurse(n.object);
|
|
372
|
+
recurse(n.index);
|
|
373
|
+
},
|
|
374
|
+
RangeExpression: (n, recurse) => {
|
|
375
|
+
recurse(n.start);
|
|
376
|
+
recurse(n.end);
|
|
377
|
+
},
|
|
378
|
+
PipeExpression: (n, recurse) => {
|
|
379
|
+
recurse(n.value);
|
|
380
|
+
for (const arg of n.args) {
|
|
381
|
+
recurse(arg);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}, () => {});
|
|
385
|
+
return names;
|
|
386
|
+
}
|
|
387
|
+
function containsExternalReference(node, boundVars) {
|
|
388
|
+
return visit(node, {
|
|
389
|
+
Program: (n, recurse) => n.statements.some(recurse),
|
|
390
|
+
NumberLiteral: () => false,
|
|
391
|
+
StringLiteral: () => false,
|
|
392
|
+
BooleanLiteral: () => false,
|
|
393
|
+
Identifier: (n) => !boundVars.has(n.name),
|
|
394
|
+
ArrayLiteral: (n, recurse) => n.elements.some(recurse),
|
|
395
|
+
BinaryOp: (n, recurse) => recurse(n.left) || recurse(n.right),
|
|
396
|
+
UnaryOp: (n, recurse) => recurse(n.argument),
|
|
397
|
+
FunctionCall: (n, recurse) => n.args.some(recurse),
|
|
398
|
+
Assignment: (n, recurse) => recurse(n.value),
|
|
399
|
+
IfExpression: (n, recurse) => recurse(n.condition) || recurse(n.consequent) || recurse(n.alternate),
|
|
400
|
+
ForExpression: (n, recurse) => {
|
|
401
|
+
const innerBound = new Set(boundVars);
|
|
402
|
+
innerBound.add(n.variable);
|
|
403
|
+
if (n.accumulator)
|
|
404
|
+
innerBound.add(n.accumulator.name);
|
|
405
|
+
return recurse(n.iterable) || (n.guard ? containsExternalReference(n.guard, innerBound) : false) || (n.accumulator ? recurse(n.accumulator.initial) : false) || containsExternalReference(n.body, innerBound);
|
|
406
|
+
},
|
|
407
|
+
IndexAccess: (n, recurse) => recurse(n.object) || recurse(n.index),
|
|
408
|
+
RangeExpression: (n, recurse) => recurse(n.start) || recurse(n.end),
|
|
409
|
+
PipeExpression: (n, recurse) => recurse(n.value) || n.args.some(recurse),
|
|
410
|
+
Placeholder: () => false
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function containsVariableReference(node) {
|
|
414
|
+
return containsExternalReference(node, new Set);
|
|
415
|
+
}
|
|
258
416
|
// src/lexer.ts
|
|
259
417
|
var KEYWORDS = new Map([
|
|
260
|
-
["if",
|
|
261
|
-
["then",
|
|
262
|
-
["else",
|
|
263
|
-
["for",
|
|
264
|
-
["in",
|
|
265
|
-
["when",
|
|
266
|
-
["into",
|
|
418
|
+
["if", 28 /* If */],
|
|
419
|
+
["then", 29 /* Then */],
|
|
420
|
+
["else", 30 /* Else */],
|
|
421
|
+
["for", 31 /* For */],
|
|
422
|
+
["in", 32 /* In */],
|
|
423
|
+
["when", 33 /* When */],
|
|
424
|
+
["into", 34 /* Into */]
|
|
267
425
|
]);
|
|
268
426
|
function createCursor(source) {
|
|
269
427
|
return {
|
|
@@ -355,7 +513,7 @@ function nextToken(cursor) {
|
|
|
355
513
|
}
|
|
356
514
|
}
|
|
357
515
|
if (cursor.pos >= cursor.len) {
|
|
358
|
-
return [
|
|
516
|
+
return [35 /* Eof */, cursor.pos, cursor.pos];
|
|
359
517
|
}
|
|
360
518
|
const start = cursor.pos;
|
|
361
519
|
const ch = peek(cursor);
|
|
@@ -456,7 +614,14 @@ function nextToken(cursor) {
|
|
|
456
614
|
advance(cursor);
|
|
457
615
|
return [17 /* Or */, start, cursor.pos];
|
|
458
616
|
}
|
|
617
|
+
if (peek(cursor) === 62) {
|
|
618
|
+
advance(cursor);
|
|
619
|
+
return [26 /* Pipe */, start, cursor.pos];
|
|
620
|
+
}
|
|
459
621
|
throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
|
|
622
|
+
case 63:
|
|
623
|
+
advance(cursor);
|
|
624
|
+
return [27 /* Question */, start, cursor.pos];
|
|
460
625
|
default:
|
|
461
626
|
throw new Error(`Unexpected character '${String.fromCharCode(ch)}' at position ${start}`);
|
|
462
627
|
}
|
|
@@ -514,46 +679,6 @@ function lexIdentifier(cursor) {
|
|
|
514
679
|
return [1 /* Identifier */, start, pos];
|
|
515
680
|
}
|
|
516
681
|
|
|
517
|
-
// src/visitor.ts
|
|
518
|
-
function visit(node, visitor) {
|
|
519
|
-
return visitPartial(node, visitor, (node2) => {
|
|
520
|
-
throw new Error(`No handler for node type: ${getNodeName(node2)}`);
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
function visitPartial(node, visitor, defaultHandler) {
|
|
524
|
-
const recurse = (n) => visitPartial(n, visitor, defaultHandler);
|
|
525
|
-
switch (node.kind) {
|
|
526
|
-
case 0 /* Program */:
|
|
527
|
-
return visitor.Program ? visitor.Program(node, recurse) : defaultHandler(node, recurse);
|
|
528
|
-
case 1 /* NumberLiteral */:
|
|
529
|
-
return visitor.NumberLiteral ? visitor.NumberLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
530
|
-
case 8 /* StringLiteral */:
|
|
531
|
-
return visitor.StringLiteral ? visitor.StringLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
532
|
-
case 9 /* BooleanLiteral */:
|
|
533
|
-
return visitor.BooleanLiteral ? visitor.BooleanLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
534
|
-
case 10 /* ArrayLiteral */:
|
|
535
|
-
return visitor.ArrayLiteral ? visitor.ArrayLiteral(node, recurse) : defaultHandler(node, recurse);
|
|
536
|
-
case 2 /* Identifier */:
|
|
537
|
-
return visitor.Identifier ? visitor.Identifier(node, recurse) : defaultHandler(node, recurse);
|
|
538
|
-
case 3 /* BinaryOp */:
|
|
539
|
-
return visitor.BinaryOp ? visitor.BinaryOp(node, recurse) : defaultHandler(node, recurse);
|
|
540
|
-
case 4 /* UnaryOp */:
|
|
541
|
-
return visitor.UnaryOp ? visitor.UnaryOp(node, recurse) : defaultHandler(node, recurse);
|
|
542
|
-
case 5 /* FunctionCall */:
|
|
543
|
-
return visitor.FunctionCall ? visitor.FunctionCall(node, recurse) : defaultHandler(node, recurse);
|
|
544
|
-
case 6 /* Assignment */:
|
|
545
|
-
return visitor.Assignment ? visitor.Assignment(node, recurse) : defaultHandler(node, recurse);
|
|
546
|
-
case 7 /* IfExpression */:
|
|
547
|
-
return visitor.IfExpression ? visitor.IfExpression(node, recurse) : defaultHandler(node, recurse);
|
|
548
|
-
case 11 /* ForExpression */:
|
|
549
|
-
return visitor.ForExpression ? visitor.ForExpression(node, recurse) : defaultHandler(node, recurse);
|
|
550
|
-
case 12 /* IndexAccess */:
|
|
551
|
-
return visitor.IndexAccess ? visitor.IndexAccess(node, recurse) : defaultHandler(node, recurse);
|
|
552
|
-
case 13 /* RangeExpression */:
|
|
553
|
-
return visitor.RangeExpression ? visitor.RangeExpression(node, recurse) : defaultHandler(node, recurse);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
682
|
// src/utils.ts
|
|
558
683
|
function typeOf(value) {
|
|
559
684
|
if (typeof value === "number")
|
|
@@ -767,14 +892,20 @@ function resolveIndex(target, index) {
|
|
|
767
892
|
if (!Number.isInteger(index)) {
|
|
768
893
|
throw new TypeError(`Index must be an integer, got ${index}`);
|
|
769
894
|
}
|
|
895
|
+
if (typeof target === "string") {
|
|
896
|
+
const codePoints = Array.from(target);
|
|
897
|
+
const len2 = codePoints.length;
|
|
898
|
+
const resolved2 = index < 0 ? len2 + index : index;
|
|
899
|
+
if (resolved2 < 0 || resolved2 >= len2) {
|
|
900
|
+
throw new RangeError(`Index ${index} out of bounds for length ${len2}`);
|
|
901
|
+
}
|
|
902
|
+
return codePoints[resolved2];
|
|
903
|
+
}
|
|
770
904
|
const len = target.length;
|
|
771
905
|
const resolved = index < 0 ? len + index : index;
|
|
772
906
|
if (resolved < 0 || resolved >= len) {
|
|
773
907
|
throw new RangeError(`Index ${index} out of bounds for length ${len}`);
|
|
774
908
|
}
|
|
775
|
-
if (typeof target === "string") {
|
|
776
|
-
return target[resolved];
|
|
777
|
-
}
|
|
778
909
|
return target[resolved];
|
|
779
910
|
}
|
|
780
911
|
function buildRange(start, end, inclusive) {
|
|
@@ -874,7 +1005,14 @@ function collectAllIdentifiers(node) {
|
|
|
874
1005
|
RangeExpression: (n, recurse) => {
|
|
875
1006
|
recurse(n.start);
|
|
876
1007
|
recurse(n.end);
|
|
877
|
-
}
|
|
1008
|
+
},
|
|
1009
|
+
PipeExpression: (n, recurse) => {
|
|
1010
|
+
recurse(n.value);
|
|
1011
|
+
for (const arg of n.args) {
|
|
1012
|
+
recurse(arg);
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
Placeholder: () => {}
|
|
878
1016
|
});
|
|
879
1017
|
return identifiers;
|
|
880
1018
|
}
|
|
@@ -882,6 +1020,8 @@ function getTokenPrecedence(kind) {
|
|
|
882
1020
|
switch (kind) {
|
|
883
1021
|
case 24 /* Eq */:
|
|
884
1022
|
return 1;
|
|
1023
|
+
case 26 /* Pipe */:
|
|
1024
|
+
return 2;
|
|
885
1025
|
case 17 /* Or */:
|
|
886
1026
|
return 3;
|
|
887
1027
|
case 16 /* And */:
|
|
@@ -910,62 +1050,6 @@ function getTokenPrecedence(kind) {
|
|
|
910
1050
|
}
|
|
911
1051
|
}
|
|
912
1052
|
|
|
913
|
-
// src/analyzer.ts
|
|
914
|
-
function extractInputVariables(ast) {
|
|
915
|
-
const inputVars = new Set;
|
|
916
|
-
const statements = isProgram(ast) ? ast.statements : [ast];
|
|
917
|
-
for (const statement of statements) {
|
|
918
|
-
if (isAssignment(statement)) {
|
|
919
|
-
if (!containsVariableReference(statement.value)) {
|
|
920
|
-
inputVars.add(statement.name);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return Array.from(inputVars);
|
|
925
|
-
}
|
|
926
|
-
function extractAssignedVariables(ast) {
|
|
927
|
-
const seen = new Set;
|
|
928
|
-
const names = [];
|
|
929
|
-
visitPartial(ast, {
|
|
930
|
-
Program: (n, recurse) => {
|
|
931
|
-
for (const statement of n.statements) {
|
|
932
|
-
recurse(statement);
|
|
933
|
-
}
|
|
934
|
-
},
|
|
935
|
-
Assignment: (n, recurse) => {
|
|
936
|
-
if (!seen.has(n.name)) {
|
|
937
|
-
seen.add(n.name);
|
|
938
|
-
names.push(n.name);
|
|
939
|
-
}
|
|
940
|
-
recurse(n.value);
|
|
941
|
-
},
|
|
942
|
-
IfExpression: (n, recurse) => {
|
|
943
|
-
recurse(n.condition);
|
|
944
|
-
recurse(n.consequent);
|
|
945
|
-
recurse(n.alternate);
|
|
946
|
-
},
|
|
947
|
-
ForExpression: (n, recurse) => {
|
|
948
|
-
recurse(n.iterable);
|
|
949
|
-
if (n.guard)
|
|
950
|
-
recurse(n.guard);
|
|
951
|
-
if (n.accumulator)
|
|
952
|
-
recurse(n.accumulator.initial);
|
|
953
|
-
recurse(n.body);
|
|
954
|
-
},
|
|
955
|
-
IndexAccess: (n, recurse) => {
|
|
956
|
-
recurse(n.object);
|
|
957
|
-
recurse(n.index);
|
|
958
|
-
},
|
|
959
|
-
RangeExpression: (n, recurse) => {
|
|
960
|
-
recurse(n.start);
|
|
961
|
-
recurse(n.end);
|
|
962
|
-
}
|
|
963
|
-
}, () => {});
|
|
964
|
-
return names;
|
|
965
|
-
}
|
|
966
|
-
function containsVariableReference(node) {
|
|
967
|
-
return collectAllIdentifiers(node).size > 0;
|
|
968
|
-
}
|
|
969
1053
|
// src/codegen.ts
|
|
970
1054
|
function needsParens(node, operator, isLeft) {
|
|
971
1055
|
if (!isBinaryOp(node))
|
|
@@ -1049,15 +1133,16 @@ function generateNode(node) {
|
|
|
1049
1133
|
BinaryOp: (n, recurse) => {
|
|
1050
1134
|
const left = recurse(n.left);
|
|
1051
1135
|
const right = recurse(n.right);
|
|
1052
|
-
const
|
|
1136
|
+
const opPrec = getOperatorPrecedence(n.operator);
|
|
1137
|
+
const leftNeedsParens = needsParens(n.left, n.operator, true) || n.operator === "^" && isUnaryOp(n.left) || isIfExpression(n.left) || isForExpression(n.left) || isAssignment(n.left) || isRangeExpression(n.left) && opPrec >= 7;
|
|
1053
1138
|
const leftCode = leftNeedsParens ? `(${left})` : left;
|
|
1054
|
-
const rightNeedsParens = needsParens(n.right, n.operator, false);
|
|
1139
|
+
const rightNeedsParens = needsParens(n.right, n.operator, false) || isPipeExpression(n.right) || isAssignment(n.right) || isRangeExpression(n.right) && opPrec >= 6;
|
|
1055
1140
|
const rightCode = rightNeedsParens ? `(${right})` : right;
|
|
1056
1141
|
return `${leftCode} ${n.operator} ${rightCode}`;
|
|
1057
1142
|
},
|
|
1058
1143
|
UnaryOp: (n, recurse) => {
|
|
1059
1144
|
const arg = recurse(n.argument);
|
|
1060
|
-
const parensNeeded = isBinaryOp(n.argument) || isAssignment(n.argument);
|
|
1145
|
+
const parensNeeded = isBinaryOp(n.argument) || isAssignment(n.argument) || isPipeExpression(n.argument) || isRangeExpression(n.argument);
|
|
1061
1146
|
const argCode = parensNeeded ? `(${arg})` : arg;
|
|
1062
1147
|
return `${n.operator}${argCode}`;
|
|
1063
1148
|
},
|
|
@@ -1089,7 +1174,7 @@ function generateNode(node) {
|
|
|
1089
1174
|
IndexAccess: (n, recurse) => {
|
|
1090
1175
|
const object = recurse(n.object);
|
|
1091
1176
|
const index = recurse(n.index);
|
|
1092
|
-
const needsParens2 = isBinaryOp(n.object) || isUnaryOp(n.object) || isAssignment(n.object) || isRangeExpression(n.object);
|
|
1177
|
+
const needsParens2 = isBinaryOp(n.object) || isUnaryOp(n.object) || isAssignment(n.object) || isRangeExpression(n.object) || isPipeExpression(n.object);
|
|
1093
1178
|
const objectCode = needsParens2 ? `(${object})` : object;
|
|
1094
1179
|
return `${objectCode}[${index}]`;
|
|
1095
1180
|
},
|
|
@@ -1097,12 +1182,20 @@ function generateNode(node) {
|
|
|
1097
1182
|
const start = recurse(n.start);
|
|
1098
1183
|
const end = recurse(n.end);
|
|
1099
1184
|
const op = n.inclusive ? "..=" : "..";
|
|
1100
|
-
const startNeedsParens = isBinaryOp(n.start) || isRangeExpression(n.start);
|
|
1101
|
-
const endNeedsParens = isBinaryOp(n.end) || isRangeExpression(n.end);
|
|
1185
|
+
const startNeedsParens = isBinaryOp(n.start) || isRangeExpression(n.start) || isIfExpression(n.start) || isForExpression(n.start) || isAssignment(n.start);
|
|
1186
|
+
const endNeedsParens = isBinaryOp(n.end) || isRangeExpression(n.end) || isPipeExpression(n.end) || isAssignment(n.end);
|
|
1102
1187
|
const startCode = startNeedsParens ? `(${start})` : start;
|
|
1103
1188
|
const endCode = endNeedsParens ? `(${end})` : end;
|
|
1104
1189
|
return `${startCode}${op}${endCode}`;
|
|
1105
|
-
}
|
|
1190
|
+
},
|
|
1191
|
+
PipeExpression: (n, recurse) => {
|
|
1192
|
+
const value = recurse(n.value);
|
|
1193
|
+
const argsCode = n.args.map(recurse).join(", ");
|
|
1194
|
+
const valueNeedsParens = isAssignment(n.value) || isIfExpression(n.value) || isForExpression(n.value);
|
|
1195
|
+
const valueCode = valueNeedsParens ? `(${value})` : value;
|
|
1196
|
+
return `${valueCode} |> ${n.name}(${argsCode})`;
|
|
1197
|
+
},
|
|
1198
|
+
Placeholder: () => "?"
|
|
1106
1199
|
});
|
|
1107
1200
|
}
|
|
1108
1201
|
// src/parser.ts
|
|
@@ -1132,7 +1225,7 @@ function parse(source) {
|
|
|
1132
1225
|
};
|
|
1133
1226
|
const statements = [];
|
|
1134
1227
|
const statementOffsets = [];
|
|
1135
|
-
while (state.currentToken[0] !==
|
|
1228
|
+
while (state.currentToken[0] !== 35 /* Eof */) {
|
|
1136
1229
|
statementOffsets.push(state.currentToken[1]);
|
|
1137
1230
|
statements.push(parseExpression(state, 0));
|
|
1138
1231
|
}
|
|
@@ -1305,40 +1398,40 @@ function parsePrefix(state) {
|
|
|
1305
1398
|
advance2(state);
|
|
1306
1399
|
return array(elements);
|
|
1307
1400
|
}
|
|
1308
|
-
if (tokenKind ===
|
|
1401
|
+
if (tokenKind === 28 /* If */) {
|
|
1309
1402
|
advance2(state);
|
|
1310
1403
|
const condition = parseExpression(state, 0);
|
|
1311
|
-
if (peekKind(state) !==
|
|
1404
|
+
if (peekKind(state) !== 29 /* Then */) {
|
|
1312
1405
|
throw new Error('Expected "then" in if expression');
|
|
1313
1406
|
}
|
|
1314
1407
|
advance2(state);
|
|
1315
1408
|
const consequent = parseExpression(state, 0);
|
|
1316
|
-
if (peekKind(state) !==
|
|
1409
|
+
if (peekKind(state) !== 30 /* Else */) {
|
|
1317
1410
|
throw new Error('Expected "else" in if expression');
|
|
1318
1411
|
}
|
|
1319
1412
|
advance2(state);
|
|
1320
1413
|
const alternate = parseExpression(state, 0);
|
|
1321
1414
|
return ifExpr(condition, consequent, alternate);
|
|
1322
1415
|
}
|
|
1323
|
-
if (tokenKind ===
|
|
1416
|
+
if (tokenKind === 31 /* For */) {
|
|
1324
1417
|
advance2(state);
|
|
1325
1418
|
if (peekKind(state) !== 1 /* Identifier */) {
|
|
1326
1419
|
throw new Error('Expected identifier after "for"');
|
|
1327
1420
|
}
|
|
1328
1421
|
const variableName = readText(state.cursor, state.currentToken);
|
|
1329
1422
|
advance2(state);
|
|
1330
|
-
if (peekKind(state) !==
|
|
1423
|
+
if (peekKind(state) !== 32 /* In */) {
|
|
1331
1424
|
throw new Error('Expected "in" in for expression');
|
|
1332
1425
|
}
|
|
1333
1426
|
advance2(state);
|
|
1334
1427
|
const iterable = parseExpression(state, 0);
|
|
1335
1428
|
let guard = null;
|
|
1336
|
-
if (peekKind(state) ===
|
|
1429
|
+
if (peekKind(state) === 33 /* When */) {
|
|
1337
1430
|
advance2(state);
|
|
1338
1431
|
guard = parseExpression(state, 0);
|
|
1339
1432
|
}
|
|
1340
1433
|
let accumulator = null;
|
|
1341
|
-
if (peekKind(state) ===
|
|
1434
|
+
if (peekKind(state) === 34 /* Into */) {
|
|
1342
1435
|
advance2(state);
|
|
1343
1436
|
if (peekKind(state) !== 1 /* Identifier */) {
|
|
1344
1437
|
throw new Error('Expected identifier after "into"');
|
|
@@ -1352,7 +1445,7 @@ function parsePrefix(state) {
|
|
|
1352
1445
|
const initial = parseExpression(state, 0);
|
|
1353
1446
|
accumulator = { name: accName, initial };
|
|
1354
1447
|
}
|
|
1355
|
-
if (peekKind(state) !==
|
|
1448
|
+
if (peekKind(state) !== 29 /* Then */) {
|
|
1356
1449
|
throw new Error('Expected "then" in for expression');
|
|
1357
1450
|
}
|
|
1358
1451
|
advance2(state);
|
|
@@ -1413,6 +1506,34 @@ function parseInfix(state, left, precedence) {
|
|
|
1413
1506
|
const right = parseExpression(state, precedence + 1);
|
|
1414
1507
|
return rangeExpr(left, right, inclusive);
|
|
1415
1508
|
}
|
|
1509
|
+
if (tokenKind === 26 /* Pipe */) {
|
|
1510
|
+
advance2(state);
|
|
1511
|
+
if (peekKind(state) !== 1 /* Identifier */) {
|
|
1512
|
+
throw new Error("Expected function name after |>");
|
|
1513
|
+
}
|
|
1514
|
+
const name = readText(state.cursor, state.currentToken);
|
|
1515
|
+
advance2(state);
|
|
1516
|
+
if (peekKind(state) !== 20 /* LParen */) {
|
|
1517
|
+
throw new Error("Expected ( after function name in pipe expression");
|
|
1518
|
+
}
|
|
1519
|
+
advance2(state);
|
|
1520
|
+
const args = parsePipeArguments(state);
|
|
1521
|
+
if (peekKind(state) !== 21 /* RParen */) {
|
|
1522
|
+
throw new Error("Expected closing parenthesis");
|
|
1523
|
+
}
|
|
1524
|
+
advance2(state);
|
|
1525
|
+
let hasPlaceholder = false;
|
|
1526
|
+
for (const arg of args) {
|
|
1527
|
+
if (arg.kind === 15 /* Placeholder */) {
|
|
1528
|
+
hasPlaceholder = true;
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (!hasPlaceholder) {
|
|
1533
|
+
throw new Error("Pipe expression requires at least one ? placeholder");
|
|
1534
|
+
}
|
|
1535
|
+
return pipeExpr(left, name, args);
|
|
1536
|
+
}
|
|
1416
1537
|
if (isBinaryOperator(tokenKind)) {
|
|
1417
1538
|
const operator = readText(state.cursor, state.currentToken);
|
|
1418
1539
|
advance2(state);
|
|
@@ -1434,6 +1555,25 @@ function parseFunctionArguments(state) {
|
|
|
1434
1555
|
}
|
|
1435
1556
|
return args;
|
|
1436
1557
|
}
|
|
1558
|
+
function parsePipeArguments(state) {
|
|
1559
|
+
if (peekKind(state) === 21 /* RParen */) {
|
|
1560
|
+
return [];
|
|
1561
|
+
}
|
|
1562
|
+
const args = [];
|
|
1563
|
+
args.push(parsePipeArg(state));
|
|
1564
|
+
while (peekKind(state) === 25 /* Comma */) {
|
|
1565
|
+
advance2(state);
|
|
1566
|
+
args.push(parsePipeArg(state));
|
|
1567
|
+
}
|
|
1568
|
+
return args;
|
|
1569
|
+
}
|
|
1570
|
+
function parsePipeArg(state) {
|
|
1571
|
+
if (peekKind(state) === 27 /* Question */) {
|
|
1572
|
+
advance2(state);
|
|
1573
|
+
return placeholder();
|
|
1574
|
+
}
|
|
1575
|
+
return parseExpression(state, 0);
|
|
1576
|
+
}
|
|
1437
1577
|
var UNARY_PRECEDENCE = 8;
|
|
1438
1578
|
function isBinaryOperator(kind) {
|
|
1439
1579
|
return BINARY_OPERATOR_TOKENS.has(kind);
|
|
@@ -1550,6 +1690,21 @@ function evaluateNode(node, context, variables, externalVariables) {
|
|
|
1550
1690
|
assertNumber(end, "Range end");
|
|
1551
1691
|
return buildRange(start, end, n.inclusive);
|
|
1552
1692
|
},
|
|
1693
|
+
PipeExpression: (n, recurse) => {
|
|
1694
|
+
const pipedValue = recurse(n.value);
|
|
1695
|
+
const fn = context.functions?.[n.name];
|
|
1696
|
+
if (fn === undefined) {
|
|
1697
|
+
throw new Error(`Undefined function: ${n.name}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof fn !== "function") {
|
|
1700
|
+
throw new Error(`${n.name} is not a function`);
|
|
1701
|
+
}
|
|
1702
|
+
const evaluatedArgs = n.args.map((arg) => arg.kind === 15 /* Placeholder */ ? pipedValue : recurse(arg));
|
|
1703
|
+
return fn(...evaluatedArgs);
|
|
1704
|
+
},
|
|
1705
|
+
Placeholder: () => {
|
|
1706
|
+
throw new Error("Placeholder outside pipe expression");
|
|
1707
|
+
},
|
|
1553
1708
|
ForExpression: (n, recurse) => {
|
|
1554
1709
|
const iterable = recurse(n.iterable);
|
|
1555
1710
|
let items;
|
|
@@ -1653,7 +1808,7 @@ function eliminateDeadCode(program2) {
|
|
|
1653
1808
|
continue;
|
|
1654
1809
|
}
|
|
1655
1810
|
if (isAssignment(stmt)) {
|
|
1656
|
-
if (liveVars.has(stmt.name)) {
|
|
1811
|
+
if (liveVars.has(stmt.name) || mightHaveSideEffects(stmt.value)) {
|
|
1657
1812
|
keptIndices.push(i);
|
|
1658
1813
|
const identifiers = collectAllIdentifiers(stmt.value);
|
|
1659
1814
|
for (const id of identifiers) {
|
|
@@ -1687,14 +1842,37 @@ function eliminateDeadCode(program2) {
|
|
|
1687
1842
|
function isLiteral(node) {
|
|
1688
1843
|
return isNumberLiteral(node) || isStringLiteral(node) || isBooleanLiteral(node);
|
|
1689
1844
|
}
|
|
1845
|
+
function mightHaveSideEffects(node) {
|
|
1846
|
+
return visit(node, {
|
|
1847
|
+
Program: (n, recurse) => n.statements.some(recurse),
|
|
1848
|
+
NumberLiteral: () => false,
|
|
1849
|
+
StringLiteral: () => false,
|
|
1850
|
+
BooleanLiteral: () => false,
|
|
1851
|
+
Identifier: () => false,
|
|
1852
|
+
ArrayLiteral: (n, recurse) => n.elements.some(recurse),
|
|
1853
|
+
BinaryOp: (n, recurse) => recurse(n.left) || recurse(n.right),
|
|
1854
|
+
UnaryOp: (n, recurse) => recurse(n.argument),
|
|
1855
|
+
FunctionCall: () => true,
|
|
1856
|
+
PipeExpression: () => true,
|
|
1857
|
+
Placeholder: () => false,
|
|
1858
|
+
Assignment: () => true,
|
|
1859
|
+
IfExpression: (n, recurse) => recurse(n.condition) || recurse(n.consequent) || recurse(n.alternate),
|
|
1860
|
+
ForExpression: (n, recurse) => recurse(n.iterable) || (n.guard ? recurse(n.guard) : false) || (n.accumulator ? recurse(n.accumulator.initial) : false) || recurse(n.body),
|
|
1861
|
+
IndexAccess: (n, recurse) => recurse(n.object) || recurse(n.index),
|
|
1862
|
+
RangeExpression: (n, recurse) => recurse(n.start) || recurse(n.end)
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1690
1865
|
function propagateConstants(program2, externalVariables) {
|
|
1691
1866
|
const statements = program2.statements;
|
|
1692
1867
|
const forLoopVars = new Set;
|
|
1693
1868
|
for (const stmt of statements) {
|
|
1694
1869
|
collectForLoopVars(stmt, forLoopVars);
|
|
1695
1870
|
}
|
|
1871
|
+
const allAssignmentCounts = new Map;
|
|
1872
|
+
for (const stmt of statements) {
|
|
1873
|
+
countAssignments(stmt, allAssignmentCounts);
|
|
1874
|
+
}
|
|
1696
1875
|
const knownValues = new Map;
|
|
1697
|
-
const reassigned = new Set;
|
|
1698
1876
|
for (const stmt of statements) {
|
|
1699
1877
|
if (!isAssignment(stmt))
|
|
1700
1878
|
continue;
|
|
@@ -1702,11 +1880,9 @@ function propagateConstants(program2, externalVariables) {
|
|
|
1702
1880
|
continue;
|
|
1703
1881
|
if (forLoopVars.has(stmt.name))
|
|
1704
1882
|
continue;
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
reassigned.add(stmt.name);
|
|
1883
|
+
const totalCount = allAssignmentCounts.get(stmt.name) ?? 0;
|
|
1884
|
+
if (totalCount > 1)
|
|
1708
1885
|
continue;
|
|
1709
|
-
}
|
|
1710
1886
|
if (isLiteral(stmt.value)) {
|
|
1711
1887
|
knownValues.set(stmt.name, stmt.value);
|
|
1712
1888
|
}
|
|
@@ -1786,7 +1962,13 @@ function collectReferencedIdentifiers(node, ids) {
|
|
|
1786
1962
|
RangeExpression: (n, recurse) => {
|
|
1787
1963
|
recurse(n.start);
|
|
1788
1964
|
recurse(n.end);
|
|
1789
|
-
}
|
|
1965
|
+
},
|
|
1966
|
+
PipeExpression: (n, recurse) => {
|
|
1967
|
+
recurse(n.value);
|
|
1968
|
+
for (const a of n.args)
|
|
1969
|
+
recurse(a);
|
|
1970
|
+
},
|
|
1971
|
+
Placeholder: () => {}
|
|
1790
1972
|
});
|
|
1791
1973
|
}
|
|
1792
1974
|
function collectForLoopVars(node, vars) {
|
|
@@ -1840,7 +2022,71 @@ function collectForLoopVars(node, vars) {
|
|
|
1840
2022
|
RangeExpression: (n, recurse) => {
|
|
1841
2023
|
recurse(n.start);
|
|
1842
2024
|
recurse(n.end);
|
|
1843
|
-
}
|
|
2025
|
+
},
|
|
2026
|
+
PipeExpression: (n, recurse) => {
|
|
2027
|
+
recurse(n.value);
|
|
2028
|
+
for (const a of n.args)
|
|
2029
|
+
recurse(a);
|
|
2030
|
+
},
|
|
2031
|
+
Placeholder: () => {}
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
function countAssignments(node, counts) {
|
|
2035
|
+
visit(node, {
|
|
2036
|
+
Program: (n, recurse) => {
|
|
2037
|
+
for (const s of n.statements)
|
|
2038
|
+
recurse(s);
|
|
2039
|
+
},
|
|
2040
|
+
NumberLiteral: () => {},
|
|
2041
|
+
StringLiteral: () => {},
|
|
2042
|
+
BooleanLiteral: () => {},
|
|
2043
|
+
ArrayLiteral: (n, recurse) => {
|
|
2044
|
+
for (const e of n.elements)
|
|
2045
|
+
recurse(e);
|
|
2046
|
+
},
|
|
2047
|
+
Identifier: () => {},
|
|
2048
|
+
BinaryOp: (n, recurse) => {
|
|
2049
|
+
recurse(n.left);
|
|
2050
|
+
recurse(n.right);
|
|
2051
|
+
},
|
|
2052
|
+
UnaryOp: (n, recurse) => {
|
|
2053
|
+
recurse(n.argument);
|
|
2054
|
+
},
|
|
2055
|
+
FunctionCall: (n, recurse) => {
|
|
2056
|
+
for (const a of n.args)
|
|
2057
|
+
recurse(a);
|
|
2058
|
+
},
|
|
2059
|
+
Assignment: (n, recurse) => {
|
|
2060
|
+
counts.set(n.name, (counts.get(n.name) ?? 0) + 1);
|
|
2061
|
+
recurse(n.value);
|
|
2062
|
+
},
|
|
2063
|
+
IfExpression: (n, recurse) => {
|
|
2064
|
+
recurse(n.condition);
|
|
2065
|
+
recurse(n.consequent);
|
|
2066
|
+
recurse(n.alternate);
|
|
2067
|
+
},
|
|
2068
|
+
ForExpression: (n, recurse) => {
|
|
2069
|
+
recurse(n.iterable);
|
|
2070
|
+
if (n.guard)
|
|
2071
|
+
recurse(n.guard);
|
|
2072
|
+
if (n.accumulator)
|
|
2073
|
+
recurse(n.accumulator.initial);
|
|
2074
|
+
recurse(n.body);
|
|
2075
|
+
},
|
|
2076
|
+
IndexAccess: (n, recurse) => {
|
|
2077
|
+
recurse(n.object);
|
|
2078
|
+
recurse(n.index);
|
|
2079
|
+
},
|
|
2080
|
+
RangeExpression: (n, recurse) => {
|
|
2081
|
+
recurse(n.start);
|
|
2082
|
+
recurse(n.end);
|
|
2083
|
+
},
|
|
2084
|
+
PipeExpression: (n, recurse) => {
|
|
2085
|
+
recurse(n.value);
|
|
2086
|
+
for (const a of n.args)
|
|
2087
|
+
recurse(a);
|
|
2088
|
+
},
|
|
2089
|
+
Placeholder: () => {}
|
|
1844
2090
|
});
|
|
1845
2091
|
}
|
|
1846
2092
|
function substituteIdentifiers(node, knownValues) {
|
|
@@ -1877,7 +2123,9 @@ function substituteIdentifiers(node, knownValues) {
|
|
|
1877
2123
|
return preserveComments(n, forExpr(n.variable, iterable, guard, accumulator, body));
|
|
1878
2124
|
},
|
|
1879
2125
|
IndexAccess: (n, recurse) => preserveComments(n, indexAccess(recurse(n.object), recurse(n.index))),
|
|
1880
|
-
RangeExpression: (n, recurse) => preserveComments(n, rangeExpr(recurse(n.start), recurse(n.end), n.inclusive))
|
|
2126
|
+
RangeExpression: (n, recurse) => preserveComments(n, rangeExpr(recurse(n.start), recurse(n.end), n.inclusive)),
|
|
2127
|
+
PipeExpression: (n, recurse) => preserveComments(n, pipeExpr(recurse(n.value), n.name, n.args.map(recurse))),
|
|
2128
|
+
Placeholder: (n) => n
|
|
1881
2129
|
});
|
|
1882
2130
|
}
|
|
1883
2131
|
function optimize(node, externalVariables) {
|
|
@@ -2026,6 +2274,12 @@ function fold(node) {
|
|
|
2026
2274
|
}
|
|
2027
2275
|
return preserveComments(n, rangeExpr(start, end, n.inclusive));
|
|
2028
2276
|
},
|
|
2277
|
+
PipeExpression: (n, recurse) => {
|
|
2278
|
+
const value = recurse(n.value);
|
|
2279
|
+
const optimizedArgs = n.args.map(recurse);
|
|
2280
|
+
return preserveComments(n, pipeExpr(value, n.name, optimizedArgs));
|
|
2281
|
+
},
|
|
2282
|
+
Placeholder: (n) => n,
|
|
2029
2283
|
Program: (n, recurse) => {
|
|
2030
2284
|
const optimizedStatements = n.statements.map(recurse);
|
|
2031
2285
|
const result = program(optimizedStatements);
|
|
@@ -2774,6 +3028,8 @@ export {
|
|
|
2774
3028
|
isStringLiteral,
|
|
2775
3029
|
isRangeExpression,
|
|
2776
3030
|
isProgram,
|
|
3031
|
+
isPlaceholder,
|
|
3032
|
+
isPipeExpression,
|
|
2777
3033
|
isNumberLiteral,
|
|
2778
3034
|
isIndexAccess,
|
|
2779
3035
|
isIfExpression,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlewing",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A minimal, high-performance multi-type expression language with lexer, parser, and interpreter. Optimized for browsers with type-safe execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calculator",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"test": "bun test",
|
|
52
52
|
"test:coverage": "bun test --coverage",
|
|
53
53
|
"test:watch": "bun test --watch",
|
|
54
|
+
"prepublishOnly": "cp ../../README.md ../../LICENSE .",
|
|
54
55
|
"release": "bumpp --commit --push --tag"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|