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 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** - 5.62 KB gzipped, zero dependencies
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** - 136 tests, 99.52% line coverage
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
- All standard arithmetic operators with proper precedence:
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
- (3 - // exponentiation (power)
139
- 5); // unary minus
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
- 1. Unary minus (`-`) - Highest
145
- 2. Exponentiation (`^`)
146
- 3. Multiplication, division, modulo (`*`, `/`, `%`)
147
- 4. Addition, subtraction (`+`, `-`)
148
- 5. Assignment (`=`) - Lowest
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
- ast.assign("x", value);
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
- - **5.62 KB gzipped** (28.22 KB raw)
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
- - **136 tests** with **99.52% line coverage**
549
- - **99.26% function coverage**
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 any arguments and return a number
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: any[]) => number>;
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 4;
287
+ return 8;
206
288
  case "*":
207
289
  case "/":
208
290
  case "%":
209
- return 3;
291
+ return 7;
210
292
  case "+":
211
293
  case "-":
212
- return 2;
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 + 1);
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 2;
721
+ return 6;
536
722
  case "STAR" /* STAR */:
537
723
  case "SLASH" /* SLASH */:
538
724
  case "PERCENT" /* PERCENT */:
539
- return 3;
725
+ return 7;
540
726
  case "CARET" /* CARET */:
541
- return 4;
727
+ return 8;
542
728
  default:
543
729
  return 0;
544
730
  }
545
731
  }
546
732
  getUnaryPrecedence() {
547
- return 5;
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.4.2",
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",