@statedelta-libs/expressions 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,868 +5,227 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@statedelta-libs/expressions.svg)](https://www.npmjs.com/package/@statedelta-libs/expressions)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- ## Características
9
-
10
- - **DSL Puro** - 100% JSON-serializável, sem callbacks inline
11
- - **Compilação** - Compila uma vez, executa milhões de vezes
12
- - **Alta performance** - ~25-30M ops/s após compilação
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 { compile } from '@statedelta-libs/expressions';
38
-
39
- // Define o scope com funções disponíveis
40
- const scope = {
41
- filter: (pred) => (arr) => arr?.filter(pred),
42
- map: (fn) => (arr) => arr?.map(fn),
43
- sum: (arr) => arr?.reduce((a, b) => a + b, 0),
44
- isActive: (item) => item.active,
45
- getPrice: (item) => item.price,
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
- // Compilar expressão com $pipe (DSL puro)
49
- const expr = compile({
34
+ // Compila uma vez
35
+ const compiled = compiler.compile({
50
36
  $pipe: [
51
37
  { $: "items" },
52
- { $fn: "filter", args: [{ $fn: "isActive" }] },
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
- };
41
+ });
66
42
 
67
- expr.fn(data); // 40
68
- expr.deps; // ["items"]
43
+ // Executa milhões de vezes
44
+ compiled.fn({ items: [{ active: true, price: 10 }, { active: false, price: 20 }] });
45
+ compiled.deps; // ["items"]
69
46
  ```
70
47
 
71
- ## API
72
-
73
- ### compile()
74
-
75
- Compila expressão JSON DSL para função otimizada usando closures.
48
+ ## Os 5 Primitivos
76
49
 
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
- ```
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` |
84
57
 
85
- ### compileAST()
58
+ Tudo 100% JSON-serializável. Funções nunca aparecem no DSL — são referenciadas por nome e resolvidas contra o scope em runtime.
86
59
 
87
- Compila usando AST + `new Function()`. **Mais rápido na execução**, ideal para expressões executadas muitas vezes.
60
+ ## Dois Modos de Compilação
88
61
 
89
62
  ```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())
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
95
65
  ```
96
66
 
97
- **Quando usar cada um:**
67
+ **Closures** compõe funções JavaScript aninhadas. Ideal para expressões executadas poucas vezes ou ambientes com CSP restritivo.
98
68
 
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 |
69
+ **JIT** gera código JavaScript via AST e `new Function()`. Ideal para hot paths executados muitas vezes. Break-even em ~8 execuções.
106
70
 
107
- #### useAccessor (compileAST)
71
+ ## Extensibilidade
108
72
 
109
- Para contextos inteligentes (ex: `TickContext`), use `useAccessor: true` para gerar `accessor(path, data)` ao invés de acesso direto:
73
+ ### Scope funções disponíveis
110
74
 
111
75
  ```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
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; }
120
83
  },
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" }
84
+ },
175
85
  });
176
- // ["$isVip", "price.vip", "price.regular"]
177
86
  ```
178
87
 
179
- ### normalize()
88
+ ### Normalize — DSL customizado
180
89
 
181
- Normaliza expressões customizadas para DSL puro **antes** da compilação.
90
+ Transforma sugar syntax em DSL puro **antes** da compilação. Para quando o resultado é expression DSL válido.
182
91
 
183
92
  ```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
- }),
93
+ const transforms = {
94
+ $double: (node) => ({ $fn: "multiply", args: [node.$double, 2] }),
191
95
  };
192
96
 
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 });
97
+ const pure = compiler.normalize({ $double: { $: "value" } }, transforms);
98
+ compiler.compile(pure);
202
99
  ```
203
100
 
204
- ## Tipos de Expressão
205
-
206
- ### Literal
101
+ ### Boundaries compiladores externos
207
102
 
208
- ```typescript
209
- 42
210
- "hello"
211
- true
212
- [1, 2, 3]
213
- { a: 1 }
214
- ```
215
-
216
- ### Reference ($)
103
+ Intercepta nós **durante** a compilação e terceiriza para outro algoritmo. Para quando o nó precisa de um compilador completamente diferente.
217
104
 
218
105
  ```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
-
226
- ```typescript
227
- {
228
- $if: { left: { $: "age" }, op: "gte", right: 18 },
229
- then: "adult",
230
- else: "minor"
231
- }
232
-
233
- // Shorthand
234
- { $if: "$isVip", then: 0.2, else: 0 }
235
- { $if: "!$isBlocked", then: "allowed" }
236
- ```
237
-
238
- ### Pipe ($pipe)
239
-
240
- Composição com valor inicial - o valor passa por cada função em sequência.
241
-
242
- ```typescript
243
- {
244
- $pipe: [
245
- { $: "items" }, // Valor inicial
246
- { $fn: "filter", args: [{ $fn: "isActive" }] }, // Curried filter
247
- { $fn: "map", args: [{ $fn: "getPrice" }] }, // Curried map
248
- { $fn: "sum" } // Referência à função
249
- ]
250
- }
251
- ```
252
-
253
- ### Function Call ($fn)
254
-
255
- Chama função do scope com argumentos compilados.
256
-
257
- ```typescript
258
- // Com args: chama a função
259
- { $fn: "add", args: [{ $: "a" }, { $: "b" }] }
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)
273
- ```
274
-
275
- ### Condition
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 }
282
-
283
- // Ref vs Ref
284
- { left: { $: "price" }, op: "lte", right: { $: "budget" } }
285
-
286
- // $fn nos lados
287
- { left: { $fn: "add", args: [{ $: "a" }, { $: "b" }] }, op: "gt", right: 100 }
288
-
289
- // $pipe no lado
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.
307
-
308
- ```typescript
309
- // Básico - tryCatch
310
- { $cb: "tryCatch", body: { $fn: "query" } }
311
-
312
- // Com parâmetros
313
- { $cb: "transaction", body: { $fn: "save" }, params: { isolation: "serializable" } }
314
-
315
- // Aninhado
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
106
+ import type { BoundaryDef } from '@statedelta-libs/expressions';
341
107
 
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
- }
108
+ // $raw — passthrough, nada é compilado
109
+ const rawBoundary: BoundaryDef = {
110
+ check: (node) => "$raw" in node,
111
+ handle: (node) => () => node.$raw,
362
112
  };
363
113
 
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 });
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);
120
+ },
395
121
  };
396
122
 
397
- const context: Context = { tryCatch, transaction, cached, simulate };
123
+ const compiler = new ExpressionCompiler({
124
+ scope,
125
+ boundaries: [rawBoundary, rulesBoundary],
126
+ });
398
127
 
399
- compile(expression, { scope, context });
128
+ // $rules é interceptado pelo boundary, compilado pelo ruleEngine
129
+ compiler.compile({
130
+ $fn: "add",
131
+ args: [{ $: "base" }, { $rules: { when: "vip", then: 20 } }]
132
+ });
400
133
  ```
401
134
 
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
- };
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.
434
136
 
435
- const { fn } = compile(expression, { scope });
436
- ```
137
+ | | `normalize()` | `BoundaryDef` |
138
+ |---|---|---|
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 |
437
142
 
438
- ## Custom Transforms (normalize)
143
+ ### Handlers services com contexto
439
144
 
440
- Para tipos de expressão customizados (`$query`, `$mapper`, etc.), use `normalize()` para convertê-los em DSL puro **antes** da compilação:
145
+ O `scope` é para funções puras e stateless. Quando o consumer precisa de **services inteligentes** que acessam o sistema (accessor, outros handlers, o compilador, o scope), usa `handlers`.
441
146
 
442
147
  ```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
- };
148
+ import type { HandlerContext } from '@statedelta-libs/expressions';
456
149
 
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
150
+ const compiler = new ExpressionCompiler({
151
+ scope: { add: (a, b) => a + b },
152
+ handlers: {
153
+ query: {
154
+ find(key: string) {
155
+ return db.find(key);
156
+ },
157
+ findAll() {
158
+ // chamar outro handler
159
+ const valid = this.handlers.validation.check("all");
160
+ return valid ? db.findAll() : [];
161
+ },
162
+ },
163
+ validation: {
164
+ check(value: unknown) {
165
+ // chamar scope fn
166
+ return this.scope.add(value != null ? 1 : 0, 0) > 0;
167
+ },
487
168
  },
488
- then: "check",
489
- else: "safe"
490
169
  },
491
- transforms
492
- );
493
- ```
494
-
495
- ### Proteção contra loops
496
-
497
- Se um transform retornar objeto com a mesma chave, erro é lançado:
498
-
499
- ```typescript
500
- const badTransforms = {
501
- $loop: (node) => ({ $loop: node.$loop }), // mesma chave!
502
- };
503
-
504
- normalize({ $loop: "test" }, badTransforms);
505
- // Error: Transform "$loop" returned object with same key — infinite loop
170
+ });
506
171
  ```
507
172
 
508
- ## Boundaries (resolveBoundaries)
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.
511
-
512
- ### Conceito
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
173
+ Handlers são invocados via `$fn` com sintaxe `"namespace:method"`:
519
174
 
175
+ ```json
176
+ { "$fn": "query:find", "args": [{ "$": "userId" }] }
177
+ { "$fn": "validation:check", "args": [{ "$": "value" }] }
520
178
  ```
521
- DSL "rico" DSL puro
522
- { $simulate: {...} } → { $fn: "__simulate_0", args: [] }
523
- + scope["__simulate_0"] = fn compilada
524
- ```
525
-
526
- ### Uso básico
527
179
 
528
- ```typescript
529
- import { resolveBoundaries, compile, type BoundaryResolvers } from '@statedelta-libs/expressions';
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
- };
180
+ O contexto é acessado via `this`, injetado automaticamente via `.bind()` no construtor. O `HandlerContext` é criado **uma vez** — zero alocação por chamada:
555
181
 
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
- );
182
+ | Campo | O que contém |
183
+ |-------|-------------|
184
+ | `this.accessor` | Resolver de paths customizado |
185
+ | `this.handlers` | Todos os handlers (wrapped) permite composição entre handlers |
186
+ | `this.compiler` | Instância do compilador — permite compilar sub-expressões |
187
+ | `this.scope` | Funções puras do scope |
561
188
 
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
189
+ Handlers **devem** ser regular functions ou method shorthand (arrow functions ignoram `.bind()`):
571
190
 
572
191
  ```typescript
573
- interface SimulateNode {
574
- $simulate: {
575
- effects: Expression[];
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
- },
601
- };
192
+ // method shorthand — funciona
193
+ handlers: { query: { find(key) { this.scope... } } }
602
194
 
603
- const { expr, scope } = resolveBoundaries(
604
- {
605
- $simulate: {
606
- effects: [
607
- { $fn: "increment", args: [{ $: "hp" }] },
608
- { $fn: "decrement", args: [{ $: "mp" }] },
609
- ],
610
- query: { $fn: "isAlive", args: [{ $: "hp" }] },
611
- },
612
- },
613
- { resolvers, scope: myScope }
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 |
195
+ // regular function funciona
196
+ handlers: { query: { find: function(key) { this.scope... } } }
627
197
 
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:
631
-
632
- ```typescript
633
- import { nanoid } from 'nanoid';
634
-
635
- const { expr, scope } = resolveBoundaries(expr, {
636
- resolvers,
637
- scope: baseScope,
638
- genId: () => nanoid(), // IDs únicos globais
639
- });
198
+ // arrow function — NÃO funciona (this é undefined)
199
+ handlers: { query: { find: (key) => { this.scope... } } }
640
200
  ```
641
201
 
642
- ### Diferença entre normalize e resolveBoundaries
643
-
644
- | | `normalize()` | `resolveBoundaries()` |
202
+ | | `scope` | `handlers` |
645
203
  |---|---|---|
646
- | **Propósito** | Transformar sintaxe | Compilação independente |
647
- | **Retorno** | `Expression` | `{ expr, scope }` |
648
- | **Compila slots** | Não | Sim (via `ctx.compile`) |
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
204
+ | **Natureza** | Funções puras (Ramda-style) | Services com contexto |
205
+ | **Acessa** | Apenas args compilados | `this` (HandlerContext) + args |
206
+ | **DSL** | `{ $fn: "add", args: [...] }` | `{ $fn: "query:find", args: [...] }` |
207
+ | **Binding** | Nenhum | `.bind(ctx)` uma vez no construtor |
208
+ | **Overhead** | Zero | Zero (ctx e bindings criados uma vez) |
657
209
 
658
- ## Builders
210
+ Zero overhead quando nenhum handler é registrado.
659
211
 
660
- Funções declarativas para construir expressões:
212
+ ### Accessor objetos inteligentes
661
213
 
662
214
  ```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
- )
683
-
684
- // Condition
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
693
-
694
- ```typescript
695
- import type {
696
- Expression,
697
- CompiledExpression,
698
- RefExpr,
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';
215
+ const compiler = new ExpressionCompiler({
216
+ scope,
217
+ accessor: (path) => tickContext.get(path), // closure auto-suficiente
218
+ });
736
219
  ```
737
220
 
738
- ## Performance
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
221
+ ## Documentação
762
222
 
763
- ### v1.1 v1.2
764
-
765
- 1. **DSL Puro**: `FnExpr.args` agora é `Expression[]` (removido suporte a callbacks inline). Funções devem ser passadas via scope:
766
- ```typescript
767
- // Antes (callbacks inline - não funciona mais)
768
- { $fn: "filter", args: [(item) => item.active] }
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
- ```
223
+ | Doc | Conteúdo |
224
+ |-----|----------|
225
+ | [docs/EXPRESSIONS-API.md](docs/EXPRESSIONS-API.md) | Referência completa da API pública |
226
+ | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Visão geral da infraestrutura |
227
+ | [docs/COMPILE.md](docs/COMPILE.md) | Internals do pipeline closures |
228
+ | [docs/JIT.md](docs/JIT.md) | Internals do pipeline JIT |
870
229
 
871
230
  ## Licença
872
231