littlewing 0.4.2 → 0.5.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 +210 -15
- package/dist/index.d.ts +120 -6
- package/dist/index.js +294 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@ A minimal, high-performance arithmetic expression language with a complete lexer
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🚀 **Minimal & Fast** - O(n) algorithms throughout (lexer, parser, executor)
|
|
8
|
-
- 📦 **Small Bundle** -
|
|
8
|
+
- 📦 **Small Bundle** - 6.89 KB gzipped, zero dependencies
|
|
9
9
|
- 🌐 **Browser Ready** - 100% ESM, no Node.js APIs
|
|
10
10
|
- 🔒 **Type-Safe** - Strict TypeScript with full type coverage
|
|
11
|
-
- ✅ **Thoroughly Tested** -
|
|
11
|
+
- ✅ **Thoroughly Tested** - 247 tests, 98.61% line coverage
|
|
12
12
|
- 📐 **Pure Arithmetic** - Numbers-only, clean semantics
|
|
13
13
|
- 🎯 **Clean API** - Intuitive dual API (class-based + functional)
|
|
14
14
|
- 📝 **Well Documented** - Complete JSDoc and examples
|
|
@@ -126,7 +126,7 @@ z = x * y;
|
|
|
126
126
|
|
|
127
127
|
### Operators
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
#### Arithmetic Operators
|
|
130
130
|
|
|
131
131
|
```typescript
|
|
132
132
|
2 + 3; // addition
|
|
@@ -134,23 +134,98 @@ All standard arithmetic operators with proper precedence:
|
|
|
134
134
|
3 * 4; // multiplication
|
|
135
135
|
10 / 2; // division
|
|
136
136
|
10 % 3; // modulo
|
|
137
|
-
2 ^
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
2 ^ 3; // exponentiation (power)
|
|
138
|
+
-5; // unary minus
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Comparison Operators
|
|
142
|
+
|
|
143
|
+
Returns `1` for true, `0` for false (following the numbers-only philosophy):
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
5 == 5; // equality → 1
|
|
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
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Logical Operators
|
|
155
|
+
|
|
156
|
+
Returns `1` for true, `0` for false. Treats `0` as false, any non-zero value as true:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
1 && 1; // logical AND → 1 (both truthy)
|
|
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:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
5 > 3 ? 100 : 50; // → 100 (condition is true)
|
|
177
|
+
0 ? 100 : 50; // → 50 (0 is falsy)
|
|
178
|
+
x = age >= 18 ? 1 : 0; // assign based on condition
|
|
179
|
+
|
|
180
|
+
// Nested ternaries
|
|
181
|
+
age < 18 ? 10 : age >= 65 ? 15 : 0; // age-based discount
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Assignment Operators
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
x = 5; // regular assignment
|
|
188
|
+
price ??= 100; // nullish assignment - only assigns if variable doesn't exist
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `??=` operator is useful for providing default values to external variables:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Without ??=
|
|
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
|
+
```
|
|
202
|
+
|
|
203
|
+
Unlike `||`, the `??=` operator preserves `0` values:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
execute("x ??= 10; x", { variables: { x: 0 } }); // → 0 (preserves zero)
|
|
207
|
+
execute("x ??= 10; x", {}); // → 10 (assigns default)
|
|
140
208
|
```
|
|
141
209
|
|
|
142
210
|
### Operator Precedence
|
|
143
211
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
149
223
|
|
|
150
224
|
Parentheses override precedence:
|
|
151
225
|
|
|
152
226
|
```typescript
|
|
153
227
|
(2 + 3) * 4; // → 20 (not 14)
|
|
228
|
+
5 > 3 && 10 > 8; // → 1 (explicit grouping, though not necessary)
|
|
154
229
|
```
|
|
155
230
|
|
|
156
231
|
### Functions
|
|
@@ -292,8 +367,11 @@ The `ast` namespace provides convenient functions for building AST nodes:
|
|
|
292
367
|
```typescript
|
|
293
368
|
import { ast } from "littlewing";
|
|
294
369
|
|
|
370
|
+
// Literals and identifiers
|
|
295
371
|
ast.number(42);
|
|
296
372
|
ast.identifier("x");
|
|
373
|
+
|
|
374
|
+
// Arithmetic operators
|
|
297
375
|
ast.add(left, right);
|
|
298
376
|
ast.subtract(left, right);
|
|
299
377
|
ast.multiply(left, right);
|
|
@@ -301,7 +379,27 @@ ast.divide(left, right);
|
|
|
301
379
|
ast.modulo(left, right);
|
|
302
380
|
ast.exponentiate(left, right);
|
|
303
381
|
ast.negate(argument);
|
|
304
|
-
|
|
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
|
|
305
403
|
ast.functionCall("abs", [ast.number(-5)]);
|
|
306
404
|
```
|
|
307
405
|
|
|
@@ -509,6 +607,101 @@ execute("fahrenheit(roomTemp)", context); // → 68
|
|
|
509
607
|
execute("kilometers(5)", context); // → 8.0467
|
|
510
608
|
```
|
|
511
609
|
|
|
610
|
+
### Conditional Logic & Validation
|
|
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;
|
|
620
|
+
|
|
621
|
+
discount = isPremium ? 0.2 :
|
|
622
|
+
age < 18 ? 0.15 :
|
|
623
|
+
age >= 65 ? 0.15 :
|
|
624
|
+
isStudent ? 0.1 : 0;
|
|
625
|
+
|
|
626
|
+
discount
|
|
627
|
+
`;
|
|
628
|
+
|
|
629
|
+
execute(discountScript); // → 0 (default 30-year-old)
|
|
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
|
+
`;
|
|
653
|
+
|
|
654
|
+
execute(eligibilityScript, {
|
|
655
|
+
variables: { age: 25, income: 45000, creditScore: 750 },
|
|
656
|
+
}); // → 1 (eligible)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Dynamic Pricing
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
import { execute } from "littlewing";
|
|
663
|
+
|
|
664
|
+
const pricingFormula = `
|
|
665
|
+
// Defaults
|
|
666
|
+
basePrice ??= 100;
|
|
667
|
+
isPeakHour ??= 0;
|
|
668
|
+
isWeekend ??= 0;
|
|
669
|
+
quantity ??= 1;
|
|
670
|
+
isMember ??= 0;
|
|
671
|
+
|
|
672
|
+
// Surge pricing
|
|
673
|
+
surgeMultiplier = isPeakHour ? 1.5 : isWeekend ? 1.2 : 1.0;
|
|
674
|
+
|
|
675
|
+
// Volume discount
|
|
676
|
+
volumeDiscount = quantity >= 10 ? 0.15 :
|
|
677
|
+
quantity >= 5 ? 0.1 :
|
|
678
|
+
quantity >= 3 ? 0.05 : 0;
|
|
679
|
+
|
|
680
|
+
// Member discount (stacks with volume)
|
|
681
|
+
memberDiscount = isMember ? 0.1 : 0;
|
|
682
|
+
|
|
683
|
+
// Calculate final price
|
|
684
|
+
adjustedPrice = basePrice * surgeMultiplier;
|
|
685
|
+
afterVolumeDiscount = adjustedPrice * (1 - volumeDiscount);
|
|
686
|
+
finalPrice = afterVolumeDiscount * (1 - memberDiscount);
|
|
687
|
+
|
|
688
|
+
finalPrice * quantity
|
|
689
|
+
`;
|
|
690
|
+
|
|
691
|
+
// Regular customer, 1 item
|
|
692
|
+
execute(pricingFormula); // → 100
|
|
693
|
+
|
|
694
|
+
// Peak hour, 5 items, member
|
|
695
|
+
execute(pricingFormula, {
|
|
696
|
+
variables: { isPeakHour: 1, quantity: 5, isMember: 1 },
|
|
697
|
+
}); // → 607.5
|
|
698
|
+
|
|
699
|
+
// Weekend, bulk order (10 items)
|
|
700
|
+
execute(pricingFormula, {
|
|
701
|
+
variables: { isWeekend: 1, quantity: 10 },
|
|
702
|
+
}); // → 1020
|
|
703
|
+
```
|
|
704
|
+
|
|
512
705
|
### Scheduling System
|
|
513
706
|
|
|
514
707
|
```typescript
|
|
@@ -538,15 +731,17 @@ const dueTimes = tasks.map((task) => ({
|
|
|
538
731
|
|
|
539
732
|
### Bundle Size
|
|
540
733
|
|
|
541
|
-
- **
|
|
734
|
+
- **6.89 KB gzipped** (37.66 KB raw)
|
|
542
735
|
- Zero dependencies
|
|
543
736
|
- Includes production-grade O(n) optimizer
|
|
737
|
+
- Full feature set: arithmetic, comparisons, logical operators, ternary, assignments
|
|
544
738
|
- Fully tree-shakeable
|
|
545
739
|
|
|
546
740
|
### Test Coverage
|
|
547
741
|
|
|
548
|
-
- **
|
|
549
|
-
- **
|
|
742
|
+
- **247 tests** with **98.61% line coverage**
|
|
743
|
+
- **98.21% function coverage**
|
|
744
|
+
- Comprehensive coverage of all operators and features
|
|
550
745
|
- All edge cases handled
|
|
551
746
|
- Type-safe execution guaranteed
|
|
552
747
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare namespace exports_ast {
|
|
2
|
-
export { unaryOp, subtract, number, negate, multiply, modulo, identifier, functionCall, exponentiate, divide, binaryOp, assign, add };
|
|
2
|
+
export { unaryOp, subtract, number, nullishAssign, notEquals, negate, multiply, modulo, logicalOr, logicalAnd, lessThan, lessEqual, identifier, greaterThan, greaterEqual, functionCall, exponentiate, equals, divide, conditional, binaryOp, assign, add };
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Runtime value type - only numbers
|
|
@@ -8,14 +8,14 @@ type RuntimeValue = number;
|
|
|
8
8
|
/**
|
|
9
9
|
* Binary operator types
|
|
10
10
|
*/
|
|
11
|
-
type Operator = "+" | "-" | "*" | "/" | "%" | "^";
|
|
11
|
+
type Operator = "+" | "-" | "*" | "/" | "%" | "^" | "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||";
|
|
12
12
|
/**
|
|
13
13
|
* Execution context providing global functions and variables
|
|
14
|
-
* Functions must accept
|
|
14
|
+
* Functions must accept zero or more number arguments and return a number
|
|
15
15
|
* Variables must be numbers
|
|
16
16
|
*/
|
|
17
17
|
interface ExecutionContext {
|
|
18
|
-
functions?: Record<string, (...args:
|
|
18
|
+
functions?: Record<string, (...args: number[]) => number>;
|
|
19
19
|
variables?: Record<string, number>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -30,10 +30,21 @@ declare enum TokenType {
|
|
|
30
30
|
SLASH = "SLASH",
|
|
31
31
|
PERCENT = "PERCENT",
|
|
32
32
|
CARET = "CARET",
|
|
33
|
+
DOUBLE_EQUALS = "DOUBLE_EQUALS",
|
|
34
|
+
NOT_EQUALS = "NOT_EQUALS",
|
|
35
|
+
LESS_THAN = "LESS_THAN",
|
|
36
|
+
GREATER_THAN = "GREATER_THAN",
|
|
37
|
+
LESS_EQUAL = "LESS_EQUAL",
|
|
38
|
+
GREATER_EQUAL = "GREATER_EQUAL",
|
|
39
|
+
LOGICAL_AND = "LOGICAL_AND",
|
|
40
|
+
LOGICAL_OR = "LOGICAL_OR",
|
|
33
41
|
LPAREN = "LPAREN",
|
|
34
42
|
RPAREN = "RPAREN",
|
|
35
43
|
EQUALS = "EQUALS",
|
|
36
44
|
COMMA = "COMMA",
|
|
45
|
+
QUESTION = "QUESTION",
|
|
46
|
+
COLON = "COLON",
|
|
47
|
+
NULLISH_ASSIGN = "NULLISH_ASSIGN",
|
|
37
48
|
EOF = "EOF"
|
|
38
49
|
}
|
|
39
50
|
/**
|
|
@@ -47,7 +58,7 @@ interface Token {
|
|
|
47
58
|
/**
|
|
48
59
|
* AST Node - base type
|
|
49
60
|
*/
|
|
50
|
-
type ASTNode = Program | NumberLiteral | Identifier | BinaryOp | UnaryOp | FunctionCall | Assignment;
|
|
61
|
+
type ASTNode = Program | NumberLiteral | Identifier | BinaryOp | UnaryOp | FunctionCall | Assignment | ConditionalExpression | NullishAssignment;
|
|
51
62
|
/**
|
|
52
63
|
* Program node (multiple statements)
|
|
53
64
|
*/
|
|
@@ -103,6 +114,26 @@ interface Assignment {
|
|
|
103
114
|
value: ASTNode;
|
|
104
115
|
}
|
|
105
116
|
/**
|
|
117
|
+
* Conditional expression (ternary operator: condition ? consequent : alternate)
|
|
118
|
+
* Returns consequent if condition !== 0, otherwise returns alternate
|
|
119
|
+
*/
|
|
120
|
+
interface ConditionalExpression {
|
|
121
|
+
type: "ConditionalExpression";
|
|
122
|
+
condition: ASTNode;
|
|
123
|
+
consequent: ASTNode;
|
|
124
|
+
alternate: ASTNode;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Nullish assignment (x ??= 5)
|
|
128
|
+
* Assigns value only if variable is undefined (not provided in context)
|
|
129
|
+
* Used for providing defaults for external variables
|
|
130
|
+
*/
|
|
131
|
+
interface NullishAssignment {
|
|
132
|
+
type: "NullishAssignment";
|
|
133
|
+
name: string;
|
|
134
|
+
value: ASTNode;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
106
137
|
* Type guard functions for discriminated union narrowing
|
|
107
138
|
*/
|
|
108
139
|
declare function isNumberLiteral(node: ASTNode): node is NumberLiteral;
|
|
@@ -112,6 +143,8 @@ declare function isUnaryOp(node: ASTNode): node is UnaryOp;
|
|
|
112
143
|
declare function isFunctionCall(node: ASTNode): node is FunctionCall;
|
|
113
144
|
declare function isAssignment(node: ASTNode): node is Assignment;
|
|
114
145
|
declare function isProgram(node: ASTNode): node is Program;
|
|
146
|
+
declare function isConditionalExpression(node: ASTNode): node is ConditionalExpression;
|
|
147
|
+
declare function isNullishAssignment(node: ASTNode): node is NullishAssignment;
|
|
115
148
|
/**
|
|
116
149
|
* Builder functions for creating AST nodes manually
|
|
117
150
|
*/
|
|
@@ -140,6 +173,15 @@ declare function functionCall(name: string, args?: ASTNode[]): FunctionCall;
|
|
|
140
173
|
*/
|
|
141
174
|
declare function assign(name: string, value: ASTNode): Assignment;
|
|
142
175
|
/**
|
|
176
|
+
* Create a nullish assignment node (x ??= 5)
|
|
177
|
+
* Assigns only if variable is undefined
|
|
178
|
+
*/
|
|
179
|
+
declare function nullishAssign(name: string, value: ASTNode): NullishAssignment;
|
|
180
|
+
/**
|
|
181
|
+
* Create a conditional expression node (ternary operator)
|
|
182
|
+
*/
|
|
183
|
+
declare function conditional(condition: ASTNode, consequent: ASTNode, alternate: ASTNode): ConditionalExpression;
|
|
184
|
+
/**
|
|
143
185
|
* Convenience functions for common operations
|
|
144
186
|
*/
|
|
145
187
|
/**
|
|
@@ -171,6 +213,44 @@ declare function exponentiate(left: ASTNode, right: ASTNode): BinaryOp;
|
|
|
171
213
|
*/
|
|
172
214
|
declare function negate(argument: ASTNode): UnaryOp;
|
|
173
215
|
/**
|
|
216
|
+
* Comparison operator convenience functions
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Create an equality comparison (==)
|
|
220
|
+
*/
|
|
221
|
+
declare function equals(left: ASTNode, right: ASTNode): BinaryOp;
|
|
222
|
+
/**
|
|
223
|
+
* Create a not-equals comparison (!=)
|
|
224
|
+
*/
|
|
225
|
+
declare function notEquals(left: ASTNode, right: ASTNode): BinaryOp;
|
|
226
|
+
/**
|
|
227
|
+
* Create a less-than comparison (<)
|
|
228
|
+
*/
|
|
229
|
+
declare function lessThan(left: ASTNode, right: ASTNode): BinaryOp;
|
|
230
|
+
/**
|
|
231
|
+
* Create a greater-than comparison (>)
|
|
232
|
+
*/
|
|
233
|
+
declare function greaterThan(left: ASTNode, right: ASTNode): BinaryOp;
|
|
234
|
+
/**
|
|
235
|
+
* Create a less-than-or-equal comparison (<=)
|
|
236
|
+
*/
|
|
237
|
+
declare function lessEqual(left: ASTNode, right: ASTNode): BinaryOp;
|
|
238
|
+
/**
|
|
239
|
+
* Create a greater-than-or-equal comparison (>=)
|
|
240
|
+
*/
|
|
241
|
+
declare function greaterEqual(left: ASTNode, right: ASTNode): BinaryOp;
|
|
242
|
+
/**
|
|
243
|
+
* Logical operator convenience functions
|
|
244
|
+
*/
|
|
245
|
+
/**
|
|
246
|
+
* Create a logical AND operation (&&)
|
|
247
|
+
*/
|
|
248
|
+
declare function logicalAnd(left: ASTNode, right: ASTNode): BinaryOp;
|
|
249
|
+
/**
|
|
250
|
+
* Create a logical OR operation (||)
|
|
251
|
+
*/
|
|
252
|
+
declare function logicalOr(left: ASTNode, right: ASTNode): BinaryOp;
|
|
253
|
+
/**
|
|
174
254
|
* CodeGenerator - converts AST nodes back to source code
|
|
175
255
|
*/
|
|
176
256
|
declare class CodeGenerator {
|
|
@@ -207,6 +287,14 @@ declare class CodeGenerator {
|
|
|
207
287
|
*/
|
|
208
288
|
private generateAssignment;
|
|
209
289
|
/**
|
|
290
|
+
* Generate code for a nullish assignment
|
|
291
|
+
*/
|
|
292
|
+
private generateNullishAssignment;
|
|
293
|
+
/**
|
|
294
|
+
* Generate code for a conditional expression (ternary operator)
|
|
295
|
+
*/
|
|
296
|
+
private generateConditionalExpression;
|
|
297
|
+
/**
|
|
210
298
|
* Check if left operand needs parentheses based on operator precedence
|
|
211
299
|
* - For left-associative operators: parens only if strictly lower precedence
|
|
212
300
|
* - For right-associative operators (^): parens if lower or equal precedence
|
|
@@ -220,6 +308,7 @@ declare class CodeGenerator {
|
|
|
220
308
|
private needsParensRight;
|
|
221
309
|
/**
|
|
222
310
|
* Get precedence of an operator (higher number = higher precedence)
|
|
311
|
+
* Must match the precedence in parser.ts
|
|
223
312
|
*/
|
|
224
313
|
private getPrecedence;
|
|
225
314
|
}
|
|
@@ -290,6 +379,17 @@ declare class Executor {
|
|
|
290
379
|
* Execute a variable assignment
|
|
291
380
|
*/
|
|
292
381
|
private executeAssignment;
|
|
382
|
+
/**
|
|
383
|
+
* Execute a nullish assignment (??=)
|
|
384
|
+
* Only assigns if the variable is undefined (not in variables map)
|
|
385
|
+
* Returns the existing value if defined, otherwise evaluates and assigns the value
|
|
386
|
+
*/
|
|
387
|
+
private executeNullishAssignment;
|
|
388
|
+
/**
|
|
389
|
+
* Execute a conditional expression (ternary operator)
|
|
390
|
+
* Returns consequent if condition !== 0, otherwise returns alternate
|
|
391
|
+
*/
|
|
392
|
+
private executeConditionalExpression;
|
|
293
393
|
}
|
|
294
394
|
/**
|
|
295
395
|
* Execute source code with given context
|
|
@@ -332,6 +432,10 @@ declare class Lexer {
|
|
|
332
432
|
*/
|
|
333
433
|
private peek;
|
|
334
434
|
/**
|
|
435
|
+
* Peek ahead n positions without consuming
|
|
436
|
+
*/
|
|
437
|
+
private peekAhead;
|
|
438
|
+
/**
|
|
335
439
|
* Check if character is a digit
|
|
336
440
|
*/
|
|
337
441
|
private isDigit;
|
|
@@ -395,6 +499,16 @@ declare class Parser {
|
|
|
395
499
|
private parseFunctionArguments;
|
|
396
500
|
/**
|
|
397
501
|
* Get operator precedence
|
|
502
|
+
* Precedence hierarchy:
|
|
503
|
+
* 0: None
|
|
504
|
+
* 1: Assignment (=, ??=)
|
|
505
|
+
* 2: Ternary conditional (? :)
|
|
506
|
+
* 3: Logical OR (||)
|
|
507
|
+
* 4: Logical AND (&&)
|
|
508
|
+
* 5: Comparison (==, !=, <, >, <=, >=)
|
|
509
|
+
* 6: Addition/Subtraction (+, -)
|
|
510
|
+
* 7: Multiplication/Division/Modulo (*, /, %)
|
|
511
|
+
* 8: Exponentiation (^)
|
|
398
512
|
*/
|
|
399
513
|
private getPrecedence;
|
|
400
514
|
/**
|
|
@@ -421,4 +535,4 @@ declare class Parser {
|
|
|
421
535
|
* @returns Parsed AST
|
|
422
536
|
*/
|
|
423
537
|
declare function parseSource(source: string): ASTNode;
|
|
424
|
-
export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isBinaryOp, isAssignment, generate, execute, defaultContext, exports_ast as ast, TokenType, Token, RuntimeValue, Parser, Lexer, Executor, ExecutionContext, CodeGenerator, ASTNode };
|
|
538
|
+
export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isNullishAssignment, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, generate, execute, defaultContext, exports_ast as ast, TokenType, Token, RuntimeValue, Parser, Lexer, Executor, ExecutionContext, CodeGenerator, ASTNode };
|
package/dist/index.js
CHANGED
|
@@ -16,13 +16,23 @@ __export(exports_ast, {
|
|
|
16
16
|
unaryOp: () => unaryOp,
|
|
17
17
|
subtract: () => subtract,
|
|
18
18
|
number: () => number,
|
|
19
|
+
nullishAssign: () => nullishAssign,
|
|
20
|
+
notEquals: () => notEquals,
|
|
19
21
|
negate: () => negate,
|
|
20
22
|
multiply: () => multiply,
|
|
21
23
|
modulo: () => modulo,
|
|
24
|
+
logicalOr: () => logicalOr,
|
|
25
|
+
logicalAnd: () => logicalAnd,
|
|
26
|
+
lessThan: () => lessThan,
|
|
27
|
+
lessEqual: () => lessEqual,
|
|
22
28
|
identifier: () => identifier,
|
|
29
|
+
greaterThan: () => greaterThan,
|
|
30
|
+
greaterEqual: () => greaterEqual,
|
|
23
31
|
functionCall: () => functionCall,
|
|
24
32
|
exponentiate: () => exponentiate,
|
|
33
|
+
equals: () => equals,
|
|
25
34
|
divide: () => divide,
|
|
35
|
+
conditional: () => conditional,
|
|
26
36
|
binaryOp: () => binaryOp,
|
|
27
37
|
assign: () => assign,
|
|
28
38
|
add: () => add
|
|
@@ -68,6 +78,21 @@ function assign(name, value) {
|
|
|
68
78
|
value
|
|
69
79
|
};
|
|
70
80
|
}
|
|
81
|
+
function nullishAssign(name, value) {
|
|
82
|
+
return {
|
|
83
|
+
type: "NullishAssignment",
|
|
84
|
+
name,
|
|
85
|
+
value
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function conditional(condition, consequent, alternate) {
|
|
89
|
+
return {
|
|
90
|
+
type: "ConditionalExpression",
|
|
91
|
+
condition,
|
|
92
|
+
consequent,
|
|
93
|
+
alternate
|
|
94
|
+
};
|
|
95
|
+
}
|
|
71
96
|
function add(left, right) {
|
|
72
97
|
return binaryOp(left, "+", right);
|
|
73
98
|
}
|
|
@@ -89,6 +114,30 @@ function exponentiate(left, right) {
|
|
|
89
114
|
function negate(argument) {
|
|
90
115
|
return unaryOp(argument);
|
|
91
116
|
}
|
|
117
|
+
function equals(left, right) {
|
|
118
|
+
return binaryOp(left, "==", right);
|
|
119
|
+
}
|
|
120
|
+
function notEquals(left, right) {
|
|
121
|
+
return binaryOp(left, "!=", right);
|
|
122
|
+
}
|
|
123
|
+
function lessThan(left, right) {
|
|
124
|
+
return binaryOp(left, "<", right);
|
|
125
|
+
}
|
|
126
|
+
function greaterThan(left, right) {
|
|
127
|
+
return binaryOp(left, ">", right);
|
|
128
|
+
}
|
|
129
|
+
function lessEqual(left, right) {
|
|
130
|
+
return binaryOp(left, "<=", right);
|
|
131
|
+
}
|
|
132
|
+
function greaterEqual(left, right) {
|
|
133
|
+
return binaryOp(left, ">=", right);
|
|
134
|
+
}
|
|
135
|
+
function logicalAnd(left, right) {
|
|
136
|
+
return binaryOp(left, "&&", right);
|
|
137
|
+
}
|
|
138
|
+
function logicalOr(left, right) {
|
|
139
|
+
return binaryOp(left, "||", right);
|
|
140
|
+
}
|
|
92
141
|
// src/types.ts
|
|
93
142
|
var TokenType;
|
|
94
143
|
((TokenType2) => {
|
|
@@ -100,10 +149,21 @@ var TokenType;
|
|
|
100
149
|
TokenType2["SLASH"] = "SLASH";
|
|
101
150
|
TokenType2["PERCENT"] = "PERCENT";
|
|
102
151
|
TokenType2["CARET"] = "CARET";
|
|
152
|
+
TokenType2["DOUBLE_EQUALS"] = "DOUBLE_EQUALS";
|
|
153
|
+
TokenType2["NOT_EQUALS"] = "NOT_EQUALS";
|
|
154
|
+
TokenType2["LESS_THAN"] = "LESS_THAN";
|
|
155
|
+
TokenType2["GREATER_THAN"] = "GREATER_THAN";
|
|
156
|
+
TokenType2["LESS_EQUAL"] = "LESS_EQUAL";
|
|
157
|
+
TokenType2["GREATER_EQUAL"] = "GREATER_EQUAL";
|
|
158
|
+
TokenType2["LOGICAL_AND"] = "LOGICAL_AND";
|
|
159
|
+
TokenType2["LOGICAL_OR"] = "LOGICAL_OR";
|
|
103
160
|
TokenType2["LPAREN"] = "LPAREN";
|
|
104
161
|
TokenType2["RPAREN"] = "RPAREN";
|
|
105
162
|
TokenType2["EQUALS"] = "EQUALS";
|
|
106
163
|
TokenType2["COMMA"] = "COMMA";
|
|
164
|
+
TokenType2["QUESTION"] = "QUESTION";
|
|
165
|
+
TokenType2["COLON"] = "COLON";
|
|
166
|
+
TokenType2["NULLISH_ASSIGN"] = "NULLISH_ASSIGN";
|
|
107
167
|
TokenType2["EOF"] = "EOF";
|
|
108
168
|
})(TokenType ||= {});
|
|
109
169
|
function isNumberLiteral(node) {
|
|
@@ -127,6 +187,12 @@ function isAssignment(node) {
|
|
|
127
187
|
function isProgram(node) {
|
|
128
188
|
return node.type === "Program";
|
|
129
189
|
}
|
|
190
|
+
function isConditionalExpression(node) {
|
|
191
|
+
return node.type === "ConditionalExpression";
|
|
192
|
+
}
|
|
193
|
+
function isNullishAssignment(node) {
|
|
194
|
+
return node.type === "NullishAssignment";
|
|
195
|
+
}
|
|
130
196
|
|
|
131
197
|
// src/codegen.ts
|
|
132
198
|
class CodeGenerator {
|
|
@@ -145,6 +211,10 @@ class CodeGenerator {
|
|
|
145
211
|
return this.generateFunctionCall(node);
|
|
146
212
|
if (isAssignment(node))
|
|
147
213
|
return this.generateAssignment(node);
|
|
214
|
+
if (isNullishAssignment(node))
|
|
215
|
+
return this.generateNullishAssignment(node);
|
|
216
|
+
if (isConditionalExpression(node))
|
|
217
|
+
return this.generateConditionalExpression(node);
|
|
148
218
|
throw new Error(`Unknown node type`);
|
|
149
219
|
}
|
|
150
220
|
generateProgram(node) {
|
|
@@ -179,6 +249,18 @@ class CodeGenerator {
|
|
|
179
249
|
const value = this.generate(node.value);
|
|
180
250
|
return `${node.name} = ${value}`;
|
|
181
251
|
}
|
|
252
|
+
generateNullishAssignment(node) {
|
|
253
|
+
const value = this.generate(node.value);
|
|
254
|
+
return `${node.name} ??= ${value}`;
|
|
255
|
+
}
|
|
256
|
+
generateConditionalExpression(node) {
|
|
257
|
+
const condition = this.generate(node.condition);
|
|
258
|
+
const consequent = this.generate(node.consequent);
|
|
259
|
+
const alternate = this.generate(node.alternate);
|
|
260
|
+
const conditionNeedsParens = isAssignment(node.condition) || isBinaryOp(node.condition) && this.getPrecedence(node.condition.operator) <= 2;
|
|
261
|
+
const conditionCode = conditionNeedsParens ? `(${condition})` : condition;
|
|
262
|
+
return `${conditionCode} ? ${consequent} : ${alternate}`;
|
|
263
|
+
}
|
|
182
264
|
needsParensLeft(node, operator) {
|
|
183
265
|
if (!isBinaryOp(node))
|
|
184
266
|
return false;
|
|
@@ -202,14 +284,25 @@ class CodeGenerator {
|
|
|
202
284
|
getPrecedence(operator) {
|
|
203
285
|
switch (operator) {
|
|
204
286
|
case "^":
|
|
205
|
-
return
|
|
287
|
+
return 8;
|
|
206
288
|
case "*":
|
|
207
289
|
case "/":
|
|
208
290
|
case "%":
|
|
209
|
-
return
|
|
291
|
+
return 7;
|
|
210
292
|
case "+":
|
|
211
293
|
case "-":
|
|
212
|
-
return
|
|
294
|
+
return 6;
|
|
295
|
+
case "==":
|
|
296
|
+
case "!=":
|
|
297
|
+
case "<":
|
|
298
|
+
case ">":
|
|
299
|
+
case "<=":
|
|
300
|
+
case ">=":
|
|
301
|
+
return 5;
|
|
302
|
+
case "&&":
|
|
303
|
+
return 4;
|
|
304
|
+
case "||":
|
|
305
|
+
return 3;
|
|
213
306
|
default:
|
|
214
307
|
return 0;
|
|
215
308
|
}
|
|
@@ -309,14 +402,64 @@ class Lexer {
|
|
|
309
402
|
this.position++;
|
|
310
403
|
return { type: "RPAREN" /* RPAREN */, value: ")", position: start };
|
|
311
404
|
case "=":
|
|
405
|
+
if (this.peek() === "=") {
|
|
406
|
+
this.position += 2;
|
|
407
|
+
return { type: "DOUBLE_EQUALS" /* DOUBLE_EQUALS */, value: "==", position: start };
|
|
408
|
+
}
|
|
312
409
|
this.position++;
|
|
313
410
|
return { type: "EQUALS" /* EQUALS */, value: "=", position: start };
|
|
411
|
+
case "!":
|
|
412
|
+
if (this.peek() === "=") {
|
|
413
|
+
this.position += 2;
|
|
414
|
+
return { type: "NOT_EQUALS" /* NOT_EQUALS */, value: "!=", position: start };
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`Unexpected character '${char}' at position ${start}`);
|
|
417
|
+
case "<":
|
|
418
|
+
if (this.peek() === "=") {
|
|
419
|
+
this.position += 2;
|
|
420
|
+
return { type: "LESS_EQUAL" /* LESS_EQUAL */, value: "<=", position: start };
|
|
421
|
+
}
|
|
422
|
+
this.position++;
|
|
423
|
+
return { type: "LESS_THAN" /* LESS_THAN */, value: "<", position: start };
|
|
424
|
+
case ">":
|
|
425
|
+
if (this.peek() === "=") {
|
|
426
|
+
this.position += 2;
|
|
427
|
+
return { type: "GREATER_EQUAL" /* GREATER_EQUAL */, value: ">=", position: start };
|
|
428
|
+
}
|
|
429
|
+
this.position++;
|
|
430
|
+
return { type: "GREATER_THAN" /* GREATER_THAN */, value: ">", position: start };
|
|
431
|
+
case "?":
|
|
432
|
+
if (this.peek() === "?" && this.peekAhead(2) === "=") {
|
|
433
|
+
this.position += 3;
|
|
434
|
+
return {
|
|
435
|
+
type: "NULLISH_ASSIGN" /* NULLISH_ASSIGN */,
|
|
436
|
+
value: "??=",
|
|
437
|
+
position: start
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
this.position++;
|
|
441
|
+
return { type: "QUESTION" /* QUESTION */, value: "?", position: start };
|
|
442
|
+
case ":":
|
|
443
|
+
this.position++;
|
|
444
|
+
return { type: "COLON" /* COLON */, value: ":", position: start };
|
|
314
445
|
case ",":
|
|
315
446
|
this.position++;
|
|
316
447
|
return { type: "COMMA" /* COMMA */, value: ",", position: start };
|
|
317
448
|
case ";":
|
|
318
449
|
this.position++;
|
|
319
450
|
return this.nextToken();
|
|
451
|
+
case "&":
|
|
452
|
+
if (this.peek() === "&") {
|
|
453
|
+
this.position += 2;
|
|
454
|
+
return { type: "LOGICAL_AND" /* LOGICAL_AND */, value: "&&", position: start };
|
|
455
|
+
}
|
|
456
|
+
throw new Error(`Unexpected character '${char}' at position ${start}`);
|
|
457
|
+
case "|":
|
|
458
|
+
if (this.peek() === "|") {
|
|
459
|
+
this.position += 2;
|
|
460
|
+
return { type: "LOGICAL_OR" /* LOGICAL_OR */, value: "||", position: start };
|
|
461
|
+
}
|
|
462
|
+
throw new Error(`Unexpected character '${char}' at position ${start}`);
|
|
320
463
|
default:
|
|
321
464
|
throw new Error(`Unexpected character '${char}' at position ${start}`);
|
|
322
465
|
}
|
|
@@ -389,6 +532,9 @@ class Lexer {
|
|
|
389
532
|
peek() {
|
|
390
533
|
return this.getCharAt(this.position + 1);
|
|
391
534
|
}
|
|
535
|
+
peekAhead(n) {
|
|
536
|
+
return this.getCharAt(this.position + n);
|
|
537
|
+
}
|
|
392
538
|
isDigit(char) {
|
|
393
539
|
return char >= "0" && char <= "9";
|
|
394
540
|
}
|
|
@@ -442,12 +588,38 @@ class Parser {
|
|
|
442
588
|
}
|
|
443
589
|
const identName = left.name;
|
|
444
590
|
this.advance();
|
|
445
|
-
const value = this.parseExpression(precedence
|
|
591
|
+
const value = this.parseExpression(precedence);
|
|
446
592
|
left = {
|
|
447
593
|
type: "Assignment",
|
|
448
594
|
name: identName,
|
|
449
595
|
value
|
|
450
596
|
};
|
|
597
|
+
} else if (token.type === "NULLISH_ASSIGN" /* NULLISH_ASSIGN */) {
|
|
598
|
+
if (left.type !== "Identifier") {
|
|
599
|
+
throw new Error("Invalid assignment target");
|
|
600
|
+
}
|
|
601
|
+
const identName = left.name;
|
|
602
|
+
this.advance();
|
|
603
|
+
const value = this.parseExpression(precedence);
|
|
604
|
+
left = {
|
|
605
|
+
type: "NullishAssignment",
|
|
606
|
+
name: identName,
|
|
607
|
+
value
|
|
608
|
+
};
|
|
609
|
+
} else if (token.type === "QUESTION" /* QUESTION */) {
|
|
610
|
+
this.advance();
|
|
611
|
+
const consequent = this.parseExpression(0);
|
|
612
|
+
if (this.peek().type !== "COLON" /* COLON */) {
|
|
613
|
+
throw new Error("Expected : in ternary expression");
|
|
614
|
+
}
|
|
615
|
+
this.advance();
|
|
616
|
+
const alternate = this.parseExpression(precedence);
|
|
617
|
+
left = {
|
|
618
|
+
type: "ConditionalExpression",
|
|
619
|
+
condition: left,
|
|
620
|
+
consequent,
|
|
621
|
+
alternate
|
|
622
|
+
};
|
|
451
623
|
} else if (this.isBinaryOperator(token.type)) {
|
|
452
624
|
const operator = token.value;
|
|
453
625
|
this.advance();
|
|
@@ -529,25 +701,39 @@ class Parser {
|
|
|
529
701
|
getPrecedence(type) {
|
|
530
702
|
switch (type) {
|
|
531
703
|
case "EQUALS" /* EQUALS */:
|
|
704
|
+
case "NULLISH_ASSIGN" /* NULLISH_ASSIGN */:
|
|
532
705
|
return 1;
|
|
706
|
+
case "QUESTION" /* QUESTION */:
|
|
707
|
+
return 2;
|
|
708
|
+
case "LOGICAL_OR" /* LOGICAL_OR */:
|
|
709
|
+
return 3;
|
|
710
|
+
case "LOGICAL_AND" /* LOGICAL_AND */:
|
|
711
|
+
return 4;
|
|
712
|
+
case "DOUBLE_EQUALS" /* DOUBLE_EQUALS */:
|
|
713
|
+
case "NOT_EQUALS" /* NOT_EQUALS */:
|
|
714
|
+
case "LESS_THAN" /* LESS_THAN */:
|
|
715
|
+
case "GREATER_THAN" /* GREATER_THAN */:
|
|
716
|
+
case "LESS_EQUAL" /* LESS_EQUAL */:
|
|
717
|
+
case "GREATER_EQUAL" /* GREATER_EQUAL */:
|
|
718
|
+
return 5;
|
|
533
719
|
case "PLUS" /* PLUS */:
|
|
534
720
|
case "MINUS" /* MINUS */:
|
|
535
|
-
return
|
|
721
|
+
return 6;
|
|
536
722
|
case "STAR" /* STAR */:
|
|
537
723
|
case "SLASH" /* SLASH */:
|
|
538
724
|
case "PERCENT" /* PERCENT */:
|
|
539
|
-
return
|
|
725
|
+
return 7;
|
|
540
726
|
case "CARET" /* CARET */:
|
|
541
|
-
return
|
|
727
|
+
return 8;
|
|
542
728
|
default:
|
|
543
729
|
return 0;
|
|
544
730
|
}
|
|
545
731
|
}
|
|
546
732
|
getUnaryPrecedence() {
|
|
547
|
-
return
|
|
733
|
+
return 6;
|
|
548
734
|
}
|
|
549
735
|
isBinaryOperator(type) {
|
|
550
|
-
return type === "PLUS" /* PLUS */ || type === "MINUS" /* MINUS */ || type === "STAR" /* STAR */ || type === "SLASH" /* SLASH */ || type === "PERCENT" /* PERCENT */ || type === "CARET" /* CARET */;
|
|
736
|
+
return type === "PLUS" /* PLUS */ || type === "MINUS" /* MINUS */ || type === "STAR" /* STAR */ || type === "SLASH" /* SLASH */ || type === "PERCENT" /* PERCENT */ || type === "CARET" /* CARET */ || type === "DOUBLE_EQUALS" /* DOUBLE_EQUALS */ || type === "NOT_EQUALS" /* NOT_EQUALS */ || type === "LESS_THAN" /* LESS_THAN */ || type === "GREATER_THAN" /* GREATER_THAN */ || type === "LESS_EQUAL" /* LESS_EQUAL */ || type === "GREATER_EQUAL" /* GREATER_EQUAL */ || type === "LOGICAL_AND" /* LOGICAL_AND */ || type === "LOGICAL_OR" /* LOGICAL_OR */;
|
|
551
737
|
}
|
|
552
738
|
peek() {
|
|
553
739
|
if (this.current >= this.tokens.length) {
|
|
@@ -593,6 +779,10 @@ class Executor {
|
|
|
593
779
|
return this.executeFunctionCall(node);
|
|
594
780
|
if (isAssignment(node))
|
|
595
781
|
return this.executeAssignment(node);
|
|
782
|
+
if (isNullishAssignment(node))
|
|
783
|
+
return this.executeNullishAssignment(node);
|
|
784
|
+
if (isConditionalExpression(node))
|
|
785
|
+
return this.executeConditionalExpression(node);
|
|
596
786
|
throw new Error(`Unknown node type`);
|
|
597
787
|
}
|
|
598
788
|
executeProgram(node) {
|
|
@@ -634,6 +824,22 @@ class Executor {
|
|
|
634
824
|
return left % right;
|
|
635
825
|
case "^":
|
|
636
826
|
return left ** right;
|
|
827
|
+
case "==":
|
|
828
|
+
return left === right ? 1 : 0;
|
|
829
|
+
case "!=":
|
|
830
|
+
return left !== right ? 1 : 0;
|
|
831
|
+
case "<":
|
|
832
|
+
return left < right ? 1 : 0;
|
|
833
|
+
case ">":
|
|
834
|
+
return left > right ? 1 : 0;
|
|
835
|
+
case "<=":
|
|
836
|
+
return left <= right ? 1 : 0;
|
|
837
|
+
case ">=":
|
|
838
|
+
return left >= right ? 1 : 0;
|
|
839
|
+
case "&&":
|
|
840
|
+
return left !== 0 && right !== 0 ? 1 : 0;
|
|
841
|
+
case "||":
|
|
842
|
+
return left !== 0 || right !== 0 ? 1 : 0;
|
|
637
843
|
default:
|
|
638
844
|
throw new Error(`Unknown operator: ${node.operator}`);
|
|
639
845
|
}
|
|
@@ -661,6 +867,18 @@ class Executor {
|
|
|
661
867
|
this.variables.set(node.name, value);
|
|
662
868
|
return value;
|
|
663
869
|
}
|
|
870
|
+
executeNullishAssignment(node) {
|
|
871
|
+
if (this.variables.has(node.name)) {
|
|
872
|
+
return this.variables.get(node.name);
|
|
873
|
+
}
|
|
874
|
+
const value = this.execute(node.value);
|
|
875
|
+
this.variables.set(node.name, value);
|
|
876
|
+
return value;
|
|
877
|
+
}
|
|
878
|
+
executeConditionalExpression(node) {
|
|
879
|
+
const condition = this.execute(node.condition);
|
|
880
|
+
return condition !== 0 ? this.execute(node.consequent) : this.execute(node.alternate);
|
|
881
|
+
}
|
|
664
882
|
}
|
|
665
883
|
function execute(source, context) {
|
|
666
884
|
const ast = parseSource(source);
|
|
@@ -697,7 +915,7 @@ function analyzeProgram(node) {
|
|
|
697
915
|
const stmt = node.statements[i];
|
|
698
916
|
if (!stmt)
|
|
699
917
|
continue;
|
|
700
|
-
if (isAssignment(stmt)) {
|
|
918
|
+
if (isAssignment(stmt) || isNullishAssignment(stmt)) {
|
|
701
919
|
const varName = stmt.name;
|
|
702
920
|
const count = assignmentCounts.get(varName) || 0;
|
|
703
921
|
assignmentCounts.set(varName, count + 1);
|
|
@@ -784,7 +1002,7 @@ function collectDependencies(node, deps) {
|
|
|
784
1002
|
if (isNumberLiteral(node)) {
|
|
785
1003
|
return false;
|
|
786
1004
|
}
|
|
787
|
-
if (isAssignment(node)) {
|
|
1005
|
+
if (isAssignment(node) || isNullishAssignment(node)) {
|
|
788
1006
|
return collectDependencies(node.value, deps);
|
|
789
1007
|
}
|
|
790
1008
|
if (isBinaryOp(node)) {
|
|
@@ -801,6 +1019,12 @@ function collectDependencies(node, deps) {
|
|
|
801
1019
|
}
|
|
802
1020
|
return true;
|
|
803
1021
|
}
|
|
1022
|
+
if (isConditionalExpression(node)) {
|
|
1023
|
+
const condHasCall = collectDependencies(node.condition, deps);
|
|
1024
|
+
const consHasCall = collectDependencies(node.consequent, deps);
|
|
1025
|
+
const altHasCall = collectDependencies(node.alternate, deps);
|
|
1026
|
+
return condHasCall || consHasCall || altHasCall;
|
|
1027
|
+
}
|
|
804
1028
|
if (isProgram(node)) {
|
|
805
1029
|
let hasCall = false;
|
|
806
1030
|
for (const stmt of node.statements) {
|
|
@@ -920,12 +1144,32 @@ function evaluateWithConstants(node, constants) {
|
|
|
920
1144
|
arguments: node.arguments.map((arg) => evaluateWithConstants(arg, constants))
|
|
921
1145
|
};
|
|
922
1146
|
}
|
|
1147
|
+
if (isConditionalExpression(node)) {
|
|
1148
|
+
const condition = evaluateWithConstants(node.condition, constants);
|
|
1149
|
+
const consequent = evaluateWithConstants(node.consequent, constants);
|
|
1150
|
+
const alternate = evaluateWithConstants(node.alternate, constants);
|
|
1151
|
+
if (isNumberLiteral(condition)) {
|
|
1152
|
+
return condition.value !== 0 ? consequent : alternate;
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
...node,
|
|
1156
|
+
condition,
|
|
1157
|
+
consequent,
|
|
1158
|
+
alternate
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
923
1161
|
if (isAssignment(node)) {
|
|
924
1162
|
return {
|
|
925
1163
|
...node,
|
|
926
1164
|
value: evaluateWithConstants(node.value, constants)
|
|
927
1165
|
};
|
|
928
1166
|
}
|
|
1167
|
+
if (isNullishAssignment(node)) {
|
|
1168
|
+
return {
|
|
1169
|
+
...node,
|
|
1170
|
+
value: evaluateWithConstants(node.value, constants)
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
929
1173
|
return node;
|
|
930
1174
|
}
|
|
931
1175
|
function replaceWithConstants(node, constants) {
|
|
@@ -954,7 +1198,7 @@ function eliminateDeadCodeOptimal(node, analysis, allConstants) {
|
|
|
954
1198
|
filteredStatements.push(stmt);
|
|
955
1199
|
continue;
|
|
956
1200
|
}
|
|
957
|
-
if (isAssignment(stmt)) {
|
|
1201
|
+
if (isAssignment(stmt) || isNullishAssignment(stmt)) {
|
|
958
1202
|
if (variablesInUse.has(stmt.name)) {
|
|
959
1203
|
filteredStatements.push(stmt);
|
|
960
1204
|
}
|
|
@@ -999,6 +1243,12 @@ function basicOptimize(node) {
|
|
|
999
1243
|
value: basicOptimize(node.value)
|
|
1000
1244
|
};
|
|
1001
1245
|
}
|
|
1246
|
+
if (isNullishAssignment(node)) {
|
|
1247
|
+
return {
|
|
1248
|
+
...node,
|
|
1249
|
+
value: basicOptimize(node.value)
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1002
1252
|
if (isBinaryOp(node)) {
|
|
1003
1253
|
const left = basicOptimize(node.left);
|
|
1004
1254
|
const right = basicOptimize(node.right);
|
|
@@ -1028,6 +1278,20 @@ function basicOptimize(node) {
|
|
|
1028
1278
|
arguments: node.arguments.map((arg) => basicOptimize(arg))
|
|
1029
1279
|
};
|
|
1030
1280
|
}
|
|
1281
|
+
if (isConditionalExpression(node)) {
|
|
1282
|
+
const condition = basicOptimize(node.condition);
|
|
1283
|
+
const consequent = basicOptimize(node.consequent);
|
|
1284
|
+
const alternate = basicOptimize(node.alternate);
|
|
1285
|
+
if (isNumberLiteral(condition)) {
|
|
1286
|
+
return condition.value !== 0 ? consequent : alternate;
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
...node,
|
|
1290
|
+
condition,
|
|
1291
|
+
consequent,
|
|
1292
|
+
alternate
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1031
1295
|
if (isProgram(node)) {
|
|
1032
1296
|
return {
|
|
1033
1297
|
...node,
|
|
@@ -1056,6 +1320,22 @@ function evaluateBinaryOp(operator, left, right) {
|
|
|
1056
1320
|
return left % right;
|
|
1057
1321
|
case "^":
|
|
1058
1322
|
return left ** right;
|
|
1323
|
+
case "==":
|
|
1324
|
+
return left === right ? 1 : 0;
|
|
1325
|
+
case "!=":
|
|
1326
|
+
return left !== right ? 1 : 0;
|
|
1327
|
+
case "<":
|
|
1328
|
+
return left < right ? 1 : 0;
|
|
1329
|
+
case ">":
|
|
1330
|
+
return left > right ? 1 : 0;
|
|
1331
|
+
case "<=":
|
|
1332
|
+
return left <= right ? 1 : 0;
|
|
1333
|
+
case ">=":
|
|
1334
|
+
return left >= right ? 1 : 0;
|
|
1335
|
+
case "&&":
|
|
1336
|
+
return left !== 0 && right !== 0 ? 1 : 0;
|
|
1337
|
+
case "||":
|
|
1338
|
+
return left !== 0 || right !== 0 ? 1 : 0;
|
|
1059
1339
|
default:
|
|
1060
1340
|
throw new Error(`Unknown operator: ${operator}`);
|
|
1061
1341
|
}
|
|
@@ -1066,8 +1346,10 @@ export {
|
|
|
1066
1346
|
isUnaryOp,
|
|
1067
1347
|
isProgram,
|
|
1068
1348
|
isNumberLiteral,
|
|
1349
|
+
isNullishAssignment,
|
|
1069
1350
|
isIdentifier,
|
|
1070
1351
|
isFunctionCall,
|
|
1352
|
+
isConditionalExpression,
|
|
1071
1353
|
isBinaryOp,
|
|
1072
1354
|
isAssignment,
|
|
1073
1355
|
generate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlewing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A minimal, high-performance arithmetic expression language with lexer, parser, and executor. Optimized for browsers with zero dependencies and type-safe execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arithmetic",
|