npm package: @statedelta-libs/expressions
Description: JSON DSL compiler for optimized functions - StateDelta expression engine
Author: Anderson D. Rosa
Latest version: 3.2.0

# @statedelta-libs/expressions

> Compilador de JSON DSL puro para funções otimizadas.

[![npm version](https://img.shields.io/npm/v/@statedelta-libs/expressions.svg)](https://www.npmjs.com/package/@statedelta-libs/expressions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 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

```bash
pnpm add @statedelta-libs/expressions
```

## Quick Start

```typescript
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

```typescript
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ção
```

**Closures** 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.

```typescript
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 2
```

`BoundFn` é callable direto — sem `.fn`. Funciona com `.map()`, `.filter()` e qualquer API que espera funções. Metadata como propriedades:

```typescript
const fn = compiler.compile(expr).bind();
fn(data);    // executa
fn.deps;     // paths observados
fn.hash;     // hash estrutural
```

### DI-first — construtor sem contexto

O compilador funciona sem contexto no construtor. Contexto é injetado via `bind()`:

```typescript
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:

```typescript
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ó bind
```

**Cenários habilitados:**

- **Multi-tenant** — `CompileCache` compartilhado, 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

```typescript
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.

```typescript
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.

```typescript
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`.

```typescript
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"`:

```json
{ "$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()`):

```typescript
// 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

```typescript
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](EXPRESSIONS-API.md) | Referência completa da API pública |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Arquitetura interna (IR, backends, cache) |
| [docs/TEMPLATE.md](docs/TEMPLATE.md) | Template compiler (compileDefinition) |

## Licença

MIT © Anderson D. Rosa