simplex-lang 0.2.2 → 1.0.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.
Files changed (51) hide show
  1. package/README.md +530 -13
  2. package/build/parser/index.js +1161 -479
  3. package/build/parser/index.js.map +1 -1
  4. package/build/src/compiler.d.ts +26 -18
  5. package/build/src/compiler.js +211 -457
  6. package/build/src/compiler.js.map +1 -1
  7. package/build/src/constants.d.ts +25 -0
  8. package/build/src/constants.js +29 -0
  9. package/build/src/constants.js.map +1 -0
  10. package/build/src/error-mapping.d.ts +31 -0
  11. package/build/src/error-mapping.js +72 -0
  12. package/build/src/error-mapping.js.map +1 -0
  13. package/build/src/errors.d.ts +8 -5
  14. package/build/src/errors.js +8 -10
  15. package/build/src/errors.js.map +1 -1
  16. package/build/src/index.d.ts +1 -0
  17. package/build/src/index.js +1 -0
  18. package/build/src/index.js.map +1 -1
  19. package/build/src/simplex-tree.d.ts +25 -3
  20. package/build/src/simplex.peggy +120 -3
  21. package/build/src/tools/index.d.ts +25 -6
  22. package/build/src/tools/index.js +91 -19
  23. package/build/src/tools/index.js.map +1 -1
  24. package/build/src/version.js +1 -1
  25. package/build/src/visitors.d.ts +15 -0
  26. package/build/src/visitors.js +330 -0
  27. package/build/src/visitors.js.map +1 -0
  28. package/package.json +10 -8
  29. package/parser/index.js +1161 -479
  30. package/parser/index.js.map +1 -1
  31. package/src/compiler.ts +332 -609
  32. package/src/constants.ts +30 -0
  33. package/src/error-mapping.ts +112 -0
  34. package/src/errors.ts +8 -12
  35. package/src/index.ts +1 -0
  36. package/src/simplex-tree.ts +30 -2
  37. package/src/simplex.peggy +120 -3
  38. package/src/tools/index.ts +117 -24
  39. package/src/visitors.ts +491 -0
  40. package/build/src/tools/cast.d.ts +0 -2
  41. package/build/src/tools/cast.js +0 -20
  42. package/build/src/tools/cast.js.map +0 -1
  43. package/build/src/tools/ensure.d.ts +0 -3
  44. package/build/src/tools/ensure.js +0 -30
  45. package/build/src/tools/ensure.js.map +0 -1
  46. package/build/src/tools/guards.d.ts +0 -2
  47. package/build/src/tools/guards.js +0 -20
  48. package/build/src/tools/guards.js.map +0 -1
  49. package/src/tools/cast.ts +0 -26
  50. package/src/tools/ensure.ts +0 -41
  51. package/src/tools/guards.ts +0 -29
package/README.md CHANGED
@@ -2,36 +2,553 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/simplex-lang.svg?cacheSeconds=1800&style=flat-square)](https://www.npmjs.com/package/simplex-lang)
4
4
  [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/wmakeev/simplex/main.yml?style=flat-square)](https://github.com/wmakeev/simplex/actions/workflows/main.yml)
5
+ ![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/wmakeev/simplex/main/badges/coverage.json)
5
6
  ![no dependencies](https://img.shields.io/badge/dependencies-no-green?style=flat-square)
6
7
  [![parser](https://img.shields.io/badge/parser-peggy-pink?style=flat-square)](https://peggyjs.org/)
7
- <!-- [![Codecov](https://img.shields.io/codecov/c/github/wmakeev/simplex?style=flat-square)](https://app.codecov.io/gh/wmakeev/simplex/tree/master/) -->
8
8
 
9
- > **SimpEx** - javascript **Simpl**e **Ex**pression language
9
+ > **SimplEx** — a zero-dependency TypeScript compiler that turns expression strings into safe, sandboxed JavaScript functions.
10
10
 
11
11
  ## Table of contents <!-- omit in toc -->
12
12
 
13
- - [Quick start](#quick-start)
14
- - [Links](#links)
13
+ - [Why SimplEx?](#why-simplex)
14
+ - [Quick Start](#quick-start)
15
+ - [Playground](#playground)
16
+ - [Like JS, but...](#like-js-but)
17
+ - [Language Reference](#language-reference)
18
+ - [Literals](#literals)
19
+ - [Operators](#operators)
20
+ - [String Concatenation](#string-concatenation)
21
+ - [Collections](#collections)
22
+ - [Property Access](#property-access)
23
+ - [Function Calls](#function-calls)
24
+ - [Currying with #](#currying-with-)
25
+ - [Conditionals](#conditionals)
26
+ - [Pipe Operators](#pipe-operators)
27
+ - [Lambda Expressions](#lambda-expressions)
28
+ - [Let Expressions](#let-expressions)
29
+ - [Template Literals](#template-literals)
30
+ - [Comments](#comments)
31
+ - [Reserved Words](#reserved-words)
32
+ - [Data and Scope](#data-and-scope)
33
+ - [API Reference](#api-reference)
34
+ - [compile()](#compile)
35
+ - [CompileOptions](#compileoptions)
36
+ - [Errors](#errors)
37
+ - [Customization](#customization)
38
+ - [Using External Functions](#using-external-functions)
39
+ - [License](#license)
15
40
 
16
- ## Quick start
41
+ ## Why SimplEx?
42
+
43
+ SimplEx is designed for scenarios where you need to evaluate user-provided expressions safely:
44
+
45
+ - **ETL/ELT pipelines** — calculated fields, data transformations, filtering rules
46
+ - **Business rules engines** — config-driven formulas and conditions
47
+ - **Template engines** — dynamic value computation
48
+ - **Spreadsheet-like UIs** — user-defined formulas
49
+
50
+ **Why not just `eval()`?** SimplEx expressions run in a fully sandboxed environment with no access to the global scope, prototype chains, or Node.js/browser APIs. Users can only work with data and functions you explicitly provide.
51
+
52
+ **Why not a full language?** SimplEx is expression-only — no statements, no assignments, no loops, no side effects. Every expression deterministically computes a single value. This makes expressions easy to reason about and safe to store in configs and databases.
53
+
54
+ **What you get:**
55
+
56
+ - Familiar JS-like syntax — if you know JavaScript, you already know most of SimplEx
57
+ - Runtime type safety — arithmetic rejects `NaN`/`Infinity`, clear errors with source locations
58
+ - Fully customizable — override any operator, identifier resolution, property access, or pipe behavior
59
+ - Zero dependencies, ESM-only, TypeScript-first
60
+
61
+ ## Quick Start
62
+
63
+ ```bash
64
+ npm install simplex-lang
65
+ ```
17
66
 
18
67
  ```ts
19
68
  import { compile } from 'simplex-lang'
20
69
 
21
- const fn = compile(`(a + b) * min(a, b) + 10`, {
70
+ // Pass functions via globals, data at runtime
71
+ const fn = compile('(a + b) * min(a, b) + 10', {
72
+ globals: { min: Math.min }
73
+ })
74
+
75
+ fn({ a: 2, b: 3 }) // 20
76
+ ```
77
+
78
+ ```ts
79
+ // Pure data expression — no globals needed
80
+ const expr = compile('price * quantity * (1 - discount)')
81
+
82
+ expr({ price: 100, quantity: 5, discount: 0.1 }) // 450
83
+ ```
84
+
85
+ ## Playground
86
+
87
+ Try SimplEx in the browser — edit expressions, inspect the AST, and see results instantly:
88
+
89
+ **[SimplEx Playground](https://wmakeev.github.io/simplex/)**
90
+
91
+ ## Like JS, but...
92
+
93
+ SimplEx syntax is intentionally close to JavaScript. If you know JS, you can start writing SimplEx immediately. Here are the key differences:
94
+
95
+ | Concept | JavaScript | SimplEx | Why |
96
+ |---|---|---|---|
97
+ | String concatenation | `"a" + "b"` | `"a" & "b"` | `+` is reserved for numeric addition only |
98
+ | Conditional | `x ? a : b` | `if x then a else b` | Readable keyword syntax |
99
+ | Modulo | `a % b` | `a mod b` | `%` is the topic reference in pipes |
100
+ | Exponentiation | `a ** b` | `a ^ b` | Shorter syntax |
101
+ | Logical NOT | `!x` | `not x` | Word operator |
102
+ | Logical AND/OR | `a && b` returns `a` or `b` | `a and b` returns `boolean` | `&&`/`\|\|` also available, but return booleans too |
103
+ | Equality | `===` / `!==` | `==` / `!=` | Always strict — no loose equality exists |
104
+ | Optional chaining | `obj?.prop` | `obj.prop` | Null-safe by default — `null.x` returns `undefined` |
105
+ | Optional call | `fn?.()` | `fn()` | Calling `null`/`undefined` returns `undefined` |
106
+ | Pipe | Stage 2 proposal | `x \| % + 1` | Built-in with `%` as topic reference |
107
+ | Partial application | — | `fn(#, 3)` | `#` creates a curried function |
108
+ | `let` | Statement | `let x = 5, x + 1` | Expression that returns a value |
109
+ | `in` operator | Checks prototype chain | Checks own keys only | Works with objects, arrays, and Maps |
110
+
111
+ **Everything else works as you'd expect from JavaScript:** arrow functions (`x => x + 1`), template literals (`` `hello ${name}` ``), tagged templates, arrays, objects, spread operators, dot/bracket property access, nullish coalescing (`??`), `typeof`, and comments (`//`, `/* */`).
112
+
113
+ ## Language Reference
114
+
115
+ ### Literals
116
+
117
+ | Expression | Description |
118
+ | --- | --- |
119
+ | `42`, `.5`, `1.2e3`, `0xFF` | Numbers (integer, decimal, scientific, hex) |
120
+ | `"hello"`, `'world'` | Strings (supports `\n`, `\t`, `\uXXXX` escapes) |
121
+ | `true`, `false` | Booleans |
122
+ | `null` | Null |
123
+ | `undefined` | Undefined (identifier, not a keyword) |
124
+
125
+ ### Operators
126
+
127
+ Operators listed by precedence (highest first):
128
+
129
+ | Precedence | Operators | Description |
130
+ |---|---|---|
131
+ | 1 | `+x` `-x` `not x` `typeof x` | Unary |
132
+ | 2 | `^` | Exponentiation (right-associative) |
133
+ | 3 | `*` `/` `mod` | Multiplicative |
134
+ | 4 | `+` `-` | Additive (numbers only) |
135
+ | 5 | `&` | String concatenation (coerces to string) |
136
+ | 6 | `<` `<=` `>` `>=` `in` | Relational |
137
+ | 7 | `==` `!=` | Equality (strict) |
138
+ | 8 | `and` `&&` | Logical AND (short-circuit, returns boolean) |
139
+ | 9 | `or` `\|\|` | Logical OR (short-circuit, returns boolean) |
140
+ | 10 | `??` | Nullish coalescing |
141
+ | 11 | `\|` `\|?` `\|>` | Pipe operators |
142
+
143
+ **Runtime type enforcement:**
144
+
145
+ - Arithmetic (`+`, `-`, `*`, `/`, `mod`, `^`) — operands must be finite numbers or bigints
146
+ - Relational (`<`, `>`, `<=`, `>=`) — operands must be numbers or strings
147
+ - `&` — coerces any value to string
148
+ - `==`/`!=` — strict comparison, no coercion
149
+
150
+ ### String Concatenation
151
+
152
+ The `+` operator only works with numbers. Use `&` to concatenate strings:
153
+
154
+ | Expression | Result |
155
+ | --- | --- |
156
+ | `"Hello" & " " & "world"` | `"Hello world"` |
157
+ | `"Count: " & 42` | `"Count: 42"` (coerces to string) |
158
+ | `"Values: " & [1, 2, 3]` | `"Values: 1,2,3"` |
159
+
160
+ ### Collections
161
+
162
+ **Arrays:**
163
+
164
+ | Expression | Description |
165
+ | --- | --- |
166
+ | `[1, 2, 3]` | Array literal |
167
+ | `[1, , 3]` | Sparse array |
168
+ | `[1, ...other, 4]` | Spread (arrays only) |
169
+
170
+ **Objects:**
171
+
172
+ | Expression | Description |
173
+ | --- | --- |
174
+ | `{ a: 1, b: 2 }` | Object literal |
175
+ | `{ "special-key": 1 }` | Quoted key |
176
+ | `{ [dynamic]: value }` | Computed key |
177
+ | `{ ...base, extra: true }` | Spread (objects only) |
178
+
179
+ ### Property Access
180
+
181
+ | Expression | Description |
182
+ | --- | --- |
183
+ | `obj.name` | Dot access (own properties only) |
184
+ | `obj["key"]` | Bracket access (own properties only) |
185
+ | `arr[0]` | Index access |
186
+ | `obj.nested.deep` | Chaining |
187
+ | `null.anything` | `undefined` (null-safe, no error) |
188
+ | `expr!` | Non-null assert — throws if `null`/`undefined` |
189
+ | `a.b!.c.d!` | Chainable non-null assertions |
190
+ | `foo!(args)` | Assert non-null, then call |
191
+
192
+ > **Note:** Unlike JavaScript (which has optional chaining `?.` and no runtime `!`), SimplEx has null-safe member access by default but explicit non-null assertion via `!`. This is inverted from JS — more practical for an expression language working with optional data structures.
193
+
194
+ **Extension methods** (`::`) — call methods registered via the `extensions` compile option. `obj::method(args)` calls `extensionMap.method(obj, args)`. Requires `extensions` in `CompileOptions`. Null-safe: `null::method()` returns `undefined`. Throws if no extension methods are defined for the type or the method is not found.
195
+
196
+ ### Function Calls
197
+
198
+ | Expression | Description |
199
+ | --- | --- |
200
+ | `min(1, 2)` | Global function |
201
+ | `obj.method(x)` | Method call |
202
+ | `fn()()` | Chaining |
203
+ | `null()` | `undefined` (null-safe) |
204
+
205
+ ### Currying with `#`
206
+
207
+ The `#` placeholder in function arguments creates a partially applied function:
208
+
209
+ | Expression | Equivalent |
210
+ | --- | --- |
211
+ | `add(#, 3)` | `x => add(x, 3)` |
212
+ | `add(1, #)` | `x => add(1, x)` |
213
+ | `mul(#, 2, #)` | `(a, b) => mul(a, 2, b)` |
214
+ | `[1, 2, 3] \| map(%, add(#, 10))` | `[11, 12, 13]` |
215
+
216
+ ### Conditionals
217
+
218
+ | Expression | Description |
219
+ | --- | --- |
220
+ | `if score >= 90 then "A" else "B"` | Conditional with else |
221
+ | `if active then value` | Else is optional (defaults to `undefined`) |
222
+
223
+ Falsy values: `0`, `""`, `false`, `null`, `undefined`, `NaN`. Everything else is truthy.
224
+
225
+ ### Pipe Operators
226
+
227
+ Pipes chain a value through a series of expressions. The `%` topic reference holds the current value:
228
+
229
+ | Expression | Result |
230
+ | --- | --- |
231
+ | `5 \| % + 1` | `6` |
232
+ | `5 \| % * 2 \| % + 1` | `11` |
233
+ | `1 \| add(%, 2) \| % * 4` | `12` |
234
+ | `value \|? toUpper(%)` | If `value` is `null`, returns `null` (`\|?` short-circuits) |
235
+
236
+ **`|>` (forward pipe)** — reserved. Override `pipe` in compile options to implement custom semantics.
237
+
238
+ ### Lambda Expressions
239
+
240
+ | Expression | Description |
241
+ | --- | --- |
242
+ | `x => x + 1` | Single parameter |
243
+ | `(a, b) => a + b` | Multiple parameters |
244
+ | `() => 42` | No parameters |
245
+ | `a => b => a + b` | Curried (nested) |
246
+
247
+ Lambdas are closures — they capture the enclosing scope. Parameters shadow outer variables.
248
+
249
+ ### Let Expressions
250
+
251
+ `let` creates local bindings and evaluates a body expression:
252
+
253
+ | Expression | Result |
254
+ | --- | --- |
255
+ | `let x = 5, x + 1` | `6` |
256
+ | `let a = 1, b = a + 1, a + b` | `3` |
257
+ | `let tax = price * 0.2, price + tax` | Sequential binding |
258
+
259
+ Bindings are sequential — each initializer can reference previous bindings. The last comma-separated expression is the body. Duplicate names cause a `CompileError`.
260
+
261
+ ### Template Literals
262
+
263
+ | Expression | Description |
264
+ | --- | --- |
265
+ | `` `Hello ${name}, you have ${count} items` `` | String interpolation |
266
+ | `` `Price: ${price * (1 + tax)}` `` | Any expression inside `${}` |
267
+ | `` `Nested: ${`inner ${x}`}` `` | Nested template literals |
268
+ | Multiline content | Allowed (unlike regular strings) |
269
+
270
+ **Tagged template literals** — any expression before a template literal calls it as a tag function:
271
+
272
+ | Expression | Description |
273
+ | --- | --- |
274
+ | `` sql`SELECT * FROM ${table}` `` | Tag receives `(strings, ...values)` |
275
+ | `` obj.escape`user input: ${value}` `` | Member expression as tag |
276
+
277
+ The tag function receives an array of static string parts and the interpolated values (not coerced to strings). It can return any type.
278
+
279
+ ### Comments
280
+
281
+ | Syntax | Description |
282
+ | --- | --- |
283
+ | `// comment` | Single-line comment |
284
+ | `/* comment */` | Multi-line / inline comment |
285
+
286
+ ### Reserved Words
287
+
288
+ `if`, `then`, `else`, `and`, `or`, `not`, `in`, `mod`, `typeof`, `let`, `true`, `false`, `null` — cannot be used as identifiers.
289
+
290
+ ## Data and Scope
291
+
292
+ Identifiers are resolved in this order: **local scope** (lambda params, let bindings) -> **closure** -> **globals** -> **data** -> **error**.
293
+
294
+ ```ts
295
+ // Globals — compile-time constants, always take priority
296
+ const fn = compile('x + y', { globals: { x: 10 } })
297
+ fn({ x: 999, y: 20 }) // 30 (x=10 from globals, y=20 from data)
298
+
299
+ // Data — runtime values passed when calling the compiled function
300
+ const expr = compile('firstName & " " & lastName')
301
+ expr({ firstName: 'John', lastName: 'Doe' }) // "John Doe"
302
+ ```
303
+
304
+ Globals take priority over data. This lets you provide trusted constants and functions that user expressions cannot override.
305
+
306
+ ## API Reference
307
+
308
+ ### compile()
309
+
310
+ ```ts
311
+ import { compile } from 'simplex-lang'
312
+
313
+ function compile<
314
+ Data = Record<string, unknown>,
315
+ Globals = Record<string, unknown>
316
+ >(
317
+ expression: string,
318
+ options?: CompileOptions<Data, Globals>
319
+ ): (data?: Data) => unknown
320
+ ```
321
+
322
+ Compiles a SimplEx expression string into a reusable function. The returned function accepts an optional `data` argument and returns the result of evaluating the expression.
323
+
324
+ ### CompileOptions
325
+
326
+ ```ts
327
+ type CompileOptions<Data, Globals> = Partial<
328
+ ContextHelpers<Data, Globals> &
329
+ ExpressionOperators & {
330
+ globals: Globals
331
+ extensions: Map<string | object | Function, Record<string, Function>>
332
+ errorMapper: ErrorMapper | null
333
+ }
334
+ >
335
+ ```
336
+
337
+ All fields are optional. You can override any combination of:
338
+
339
+ | Option | Type | Description |
340
+ |---|---|---|
341
+ | `globals` | `Globals` | Compile-time constants and functions available to the expression |
342
+ | `extensions` | `Map<string \| object \| Function, Record<string, Function>>` | Extension methods for `::` operator. Keys: `typeof` string or class/constructor. Values: method bags |
343
+ | `errorMapper` | `ErrorMapper \| null` | Error mapping strategy. Default: auto-detected (V8). `null` disables mapping |
344
+ | `getIdentifierValue` | `(name, globals, data) => unknown` | Custom identifier resolution |
345
+ | `getProperty` | `(obj, key, extension) => unknown` | Custom property access. `extension` is `true` for `::` access |
346
+ | `callFunction` | `(fn, args) => unknown` | Custom function call behavior |
347
+ | `pipe` | `(head, tail) => unknown` | Custom pipe operator behavior |
348
+ | `nonNullAssert` | `(val) => unknown` | Custom non-null assertion for `!` operator |
349
+ | `castToBoolean` | `(val) => boolean` | Custom truthiness rules (affects `if`, `and`, `or`, `not`) |
350
+ | `castToString` | `(val) => string` | Custom string coercion (affects `&` and template literals) |
351
+ | `ensureFunction` | `(val) => Function` | Custom function validation |
352
+ | `ensureObject` | `(val) => object` | Custom object validation (for spread) |
353
+ | `ensureArray` | `(val) => unknown[]` | Custom array validation (for spread) |
354
+ | `unaryOperators` | `Record<op, (val) => unknown>` | Override unary operators |
355
+ | `binaryOperators` | `Record<op, (left, right) => unknown>` | Override binary operators |
356
+ | `logicalOperators` | `Record<op, (left, right) => unknown>` | Override logical operators (args are thunks) |
357
+
358
+ ### Errors
359
+
360
+ All SimplEx errors include the original expression and source location for precise error reporting.
361
+
362
+ **`ExpressionError`** — runtime evaluation error (unknown identifier, type mismatch, invalid operation):
363
+
364
+ ```ts
365
+ import { ExpressionError } from 'simplex-lang'
366
+
367
+ try {
368
+ compile('x + 1')({}) // x is not defined
369
+ } catch (err) {
370
+ if (err instanceof ExpressionError) {
371
+ err.message // "Unknown identifier - x"
372
+ err.expression // "x + 1"
373
+ err.location // { start: { offset, line, column }, end: { ... } }
374
+ }
375
+ }
376
+ ```
377
+
378
+ **`CompileError`** — compilation error (e.g., duplicate `let` bindings):
379
+
380
+ ```ts
381
+ import { CompileError } from 'simplex-lang'
382
+
383
+ compile('let a = 1, a = 2, a') // throws CompileError
384
+ ```
385
+
386
+ **`UnexpectedTypeError`** — runtime type validation error:
387
+
388
+ ```ts
389
+ import { UnexpectedTypeError } from 'simplex-lang'
390
+
391
+ compile('"hello" + 1')() // throws UnexpectedTypeError: expected number
392
+ ```
393
+
394
+ ## Customization
395
+
396
+ Every aspect of SimplEx evaluation can be customized through compile options.
397
+
398
+ **Custom operators** — override or extend any operator:
399
+
400
+ ```ts
401
+ import {
402
+ compile,
403
+ defaultBinaryOperators,
404
+ defaultUnaryOperators
405
+ } from 'simplex-lang'
406
+
407
+ const fn = compile('not -a + b', {
408
+ unaryOperators: {
409
+ ...defaultUnaryOperators,
410
+ not: val => Number(val) + 1 // redefine "not"
411
+ },
412
+ binaryOperators: {
413
+ ...defaultBinaryOperators,
414
+ '+': (a, b) => Number(a) * Number(b) // make "+" multiply
415
+ }
416
+ })
417
+ ```
418
+
419
+ **Custom identifier resolution** — control how variables are looked up:
420
+
421
+ ```ts
422
+ // Use a Map instead of a plain object for globals
423
+ const fn = compile('foo', {
424
+ globals: new Map([['foo', 'bar']]),
425
+ getIdentifierValue(name, globals, data) {
426
+ if (globals.has(name)) return globals.get(name)
427
+ return data[name]
428
+ }
429
+ })
430
+ ```
431
+
432
+ **Custom property access** — intercept or transform property lookups:
433
+
434
+ ```ts
435
+ const fn = compile('a.b', {
436
+ getProperty: (obj, key, extension) => `custom:${String(key)}`
437
+ })
438
+
439
+ fn({ a: { b: 'real' } }) // "custom:b"
440
+ ```
441
+
442
+ **Custom function calls** — wrap or intercept function invocations:
443
+
444
+ ```ts
445
+ const fn = compile('f(1, 2)', {
446
+ globals: { f: (a, b) => a + b },
447
+ callFunction: (fn, args) => {
448
+ if (args === null) return fn()
449
+ return `intercepted:${fn(...args)}`
450
+ }
451
+ })
452
+
453
+ fn() // "intercepted:3"
454
+ ```
455
+
456
+ **Custom pipe** — implement your own pipe semantics:
457
+
458
+ ```ts
459
+ const fn = compile('1 | % + 1', {
460
+ pipe: (head, tail) => {
461
+ let result = head
462
+ for (const t of tail) {
463
+ result = `piped:${t.next(result)}`
464
+ }
465
+ return result
466
+ }
467
+ })
468
+
469
+ fn() // "piped:2"
470
+ ```
471
+
472
+ **Custom boolean coercion** — change what counts as truthy/falsy (affects `if`, `and`, `or`, `not`):
473
+
474
+ ```ts
475
+ const fn = compile('if a then "yes" else "no"', {
476
+ castToBoolean: val => val === 'truthy'
477
+ })
478
+
479
+ fn({ a: 'truthy' }) // "yes"
480
+ fn({ a: true }) // "no" — only the string "truthy" is truthy now
481
+ ```
482
+
483
+ ## Using External Functions
484
+
485
+ SimplEx expressions can call any function you provide via `globals`. This is the primary way to extend the language.
486
+
487
+ **Basic usage — math and utilities:**
488
+
489
+ ```ts
490
+ const fn = compile('round(price * quantity * (1 - discount), 2)', {
22
491
  globals: {
23
- min: Math.min
492
+ round: (val, decimals) => {
493
+ const factor = 10 ** decimals
494
+ return Math.round(val * factor) / factor
495
+ }
24
496
  }
25
497
  })
26
498
 
27
- const result = fn({ a: 2, b: 3 })
499
+ fn({ price: 19.99, quantity: 3, discount: 0.15 }) // 50.97
500
+ ```
28
501
 
29
- console.log(result)
30
- // ↳ 20
502
+ **Function library — provide a set of utilities:**
503
+
504
+ ```ts
505
+ const stdlib = {
506
+ min: Math.min,
507
+ max: Math.max,
508
+ abs: Math.abs,
509
+ round: Math.round,
510
+ floor: Math.floor,
511
+ ceil: Math.ceil,
512
+ lower: s => s.toLowerCase(),
513
+ upper: s => s.toUpperCase(),
514
+ trim: s => s.trim(),
515
+ len: s => s.length,
516
+ includes: (arr, val) => arr.includes(val),
517
+ map: (arr, fn) => arr.map(fn),
518
+ filter: (arr, fn) => arr.filter(fn),
519
+ reduce: (arr, fn, init) => arr.reduce(fn, init),
520
+ keys: obj => Object.keys(obj),
521
+ values: obj => Object.values(obj)
522
+ }
523
+
524
+ const fn = compile('items | filter(%, x => x.active) | map(%, x => x.name) | len(%)', {
525
+ globals: stdlib
526
+ })
527
+
528
+ fn({
529
+ items: [
530
+ { name: 'A', active: true },
531
+ { name: 'B', active: false },
532
+ { name: 'C', active: true }
533
+ ]
534
+ }) // 2
31
535
  ```
32
536
 
33
- <img alt="In the process of development" src="under-construction.png"/>
537
+ **Combining lambdas with currying:**
538
+
539
+ ```ts
540
+ const fn = compile('items | map(%, add(#, 10)) | filter(%, gt(#, 15))', {
541
+ globals: {
542
+ map: (arr, fn) => arr.map(fn),
543
+ filter: (arr, fn) => arr.filter(fn),
544
+ add: (a, b) => a + b,
545
+ gt: (a, b) => a > b
546
+ }
547
+ })
548
+
549
+ fn({ items: [1, 5, 8, 12] }) // [15, 18, 22]
550
+ ```
34
551
 
35
- ## Links
552
+ ## License
36
553
 
37
- - [AST Explorer](https://astexplorer.net/)
554
+ [MIT](LICENSE)