@statedelta-libs/expressions 2.2.0 → 3.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.
- package/README.md +90 -711
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +184 -644
- package/dist/index.d.ts +184 -644
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,779 +5,158 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@statedelta-libs/expressions)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **Scope externo** - Funções puras vêm via scope, não hardcoded
|
|
14
|
-
- **Context externo** - HOCs de continuidade via context (`tryCatch`, `transaction`, etc.)
|
|
15
|
-
- **Accessor customizado** - Suporte a `ctx.get(path)` para objetos especiais
|
|
16
|
-
- **$pipe** - Composição com valor inicial (sintaxe DSL)
|
|
17
|
-
- **$cb** - Callback expressions para HOCs de continuidade
|
|
18
|
-
- **Path syntax** - Suporte a wildcards (`items[*].price`)
|
|
19
|
-
- **Dependency extraction** - Para dirty tracking/reatividade
|
|
20
|
-
- **Conditions nativo** - Condicionais integradas com expressions nos lados
|
|
21
|
-
- **normalize()** - Helper externo para custom transforms (`$query`, `$mapper`, etc.)
|
|
22
|
-
- **resolveBoundaries()** - Resolve boundaries customizados (`$simulate`, `$context`, etc.) com compilação independente
|
|
23
|
-
- **Type-safe** - TypeScript nativo com inferência
|
|
8
|
+
## O que é
|
|
9
|
+
|
|
10
|
+
Um compilador que transforma expressões declarativas em JSON puro em funções JavaScript de alta performance. A entrada é sempre JSON serializável. A saída é sempre uma `(data) => result`.
|
|
11
|
+
|
|
12
|
+
O compilador não conhece nenhuma função — todas vêm via `scope` fornecido pelo consumer. O mesmo motor serve para matemática, queries de game engine, ETL, NoCode, ou qualquer domínio.
|
|
24
13
|
|
|
25
14
|
## Instalação
|
|
26
15
|
|
|
27
16
|
```bash
|
|
28
|
-
npm install @statedelta-libs/expressions
|
|
29
|
-
# ou
|
|
30
17
|
pnpm add @statedelta-libs/expressions
|
|
31
18
|
```
|
|
32
19
|
|
|
33
20
|
## Quick Start
|
|
34
21
|
|
|
35
22
|
```typescript
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
23
|
+
import { ExpressionCompiler } from '@statedelta-libs/expressions';
|
|
24
|
+
|
|
25
|
+
const compiler = new ExpressionCompiler({
|
|
26
|
+
scope: {
|
|
27
|
+
add: (a, b) => a + b,
|
|
28
|
+
multiply: (a, b) => a * b,
|
|
29
|
+
filter: (pred) => (arr) => arr.filter(pred),
|
|
30
|
+
sum: (arr) => arr.reduce((a, b) => a + b, 0),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
46
33
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
34
|
+
// Compila uma vez
|
|
35
|
+
const compiled = compiler.compile({
|
|
49
36
|
$pipe: [
|
|
50
37
|
{ $: "items" },
|
|
51
|
-
{ $fn: "filter", args: [{ $
|
|
52
|
-
{ $fn: "map", args: [{ $fn: "getPrice" }] },
|
|
38
|
+
{ $fn: "filter", args: [{ $arrow: { $: "item.active" }, args: ["item"] }] },
|
|
53
39
|
{ $fn: "sum" }
|
|
54
40
|
]
|
|
55
|
-
}, { scope });
|
|
56
|
-
|
|
57
|
-
// Executar (muito rápido!)
|
|
58
|
-
const data = {
|
|
59
|
-
items: [
|
|
60
|
-
{ name: "A", price: 10, active: true },
|
|
61
|
-
{ name: "B", price: 20, active: false },
|
|
62
|
-
{ name: "C", price: 30, active: true }
|
|
63
|
-
]
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
expr.fn(data); // 40
|
|
67
|
-
expr.deps; // ["items"]
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## API
|
|
71
|
-
|
|
72
|
-
### compile()
|
|
73
|
-
|
|
74
|
-
Compila expressão JSON DSL para função otimizada usando closures.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const { fn, deps, hash } = compile(expression, { scope });
|
|
78
|
-
|
|
79
|
-
fn(data); // Executa
|
|
80
|
-
deps; // Paths que a expressão depende
|
|
81
|
-
hash; // Hash único (para cache)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### compileAST()
|
|
85
|
-
|
|
86
|
-
Compila usando AST + `new Function()`. **Mais rápido na execução**, ideal para expressões executadas muitas vezes.
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
import { compileAST } from '@statedelta-libs/expressions';
|
|
90
|
-
|
|
91
|
-
const { fn, deps, hash } = compileAST(expression, { scope });
|
|
92
|
-
|
|
93
|
-
fn(data); // Executa (mais rápido que compile())
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
**Quando usar cada um:**
|
|
97
|
-
|
|
98
|
-
| Cenário | Recomendação | Por quê |
|
|
99
|
-
|---------|--------------|---------|
|
|
100
|
-
| Execução única | `compile()` | Compilação mais rápida |
|
|
101
|
-
| Poucas execuções (<8x) | `compile()` | Overhead de AST não compensa |
|
|
102
|
-
| Muitas execuções (>8x) | `compileAST()` | Execução ~25-170% mais rápida |
|
|
103
|
-
| Hot path crítico | `compileAST()` | V8 JIT otimiza melhor |
|
|
104
|
-
| Expressões complexas | `compileAST()` | Ganho maior em nested calls |
|
|
105
|
-
|
|
106
|
-
#### useAccessor (compileAST)
|
|
107
|
-
|
|
108
|
-
Para contextos inteligentes (ex: `TickContext`), use `useAccessor: true` para gerar `accessor(path, data)` ao invés de acesso direto:
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
const ctx = new TickContext();
|
|
112
|
-
|
|
113
|
-
const { fn } = compileAST(
|
|
114
|
-
{ $fn: "add", args: [{ $: "hp" }, { $: "mp" }] },
|
|
115
|
-
{
|
|
116
|
-
scope: {
|
|
117
|
-
add,
|
|
118
|
-
accessor: (path) => ctx.get(path) // closure que captura ctx
|
|
119
|
-
},
|
|
120
|
-
useAccessor: true
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
fn(); // usa ctx.get("hp") + ctx.get("mp")
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### lexicalPrefix (compileAST)
|
|
128
|
-
|
|
129
|
-
Para máxima performance em paths específicos, use `lexicalPrefix` para acesso direto via destructuring:
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
const ctx = new TickContext();
|
|
133
|
-
|
|
134
|
-
const { fn } = compileAST(
|
|
135
|
-
{ $fn: "add", args: [{ $: "hp" }, { $: "params.damage" }] },
|
|
136
|
-
{
|
|
137
|
-
scope: {
|
|
138
|
-
add,
|
|
139
|
-
accessor: (path) => ctx.get(path)
|
|
140
|
-
},
|
|
141
|
-
useAccessor: true,
|
|
142
|
-
lexicalPrefix: "params" // params.* vai direto, resto via accessor
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
fn({ params: { damage: 25 } }); // ctx.get("hp") + params.damage
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### evaluate() / evaluateAST()
|
|
150
|
-
|
|
151
|
-
Compila e executa em um passo.
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
const result = evaluate(
|
|
155
|
-
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] },
|
|
156
|
-
{ a: 1, b: 2 },
|
|
157
|
-
{ scope: { add: (a, b) => a + b } }
|
|
158
|
-
);
|
|
159
|
-
// 3
|
|
160
|
-
|
|
161
|
-
// Versão AST
|
|
162
|
-
const result = evaluateAST(expression, data, { scope });
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### extractDeps()
|
|
166
|
-
|
|
167
|
-
Extrai dependências sem compilar.
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
const deps = extractDeps({
|
|
171
|
-
$if: "$isVip",
|
|
172
|
-
then: { $: "price.vip" },
|
|
173
|
-
else: { $: "price.regular" }
|
|
174
41
|
});
|
|
175
|
-
// ["$isVip", "price.vip", "price.regular"]
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### normalize()
|
|
179
|
-
|
|
180
|
-
Normaliza expressões customizadas para DSL puro **antes** da compilação.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
import { normalize, type Transforms } from '@statedelta-libs/expressions';
|
|
184
|
-
|
|
185
|
-
const transforms: Transforms = {
|
|
186
|
-
$query: (node) => ({
|
|
187
|
-
$fn: "__query",
|
|
188
|
-
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
189
|
-
}),
|
|
190
|
-
};
|
|
191
42
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
transforms
|
|
196
|
-
);
|
|
197
|
-
// → { $fn: "__query", args: ["isAttacked", { row: { $: "kingRow" } }] }
|
|
198
|
-
|
|
199
|
-
// Agora pode compilar normalmente
|
|
200
|
-
const { fn } = compile(pure, { scope });
|
|
43
|
+
// Executa milhões de vezes
|
|
44
|
+
compiled.fn({ items: [{ active: true, price: 10 }, { active: false, price: 20 }] });
|
|
45
|
+
compiled.deps; // ["items"]
|
|
201
46
|
```
|
|
202
47
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
### Literal
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
42
|
|
209
|
-
"hello"
|
|
210
|
-
true
|
|
211
|
-
[1, 2, 3]
|
|
212
|
-
{ a: 1 }
|
|
213
|
-
```
|
|
48
|
+
## Os 5 Primitivos
|
|
214
49
|
|
|
215
|
-
|
|
50
|
+
| Primitivo | O que faz | Exemplo DSL | JS equivalente |
|
|
51
|
+
|-----------|-----------|-------------|----------------|
|
|
52
|
+
| `$` | Referência a path | `{ $: "user.name" }` | `data.user.name` |
|
|
53
|
+
| `$fn` | Invocação/referência de função | `{ $fn: "add", args: [...] }` | `add(a, b)` |
|
|
54
|
+
| `$if` | Condicional | `{ $if: cond, then: a, else: b }` | `cond ? a : b` |
|
|
55
|
+
| `$pipe` | Composição left-to-right | `{ $pipe: [x, f, g] }` | `g(f(x))` |
|
|
56
|
+
| `$arrow` | Closure deferida | `{ $arrow: body, args: ["x"] }` | `(x) => body` |
|
|
216
57
|
|
|
217
|
-
|
|
218
|
-
{ $: "user.name" } // Acessa user.name
|
|
219
|
-
{ $: "items[0].price" } // Acessa índice
|
|
220
|
-
{ $: "items[*].price" } // Wildcard → [10, 20, 30]
|
|
221
|
-
```
|
|
58
|
+
Tudo 100% JSON-serializável. Funções nunca aparecem no DSL — são referenciadas por nome e resolvidas contra o scope em runtime.
|
|
222
59
|
|
|
223
|
-
|
|
60
|
+
## Dois Modos de Compilação
|
|
224
61
|
|
|
225
62
|
```typescript
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
then: "adult",
|
|
229
|
-
else: "minor"
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Shorthand
|
|
233
|
-
{ $if: "$isVip", then: 0.2, else: 0 }
|
|
234
|
-
{ $if: "!$isBlocked", then: "allowed" }
|
|
63
|
+
compiler.compile(expr); // closures — compilação rápida, ~9-20M ops/s
|
|
64
|
+
compiler.jit(expr); // JIT — compilação lenta, ~25-27M ops/s de execução
|
|
235
65
|
```
|
|
236
66
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Composição com valor inicial - o valor passa por cada função em sequência.
|
|
67
|
+
**Closures** compõe funções JavaScript aninhadas. Ideal para expressões executadas poucas vezes ou ambientes com CSP restritivo.
|
|
240
68
|
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
$pipe: [
|
|
244
|
-
{ $: "items" }, // Valor inicial
|
|
245
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }, // Curried filter
|
|
246
|
-
{ $fn: "map", args: [{ $fn: "getPrice" }] }, // Curried map
|
|
247
|
-
{ $fn: "sum" } // Referência à função
|
|
248
|
-
]
|
|
249
|
-
}
|
|
250
|
-
```
|
|
69
|
+
**JIT** gera código JavaScript via AST e `new Function()`. Ideal para hot paths executados muitas vezes. Break-even em ~8 execuções.
|
|
251
70
|
|
|
252
|
-
|
|
71
|
+
## Extensibilidade
|
|
253
72
|
|
|
254
|
-
|
|
73
|
+
### Scope — funções disponíveis
|
|
255
74
|
|
|
256
75
|
```typescript
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Com args vazio: chama função sem argumentos
|
|
266
|
-
{ $fn: "getTimestamp", args: [] }
|
|
267
|
-
// → scope.getTimestamp()
|
|
268
|
-
|
|
269
|
-
// Passando função como argumento (referência via $fn)
|
|
270
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
271
|
-
// → scope.filter(scope.isActive)
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Condition
|
|
275
|
-
|
|
276
|
-
Condicionais compiladas internamente. Ambos os lados aceitam qualquer expressão:
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// Ref vs literal (básico)
|
|
280
|
-
{ left: { $: "user.age" }, op: "gte", right: 18 }
|
|
281
|
-
|
|
282
|
-
// Ref vs Ref
|
|
283
|
-
{ left: { $: "price" }, op: "lte", right: { $: "budget" } }
|
|
284
|
-
|
|
285
|
-
// $fn nos lados
|
|
286
|
-
{ left: { $fn: "add", args: [{ $: "a" }, { $: "b" }] }, op: "gt", right: 100 }
|
|
287
|
-
|
|
288
|
-
// $pipe no lado
|
|
289
|
-
{ left: { $pipe: [{ $: "name" }, { $fn: "upper" }] }, op: "eq", right: "ADMIN" }
|
|
290
|
-
|
|
291
|
-
// Condition group
|
|
292
|
-
{
|
|
293
|
-
logic: "AND",
|
|
294
|
-
conditions: [
|
|
295
|
-
{ left: { $: "active" }, op: "eq", right: true },
|
|
296
|
-
{ left: { $fn: "len", args: [{ $: "items" }] }, op: "gte", right: 3 }
|
|
297
|
-
]
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Operadores:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`, `notContains`, `exists`, `notExists`, `matches`, `notMatches`, `startsWith`, `endsWith`
|
|
302
|
-
|
|
303
|
-
### Callback ($cb)
|
|
304
|
-
|
|
305
|
-
HOC de continuidade - executa body dentro de um context function.
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
// Básico - tryCatch
|
|
309
|
-
{ $cb: "tryCatch", body: { $fn: "query" } }
|
|
310
|
-
|
|
311
|
-
// Com parâmetros
|
|
312
|
-
{ $cb: "transaction", body: { $fn: "save" }, params: { isolation: "serializable" } }
|
|
313
|
-
|
|
314
|
-
// Aninhado
|
|
315
|
-
{
|
|
316
|
-
$cb: "tryCatch",
|
|
317
|
-
body: {
|
|
318
|
-
$cb: "transaction",
|
|
319
|
-
body: { $fn: "save" }
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Context
|
|
325
|
-
|
|
326
|
-
O context define HOCs disponíveis para `$cb`. Diferente de scope (funções puras), context functions controlam a execução do body:
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
import type { Context, ContextFn } from '@statedelta-libs/expressions';
|
|
330
|
-
|
|
331
|
-
const tryCatch: ContextFn = (cb, data, get, params) => {
|
|
332
|
-
try {
|
|
333
|
-
return cb(data, get);
|
|
334
|
-
} catch {
|
|
335
|
-
return params?.fallback ?? null;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
const transaction: ContextFn = (cb, data, get, params) => {
|
|
340
|
-
const tx = db.beginTransaction(params);
|
|
341
|
-
try {
|
|
342
|
-
const result = cb({ ...data, tx }, get);
|
|
343
|
-
tx.commit();
|
|
344
|
-
return result;
|
|
345
|
-
} catch (e) {
|
|
346
|
-
tx.rollback();
|
|
347
|
-
throw e;
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Context function pode ignorar o cb e retornar outro valor
|
|
352
|
-
const cached: ContextFn = (cb, data, get, params) => {
|
|
353
|
-
if (cache.has(params?.key)) {
|
|
354
|
-
return cache.get(params.key); // Não executa cb
|
|
355
|
-
}
|
|
356
|
-
return cb(data, get);
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
const context: Context = { tryCatch, transaction, cached };
|
|
360
|
-
|
|
361
|
-
compile(expression, { scope, context });
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
**Parâmetros do ContextFn:**
|
|
365
|
-
|
|
366
|
-
| Parâmetro | Tipo | Descrição |
|
|
367
|
-
|-----------|------|-----------|
|
|
368
|
-
| `cb` | `(data, get) => R` | Body compilado como callback |
|
|
369
|
-
| `data` | `T` | Dados do runtime |
|
|
370
|
-
| `get` | `(path) => unknown` | Resolver de paths |
|
|
371
|
-
| `params` | `unknown` | Parâmetros do `$cb.params` |
|
|
372
|
-
|
|
373
|
-
## Scope
|
|
374
|
-
|
|
375
|
-
O scope define quais funções estão disponíveis para `$fn`:
|
|
376
|
-
|
|
377
|
-
```typescript
|
|
378
|
-
import * as R from '@statedelta-libs/operators';
|
|
379
|
-
|
|
380
|
-
// Todas as funções do operators
|
|
381
|
-
const scope = R;
|
|
382
|
-
|
|
383
|
-
// Ou seleção específica
|
|
384
|
-
const scope = {
|
|
385
|
-
add: R.add,
|
|
386
|
-
filter: R.filter,
|
|
387
|
-
map: R.map,
|
|
388
|
-
sum: R.sum,
|
|
389
|
-
// Funções custom (predicados, mappers, etc.)
|
|
390
|
-
double: (n) => n * 2,
|
|
391
|
-
isActive: (item) => item.active,
|
|
392
|
-
getPrice: (item) => item.price,
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const { fn } = compile(expression, { scope });
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
## Custom Transforms (normalize)
|
|
399
|
-
|
|
400
|
-
Para tipos de expressão customizados (`$query`, `$mapper`, etc.), use `normalize()` para convertê-los em DSL puro **antes** da compilação:
|
|
401
|
-
|
|
402
|
-
```typescript
|
|
403
|
-
import { normalize, compile, type Transforms, type Expression } from '@statedelta-libs/expressions';
|
|
404
|
-
|
|
405
|
-
// Define transforms
|
|
406
|
-
const transforms: Transforms = {
|
|
407
|
-
$query: (node) => ({
|
|
408
|
-
$fn: "__query",
|
|
409
|
-
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
410
|
-
}),
|
|
411
|
-
$mapper: (node) => ({
|
|
412
|
-
$fn: "__mapper",
|
|
413
|
-
args: [node.$mapper as string],
|
|
414
|
-
}),
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Define scope com as funções de runtime
|
|
418
|
-
const scope = {
|
|
419
|
-
__query: (name, params) => queryEngine.run(name, params),
|
|
420
|
-
__mapper: (name) => mapperRegistry.get(name),
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
// 1. Normalize (converte para DSL puro)
|
|
424
|
-
const pure = normalize(
|
|
425
|
-
{ $query: "isAttacked", params: { row: { $: "kingRow" } } },
|
|
426
|
-
transforms
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
// 2. Compile (DSL puro)
|
|
430
|
-
const { fn } = compile(pure, { scope });
|
|
431
|
-
|
|
432
|
-
// 3. Execute
|
|
433
|
-
fn({ kingRow: 0 });
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
### Aninhamento
|
|
437
|
-
|
|
438
|
-
O `normalize` percorre toda a árvore, então transforms funcionam em qualquer posição:
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
normalize(
|
|
442
|
-
{
|
|
443
|
-
$if: {
|
|
444
|
-
left: { $query: "isAttacked", params: { row: 0 } },
|
|
445
|
-
op: "eq",
|
|
446
|
-
right: true
|
|
76
|
+
const compiler = new ExpressionCompiler({
|
|
77
|
+
scope: {
|
|
78
|
+
add: (a, b) => a + b,
|
|
79
|
+
filter: (pred) => (arr) => arr.filter(pred),
|
|
80
|
+
tryCatch: (arrow, params) => {
|
|
81
|
+
try { return arrow(); }
|
|
82
|
+
catch { return params?.fallback ?? null; }
|
|
447
83
|
},
|
|
448
|
-
then: "check",
|
|
449
|
-
else: "safe"
|
|
450
84
|
},
|
|
451
|
-
|
|
452
|
-
);
|
|
85
|
+
});
|
|
453
86
|
```
|
|
454
87
|
|
|
455
|
-
###
|
|
88
|
+
### Normalize — DSL customizado
|
|
456
89
|
|
|
457
|
-
|
|
90
|
+
Transforma sugar syntax em DSL puro **antes** da compilação. Para quando o resultado é expression DSL válido.
|
|
458
91
|
|
|
459
92
|
```typescript
|
|
460
|
-
const
|
|
461
|
-
$
|
|
93
|
+
const transforms = {
|
|
94
|
+
$double: (node) => ({ $fn: "multiply", args: [node.$double, 2] }),
|
|
462
95
|
};
|
|
463
96
|
|
|
464
|
-
normalize({ $
|
|
465
|
-
|
|
97
|
+
const pure = compiler.normalize({ $double: { $: "value" } }, transforms);
|
|
98
|
+
compiler.compile(pure);
|
|
466
99
|
```
|
|
467
100
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
Para DSL customizados que precisam de **compilação independente** dos slots internos, use `resolveBoundaries()`. Diferente de `normalize()` (que apenas transforma), boundaries param o fluxo de compilação e delegam para um resolver externo.
|
|
471
|
-
|
|
472
|
-
### Conceito
|
|
101
|
+
### Boundaries — compiladores externos
|
|
473
102
|
|
|
474
|
-
|
|
475
|
-
1. **Para** o fluxo normal de compilação
|
|
476
|
-
2. **Delega** para um resolver customizado
|
|
477
|
-
3. O resolver **compila slots internos** independentemente
|
|
478
|
-
4. **Substitui** por uma referência no scope
|
|
479
|
-
|
|
480
|
-
```
|
|
481
|
-
DSL "rico" DSL puro
|
|
482
|
-
{ $simulate: {...} } → { $fn: "__simulate_0", args: [] }
|
|
483
|
-
+ scope["__simulate_0"] = fn compilada
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### Uso básico
|
|
103
|
+
Intercepta nós **durante** a compilação e terceiriza para outro algoritmo. Para quando o nó precisa de um compilador completamente diferente.
|
|
487
104
|
|
|
488
105
|
```typescript
|
|
489
|
-
import {
|
|
490
|
-
|
|
491
|
-
const resolvers: BoundaryResolvers = {
|
|
492
|
-
$context: (node, { compile: compileFn, genId, scope }) => {
|
|
493
|
-
const id = `__context_${genId()}`;
|
|
494
|
-
const body = (node.$context as { body: Expression }).body;
|
|
495
|
-
|
|
496
|
-
// Compila o body internamente
|
|
497
|
-
const bodyFn = compileFn(body, { scope }).fn;
|
|
498
|
-
|
|
499
|
-
// Retorna função com lifecycle begin/end
|
|
500
|
-
const contextFn = () => (data: unknown) => {
|
|
501
|
-
console.log("begin");
|
|
502
|
-
try {
|
|
503
|
-
return bodyFn(data);
|
|
504
|
-
} finally {
|
|
505
|
-
console.log("end");
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
expr: { $fn: id, args: [] },
|
|
511
|
-
scopeEntry: [id, contextFn],
|
|
512
|
-
};
|
|
513
|
-
},
|
|
514
|
-
};
|
|
106
|
+
import type { BoundaryDef } from '@statedelta-libs/expressions';
|
|
515
107
|
|
|
516
|
-
//
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// 2. Compila DSL puro resultante
|
|
523
|
-
const { fn } = compile(expr, { scope });
|
|
524
|
-
|
|
525
|
-
// 3. Executa
|
|
526
|
-
const contextFn = fn({}); // Retorna a função com lifecycle
|
|
527
|
-
const result = contextFn({ a: 1, b: 2 }); // "begin" → 3 → "end"
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### Caso de uso: $simulate com múltiplos slots
|
|
531
|
-
|
|
532
|
-
```typescript
|
|
533
|
-
interface SimulateNode {
|
|
534
|
-
$simulate: {
|
|
535
|
-
effects: Expression[];
|
|
536
|
-
query: Expression;
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const resolvers: BoundaryResolvers = {
|
|
541
|
-
$simulate: (node, { compile: compileFn, genId, scope }) => {
|
|
542
|
-
const id = `__simulate_${genId()}`;
|
|
543
|
-
const sim = (node as unknown as SimulateNode).$simulate;
|
|
544
|
-
|
|
545
|
-
// Compila cada slot independentemente
|
|
546
|
-
const effectFns = sim.effects.map((e) => compileFn(e, { scope }).fn);
|
|
547
|
-
const queryFn = compileFn(sim.query, { scope }).fn;
|
|
548
|
-
|
|
549
|
-
// Cria função que orquestra a execução
|
|
550
|
-
const simulateFn = () => (data: unknown) => {
|
|
551
|
-
const effects = effectFns.map((fn) => fn(data));
|
|
552
|
-
const query = queryFn(data);
|
|
553
|
-
return { effects, query };
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
expr: { $fn: id, args: [] },
|
|
558
|
-
scopeEntry: [id, simulateFn],
|
|
559
|
-
};
|
|
560
|
-
},
|
|
108
|
+
// $raw — passthrough, nada é compilado
|
|
109
|
+
const rawBoundary: BoundaryDef = {
|
|
110
|
+
check: (node) => "$raw" in node,
|
|
111
|
+
handle: (node) => () => node.$raw,
|
|
561
112
|
};
|
|
562
113
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
],
|
|
570
|
-
query: { $fn: "isAlive", args: [{ $: "hp" }] },
|
|
571
|
-
},
|
|
114
|
+
// $rules — DSL estrangeiro com compilador próprio
|
|
115
|
+
const rulesBoundary: BoundaryDef = {
|
|
116
|
+
check: (node) => "$rules" in node,
|
|
117
|
+
handle: (node) => {
|
|
118
|
+
const compiled = ruleEngine.compile(node.$rules);
|
|
119
|
+
return (data) => compiled.evaluate(data);
|
|
572
120
|
},
|
|
573
|
-
|
|
574
|
-
);
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### Contexto do resolver
|
|
578
|
-
|
|
579
|
-
O resolver recebe um contexto com:
|
|
580
|
-
|
|
581
|
-
| Propriedade | Tipo | Descrição |
|
|
582
|
-
|-------------|------|-----------|
|
|
583
|
-
| `compile` | `typeof compile` | Mesmo compilador do fluxo principal |
|
|
584
|
-
| `genId` | `() => string` | Gerador de IDs únicos |
|
|
585
|
-
| `scope` | `Scope` | Scope atual (read-only) |
|
|
586
|
-
| `options` | `CompileOptions` | Opções de compilação |
|
|
587
|
-
|
|
588
|
-
### ID Generator customizado
|
|
589
|
-
|
|
590
|
-
Por padrão, IDs são gerados como `"0"`, `"1"`, `"2"`... Para IDs únicos globais, passe um `genId` customizado:
|
|
121
|
+
};
|
|
591
122
|
|
|
592
|
-
|
|
593
|
-
|
|
123
|
+
const compiler = new ExpressionCompiler({
|
|
124
|
+
scope,
|
|
125
|
+
boundaries: [rawBoundary, rulesBoundary],
|
|
126
|
+
});
|
|
594
127
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
128
|
+
// $rules é interceptado pelo boundary, compilado pelo ruleEngine
|
|
129
|
+
compiler.compile({
|
|
130
|
+
$fn: "add",
|
|
131
|
+
args: [{ $: "base" }, { $rules: { when: "vip", then: 20 } }]
|
|
599
132
|
});
|
|
600
133
|
```
|
|
601
134
|
|
|
602
|
-
|
|
135
|
+
O handler é uma closure auto-suficiente — captura o que precisa (outros compiladores, databases, etc.) por fora. Zero overhead quando não há boundaries registrados.
|
|
603
136
|
|
|
604
|
-
| | `normalize()` | `
|
|
137
|
+
| | `normalize()` | `BoundaryDef` |
|
|
605
138
|
|---|---|---|
|
|
606
|
-
| **
|
|
607
|
-
| **Retorno** | `Expression` | `
|
|
608
|
-
| **
|
|
609
|
-
| **Acumula scope** | Não | Sim |
|
|
610
|
-
| **Uso típico** | `$query` → `$fn` | `$simulate`, `$context` |
|
|
139
|
+
| **Quando roda** | Antes da compilação | Durante a compilação (no walk) |
|
|
140
|
+
| **Retorno** | `Expression` (DSL puro) | `CompiledFn` (função pronta) |
|
|
141
|
+
| **Uso típico** | Sugar syntax | DSL estrangeiro, `$raw`, rule engines |
|
|
611
142
|
|
|
612
|
-
|
|
613
|
-
- Compilar múltiplos slots independentemente
|
|
614
|
-
- Controlar ordem/momento de execução
|
|
615
|
-
- Adicionar lógica de lifecycle (begin/end, try/finally)
|
|
616
|
-
- DSL completamente diferente internamente
|
|
617
|
-
|
|
618
|
-
## Builders
|
|
619
|
-
|
|
620
|
-
Funções declarativas para construir expressões:
|
|
143
|
+
### Accessor — objetos inteligentes
|
|
621
144
|
|
|
622
145
|
```typescript
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
// Path reference
|
|
628
|
-
$("player.hp") // { $: "player.hp" }
|
|
629
|
-
|
|
630
|
-
// Function call
|
|
631
|
-
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
632
|
-
$fn("sum") // { $fn: "sum" }
|
|
633
|
-
|
|
634
|
-
// Conditional
|
|
635
|
-
$if($("isVip"), 0.2, 0) // { $if: ..., then: 0.2, else: 0 }
|
|
636
|
-
|
|
637
|
-
// Pipe
|
|
638
|
-
$pipe(
|
|
639
|
-
$("items"),
|
|
640
|
-
$fn("filter", [$fn("isActive")]),
|
|
641
|
-
$fn("sum")
|
|
642
|
-
)
|
|
643
|
-
|
|
644
|
-
// Condition
|
|
645
|
-
$cond($("age"), "gte", 18) // { left: ..., op: "gte", right: 18 }
|
|
646
|
-
|
|
647
|
-
// Callback (context)
|
|
648
|
-
$cb("tryCatch", $fn("query")) // { $cb: "tryCatch", body: {...} }
|
|
649
|
-
$cb("transaction", $fn("save"), { isolation: "serializable" })
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
## TypeScript
|
|
653
|
-
|
|
654
|
-
```typescript
|
|
655
|
-
import type {
|
|
656
|
-
Expression,
|
|
657
|
-
CompiledExpression,
|
|
658
|
-
RefExpr,
|
|
659
|
-
ConditionalExpr,
|
|
660
|
-
FnExpr,
|
|
661
|
-
PipeExpr,
|
|
662
|
-
CbExpr,
|
|
663
|
-
Condition,
|
|
664
|
-
ConditionGroup,
|
|
665
|
-
ConditionExpr,
|
|
666
|
-
ConditionOp,
|
|
667
|
-
Scope,
|
|
668
|
-
Context,
|
|
669
|
-
ContextFn,
|
|
670
|
-
PathGetterFn,
|
|
671
|
-
CompileOptions,
|
|
672
|
-
AccessorFn,
|
|
673
|
-
TransformFn,
|
|
674
|
-
Transforms,
|
|
675
|
-
// Boundaries
|
|
676
|
-
BoundaryResolver,
|
|
677
|
-
BoundaryResolvers,
|
|
678
|
-
ResolverContext,
|
|
679
|
-
ResolverResult,
|
|
680
|
-
ResolveBoundariesOptions,
|
|
681
|
-
ResolveBoundariesResult,
|
|
682
|
-
IdGenerator,
|
|
683
|
-
} from '@statedelta-libs/expressions';
|
|
684
|
-
|
|
685
|
-
// Type guards
|
|
686
|
-
import {
|
|
687
|
-
isRef,
|
|
688
|
-
isConditional,
|
|
689
|
-
isFn,
|
|
690
|
-
isPipe,
|
|
691
|
-
isCb,
|
|
692
|
-
isCondition,
|
|
693
|
-
isLiteral,
|
|
694
|
-
} from '@statedelta-libs/expressions';
|
|
146
|
+
const compiler = new ExpressionCompiler({
|
|
147
|
+
scope,
|
|
148
|
+
accessor: (path, ctx) => ctx.get(path), // ex: TickContext, reactive store
|
|
149
|
+
});
|
|
695
150
|
```
|
|
696
151
|
|
|
697
|
-
##
|
|
698
|
-
|
|
699
|
-
### Comparação Execução
|
|
700
|
-
|
|
701
|
-
| Cenário | Closures | AST | Ganho AST |
|
|
702
|
-
|---------|----------|-----|-----------|
|
|
703
|
-
| fn nested | 9M ops/s | 25M ops/s | **+169%** |
|
|
704
|
-
| ref 4 níveis | 16M ops/s | 27M ops/s | **+69%** |
|
|
705
|
-
| conditional nested | 20M ops/s | 25M ops/s | **+25%** |
|
|
706
|
-
|
|
707
|
-
**Break-even:** ~8 execuções - após isso, `compileAST()` compensa o tempo extra de compilação.
|
|
708
|
-
|
|
709
|
-
## Segurança
|
|
710
|
-
|
|
711
|
-
Ambas as abordagens são **seguras contra injeção de código**:
|
|
712
|
-
|
|
713
|
-
| Método | Proteção |
|
|
714
|
-
|--------|----------|
|
|
715
|
-
| `compile()` | Não gera código string - apenas compõe funções |
|
|
716
|
-
| `compileAST()` | Usa destructuring que valida identificadores automaticamente |
|
|
717
|
-
|
|
718
|
-
Funções só são acessíveis se existirem no `scope` fornecido pelo desenvolvedor.
|
|
719
|
-
|
|
720
|
-
## Migração
|
|
721
|
-
|
|
722
|
-
### v1.1 → v1.2
|
|
723
|
-
|
|
724
|
-
1. **DSL Puro**: `FnExpr.args` agora é `Expression[]` (removido suporte a callbacks inline). Funções devem ser passadas via scope:
|
|
725
|
-
```typescript
|
|
726
|
-
// Antes (callbacks inline - não funciona mais)
|
|
727
|
-
{ $fn: "filter", args: [(item) => item.active] }
|
|
728
|
-
|
|
729
|
-
// Depois (referência via scope)
|
|
730
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
731
|
-
// Com scope: { isActive: (item) => item.active, filter: ... }
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
2. **Removidos**: `CallbackFn`, `FnArg` types
|
|
735
|
-
|
|
736
|
-
3. **normalize()**: Custom transforms agora são externos via `normalize()`, não mais dentro de `CompileOptions`:
|
|
737
|
-
```typescript
|
|
738
|
-
// Antes
|
|
739
|
-
compile(expr, { scope, transforms });
|
|
740
|
-
|
|
741
|
-
// Depois
|
|
742
|
-
const pure = normalize(expr, transforms);
|
|
743
|
-
compile(pure, { scope });
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
4. **Novos tipos**: `Transforms` (alias para `Record<string, TransformFn>`)
|
|
747
|
-
|
|
748
|
-
### v1.2 → v2.0
|
|
749
|
-
|
|
750
|
-
1. **Novo tipo `$cb`**: HOC de continuidade para wrapping contextual:
|
|
751
|
-
```typescript
|
|
752
|
-
// tryCatch, transaction, cached, etc.
|
|
753
|
-
{ $cb: "tryCatch", body: { $fn: "query" }, params: { fallback: [] } }
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
2. **Novo `context` em CompileOptions**: Separado de `scope` para HOCs:
|
|
757
|
-
```typescript
|
|
758
|
-
compile(expr, { scope, context });
|
|
759
|
-
compileAST(expr, { scope, context });
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
3. **Novos tipos**: `CbExpr`, `Context`, `ContextFn`, `PathGetterFn`
|
|
763
|
-
|
|
764
|
-
4. **Novo type guard**: `isCb()`
|
|
765
|
-
|
|
766
|
-
5. **Novo builder**: `$cb(name, body, params?)`
|
|
767
|
-
|
|
768
|
-
6. **Novo extractor**: `extractContextFns()`
|
|
769
|
-
|
|
770
|
-
7. **Novo `resolveBoundaries()`**: Para DSL customizados com compilação independente de slots:
|
|
771
|
-
```typescript
|
|
772
|
-
const { expr, scope } = resolveBoundaries(richExpr, {
|
|
773
|
-
resolvers: { $simulate: (node, ctx) => ... },
|
|
774
|
-
scope: baseScope,
|
|
775
|
-
genId: () => nanoid(), // opcional
|
|
776
|
-
});
|
|
777
|
-
compile(expr, { scope });
|
|
778
|
-
```
|
|
152
|
+
## Documentação
|
|
779
153
|
|
|
780
|
-
|
|
154
|
+
| Doc | Conteúdo |
|
|
155
|
+
|-----|----------|
|
|
156
|
+
| [docs/EXPRESSIONS-API.md](docs/EXPRESSIONS-API.md) | Referência completa da API pública |
|
|
157
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Visão geral da infraestrutura |
|
|
158
|
+
| [docs/COMPILE.md](docs/COMPILE.md) | Internals do pipeline closures |
|
|
159
|
+
| [docs/JIT.md](docs/JIT.md) | Internals do pipeline JIT |
|
|
781
160
|
|
|
782
161
|
## Licença
|
|
783
162
|
|