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