@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 CHANGED
@@ -5,868 +5,158 @@
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
- };
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
- ```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" }
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
- ### Pipe ($pipe)
239
-
240
- Composição com valor inicial - o valor passa por cada função em sequência.
48
+ ## Os 5 Primitivos
241
49
 
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
- ```
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
- ### Function Call ($fn)
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
- Chama função do scope com argumentos compilados.
60
+ ## Dois Modos de Compilação
256
61
 
257
62
  ```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)
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
- ### 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 }
67
+ **Closures** compõe funções JavaScript aninhadas. Ideal para expressões executadas poucas vezes ou ambientes com CSP restritivo.
282
68
 
283
- // Ref vs Ref
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
- // $fn nos lados
287
- { left: { $fn: "add", args: [{ $: "a" }, { $: "b" }] }, op: "gt", right: 100 }
71
+ ## Extensibilidade
288
72
 
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.
73
+ ### Scope funções disponíveis
307
74
 
308
75
  ```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
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
- transforms
492
- );
85
+ });
493
86
  ```
494
87
 
495
- ### Proteção contra loops
88
+ ### Normalize DSL customizado
496
89
 
497
- Se um transform retornar objeto com a mesma chave, erro é lançado:
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 badTransforms = {
501
- $loop: (node) => ({ $loop: node.$loop }), // mesma chave!
93
+ const transforms = {
94
+ $double: (node) => ({ $fn: "multiply", args: [node.$double, 2] }),
502
95
  };
503
96
 
504
- normalize({ $loop: "test" }, badTransforms);
505
- // Error: Transform "$loop" returned object with same key — infinite loop
97
+ const pure = compiler.normalize({ $double: { $: "value" } }, transforms);
98
+ compiler.compile(pure);
506
99
  ```
507
100
 
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.
101
+ ### Boundaries — compiladores externos
511
102
 
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
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 { 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
- };
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
- ```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
- },
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
- 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
- },
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
- { 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 |
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
- ```typescript
633
- import { nanoid } from 'nanoid';
123
+ const compiler = new ExpressionCompiler({
124
+ scope,
125
+ boundaries: [rawBoundary, rulesBoundary],
126
+ });
634
127
 
635
- const { expr, scope } = resolveBoundaries(expr, {
636
- resolvers,
637
- scope: baseScope,
638
- genId: () => nanoid(), // IDs únicos globais
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
- ### Diferença entre normalize e resolveBoundaries
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()` | `resolveBoundaries()` |
137
+ | | `normalize()` | `BoundaryDef` |
645
138
  |---|---|---|
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
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
- // 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
143
+ ### Accessor — objetos inteligentes
693
144
 
694
145
  ```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';
146
+ const compiler = new ExpressionCompiler({
147
+ scope,
148
+ accessor: (path, ctx) => ctx.get(path), // ex: TickContext, reactive store
149
+ });
736
150
  ```
737
151
 
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
152
+ ## Documentação
762
153
 
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
- ```
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