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