@statedelta-libs/expressions
v3.2.0
Published
JSON DSL compiler for optimized functions - StateDelta expression engine
Maintainers
Readme
@statedelta-libs/expressions
Compilador de JSON DSL puro para funções otimizadas.
O que é
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.
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.
Instalação
pnpm add @statedelta-libs/expressionsQuick Start
import { ExpressionCompiler } from '@statedelta-libs/expressions';
const compiler = new ExpressionCompiler({
scope: {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
filter: (pred) => (arr) => arr.filter(pred),
sum: (arr) => arr.reduce((a, b) => a + b, 0),
},
});
// Compila uma vez
const compiled = compiler.compile({
$pipe: [
{ $: "items" },
{ $fn: "filter", args: [{ $arrow: { $: "item.active" }, args: ["item"] }] },
{ $fn: "sum" }
]
});
// Bind ao contexto → BoundFn callable
const fn = compiled.bind();
// Executa milhões de vezes
fn({ items: [{ active: true, price: 10 }, { active: false, price: 20 }] });
fn.deps; // ["items"]Os 5 Primitivos
| Primitivo | O que faz | Exemplo DSL | JS equivalente |
|-----------|-----------|-------------|----------------|
| $ | Referência a path | { $: "user.name" } | data.user.name |
| $fn | Invocação/referência de função | { $fn: "add", args: [...] } | add(a, b) |
| $if | Condicional | { $if: cond, then: a, else: b } | cond ? a : b |
| $pipe | Composição left-to-right | { $pipe: [x, f, g] } | g(f(x)) |
| $arrow | Closure deferida | { $arrow: body, args: ["x"] } | (x) => body |
Tudo 100% JSON-serializável. Funções nunca aparecem no DSL — são referenciadas por nome e resolvidas contra o scope em runtime.
Dois Modos de Compilação
compiler.compile(expr); // closures — compilação rápida, ~9-20M ops/s
compiler.jit(expr); // JIT — compilação lenta, ~25-27M ops/s de execuçãoClosures compõe funções JavaScript aninhadas. Ideal para expressões executadas poucas vezes ou ambientes com CSP restritivo.
JIT gera código JavaScript via AST e new Function(). Ideal para hot paths executados muitas vezes. Break-even em ~8 execuções.
3 Fases: Compile → Bind → Run
Compilação e contexto são independentes. Compile uma vez, bind N vezes com contextos diferentes — zero recompilação.
const compiler = new ExpressionCompiler({ scope });
// Fase 1: Compile (pesado — uma vez) → Artifact
const artifact = compiler.jit(expr);
// Fase 2: Bind (barato — só DI) → BoundFn callable
const forTenant1 = artifact.bind({ scope, accessor: tenant1Accessor });
const forTenant2 = artifact.bind({ scope, accessor: tenant2Accessor });
// Fase 3: Run
forTenant1(data); // usa accessor do tenant 1
forTenant2(data); // usa accessor do tenant 2BoundFn é callable direto — sem .fn. Funciona com .map(), .filter() e qualquer API que espera funções. Metadata como propriedades:
const fn = compiler.compile(expr).bind();
fn(data); // executa
fn.deps; // paths observados
fn.hash; // hash estruturalDI-first — construtor sem contexto
O compilador funciona sem contexto no construtor. Contexto é injetado via bind():
const compiler = new ExpressionCompiler(); // sem scope/accessor/handlers
const artifact = compiler.compile(expr);
const fn = artifact.bind({ scope, accessor });
fn(data);Compile cache compartilhável
CompileCache armazena artefatos context-free compartilháveis entre instâncias:
import { ExpressionCompiler, CompileCache } from '@statedelta-libs/expressions';
const sharedCache = new CompileCache(5000);
const tenantA = new ExpressionCompiler({ scope: scopeA, compileCache: sharedCache });
const tenantB = new ExpressionCompiler({ scope: scopeB, compileCache: sharedCache });
// Mesma expressão compilada uma vez, bind por tenant
tenantA.jit(expr).bind()(data); // compile cache miss → compile + bind
tenantB.jit(expr).bind()(data); // compile cache hit → só bindCenários habilitados:
- Multi-tenant —
CompileCachecompartilhado, bind por tenant - Hot-swap de accessor por tick — compile uma vez, bind a cada tick
- Testing — mesmo artifact, scope mockado via bind
- Lazy bind — compile no boot, bind sob demanda
Extensibilidade
Scope — funções disponíveis
const compiler = new ExpressionCompiler({
scope: {
add: (a, b) => a + b,
filter: (pred) => (arr) => arr.filter(pred),
tryCatch: (arrow, params) => {
try { return arrow(); }
catch { return params?.fallback ?? null; }
},
},
});Normalize — DSL customizado
Transforma sugar syntax em DSL puro antes da compilação. Para quando o resultado é expression DSL válido.
const transforms = {
$double: (node) => ({ $fn: "multiply", args: [node.$double, 2] }),
};
const pure = compiler.normalize({ $double: { $: "value" } }, transforms);
compiler.compile(pure);Boundaries — compiladores externos
Intercepta nós durante a compilação e terceiriza para outro algoritmo. Para quando o nó precisa de um compilador completamente diferente. Boundaries são compile-time — ficam fixos entre bind() calls.
import type { BoundaryDef } from '@statedelta-libs/expressions';
// $raw — passthrough, nada é compilado
const rawBoundary: BoundaryDef = {
check: (node) => "$raw" in node,
handle: (node) => () => node.$raw,
};
// $rules — DSL estrangeiro com compilador próprio
const rulesBoundary: BoundaryDef = {
check: (node) => "$rules" in node,
handle: (node) => {
const compiled = ruleEngine.compile(node.$rules);
return (data) => compiled.evaluate(data);
},
};
const compiler = new ExpressionCompiler({
scope,
boundaries: [rawBoundary, rulesBoundary],
});
// $rules é interceptado pelo boundary, compilado pelo ruleEngine
compiler.compile({
$fn: "add",
args: [{ $: "base" }, { $rules: { when: "vip", then: 20 } }]
});O handler é uma closure auto-suficiente — captura o que precisa (outros compiladores, databases, etc.) por fora. Zero overhead quando não há boundaries registrados.
| | normalize() | BoundaryDef |
|---|---|---|
| Quando roda | Antes da compilação | Durante a compilação (no walk) |
| Retorno | Expression (DSL puro) | CompiledFn (função pronta) |
| Uso típico | Sugar syntax | DSL estrangeiro, $raw, rule engines |
| bind() | N/A | Fixo (compile-time) |
Handlers — services com contexto
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.
import type { HandlerContext } from '@statedelta-libs/expressions';
const compiler = new ExpressionCompiler({
scope: { add: (a, b) => a + b },
handlers: {
query: {
find(key: string) {
return db.find(key);
},
findAll() {
// chamar outro handler
const valid = this.handlers.validation.check("all");
return valid ? db.findAll() : [];
},
},
validation: {
check(value: unknown) {
// chamar scope fn
return this.scope.add(value != null ? 1 : 0, 0) > 0;
},
},
},
});Handlers são invocados via $fn com sintaxe "namespace:method":
{ "$fn": "query:find", "args": [{ "$": "userId" }] }
{ "$fn": "validation:check", "args": [{ "$": "value" }] }O contexto é acessado via this, injetado automaticamente via .bind() no construtor. O HandlerContext é criado uma vez — zero alocação por chamada:
| Campo | O que contém |
|-------|-------------|
| this.accessor | Resolver de paths customizado |
| this.handlers | Todos os handlers (wrapped) — permite composição entre handlers |
| this.compiler | Instância do compilador — permite compilar sub-expressões |
| this.scope | Funções puras do scope |
Handlers devem ser regular functions ou method shorthand (arrow functions ignoram .bind()):
// method shorthand — funciona
handlers: { query: { find(key) { this.scope... } } }
// regular function — funciona
handlers: { query: { find: function(key) { this.scope... } } }
// arrow function — NÃO funciona (this é undefined)
handlers: { query: { find: (key) => { this.scope... } } }| | scope | handlers |
|---|---|---|
| Natureza | Funções puras (Ramda-style) | Services com contexto |
| Acessa | Apenas args compilados | this (HandlerContext) + args |
| DSL | { $fn: "add", args: [...] } | { $fn: "query:find", args: [...] } |
| Binding | Nenhum | .bind(ctx) uma vez no construtor |
| Overhead | Zero | Zero (ctx e bindings criados uma vez) |
| bind() | Trocável via ctx.scope | Trocável via ctx.handlers |
Zero overhead quando nenhum handler é registrado.
Accessor — objetos inteligentes
const compiler = new ExpressionCompiler({
scope,
accessor: (path) => tickContext.get(path), // closure auto-suficiente
});
// Hot-swap por tick via bind()
const artifact = compiler.jit(expr, { useAccessor: true });
const fn = artifact.bind({
scope,
accessor: (path) => newTickContext.get(path),
});Documentação
| Doc | Conteúdo | |-----|----------| | EXPRESSIONS-API.md | Referência completa da API pública | | docs/ARCHITECTURE.md | Arquitetura interna (IR, backends, cache) | | docs/TEMPLATE.md | Template compiler (compileDefinition) |
Licença
MIT © Anderson D. Rosa
