@agility-luhn/compiler
v2.2.0
Published
Compiler and generators for the LUHN domain DSL.
Readme
@luhn/compiler
Compilador da DSL LUHN: lê arquivos .luhn, valida semanticamente, expõe AST resolvida e emite artefatos para
TypeScript (interfaces + Zod + service + controller), SQL (DDL Postgres), OpenAPI 3.1 e um índice canônico de metadados.
Pacote independente e publicavel no npm. Os artefatos de distribuicao ficam em
dist/(main: "dist/index.js",types: "dist/index.d.ts").
1. Sumário das Capacidades
| Capacidade | Implementação |
|---------------------------------------------|---------------|
| Parser de .luhn | src/lexer.ts, src/parser.ts |
| Análise semântica e resolução de símbolos | src/semantic.ts |
| Loader de arquivos por application+module | src/file-loader.ts |
| API principal (compile, tipos) | src/compiler.ts, src/index.ts |
| Mini-linguagem implementation script | src/script.ts |
| Watcher com debounce | src/watcher.ts |
| Geração TypeScript (model/service/controller/Zod) | src/generator/typescript-generator.ts |
| Geração SQL (Postgres) | src/generator/sql-generator.ts |
| Geração de schema canônico do banco | src/generator/database-schema-generator.ts |a
| Geração OpenAPI 3.1 | src/generator/openapi-generator.ts |
| Geração de metadata leve | src/generator/metadata-generator.ts |
| Mapa canônico (introspecção em runtime) | src/generator/canonical-map-generator.ts |
| Validadores/fills e máscaras Delphi em type/campo | src/parser.ts, src/semantic.ts, src/generator/canonical-map-generator.ts |
| Configuration declarativo (configuration) com AST dedicada e geração orientada a business | src/ast.ts, src/parser.ts, src/semantic.ts |
| Validação Zod isolada de controller | src/controller-zod.ts |
| Protocolo de plugins (extensões em build) | src/plugin.ts |
2. API Pública
import { compile, CompileOptions, CompileResult } from '@luhn/compiler';
const result = await compile('./luhn', {
setup, // LuhnGenerationSetup (multitenancy, runtime importPath, driver SQL)
typescript: true, // gera *.interface.ts, *.schema.ts, *.model.ts, *.handle.ts
sql: true, // gera generated/database/schema.sql por modulo
openapi: true, // gera generated/openapi/openapi.json por modulo
metadata: true, // gera generated/metadata/metadata.json por modulo
outputDir: './src/luhn'
});
if (result.diagnostics.some(d => d.severity === 'error')) {
for (const d of result.diagnostics) console.error(d.code, d.message);
}CompileResult expõe:
program— AST bruta.resolved— AST apósresolveProgram(símbolos resolvidos, tipos normalizados, mixins aplicados).diagnostics—{ code, severity, message, span: { filePath, startLine, startColumn } }[].artifacts— mapapath -> conteúdode todos os arquivos emitidos antes da gravação.
Diretivas auxiliares (re-exportadas por src/index.ts): parseProgram, resolveProgram,
loadApplicationInputs, createWatcher, compileScript, geradores individuais e tipos
(Program, ResolvedProgram, LuhnGenerationSetup, Diagnostic, Span).
3. Linguagem LUHN (estado atual)
3.1 Declarações top-level
application, module, import, enum, type, record, business, configuration, mixin, service, library.
Regras estruturais aplicadas pelo semantic:
- exatamente uma
application. - toda declaração precisa pertencer a um
module. application.modules: [...]deve bater com osmodule <nome>declarados (mão dupla).- ciclo entre módulos é erro.
importprecisa resolver para símbolo ou módulo existente.- referências por FQCN em campo/parametro/mixin target são proibidas — exige
import+ nome curto. - mixin com colisão de campos → erro.
defaultprecisa ser literal compatível com o tipo (enum aceita só membro válido).service implementation scriptexige corpo.
3.1.1 Configuration (configuration)
configuration e uma declaracao de alto nivel para configuracoes operacionais vinculadas a um business,
mas no backend de geracao reaproveita a mesma semantica de persistencia/CRUD de business.
Diretrizes de uso:
- Nome no formato
business.nomeConfig(sem FQCN completo no corpo, resolucao porimport). @scopesegue as regras existentes (global,conta,unidade) e aproveita o setup de multitenancy.- O AST preserva
kind = configpara diferenciar no semantic/geradores. - A estrategia de persistencia pode ser especializada por gerador (ex.: tabela unificada de configuracoes por modulo) sem perder o contrato de CRUD/OData do runtime.
Exemplo:
import exchange.message
@summary "Roteamento por tipo de mensagem"
@scope conta
configuration message.routeByType
messageType: notificationCode
channel: channel
provider: provider
active: boolean = true
priority: smallint = 03.2 Tipos primitivos reconhecidos
uuid, string, text, number, smallint, float, money, boolean, date, timestamp, json, xml,
url, image, crypto, vector, file, storage, any.
O resolver normaliza primitivos em TS (string/number/boolean/unknown) e gera os tipos físicos
apropriados no SQL e no OpenAPI.
3.3 Campos e parâmetros
nome?: tipo[] = valor "Descricao" preferred?opcional.[]array (gerachild_tableno SQL salvo embedded_record).- default literal (string, number, boolean, ref a enum).
- string final = descrição de exibição.
preferredmarca o campo "principal" para apresentação.validators.<funcao>associa validador declarativo aotypealias ou ao campo.fills.<funcao>(campo1,campo2,...)associa preenchimento automático declarativo.= (expressao)marca o campo como calculado.- campos calculados aceitam aritmética
+ - * /, caminho de campo (a.b) e funçõessum,avg,min,max,coalesce. - o valor calculado é numérico no runtime (entrada textual numérica é convertida; vazio/null vira
0). - string com tokens Delphi (
0,9,L,l,A,a,C,c,#) define máscara de input. - expressão booleana entre parênteses no fim do campo controla visibilidade condicional.
Exemplos:
type cpf string 11 validators.cpf
record pessoa
documento: string 20 validators.cnpj
record endereco
cep: string 9 fills.cep(logradouro,bairro,cidade,estado) "00000-000"
record item
quantidade: number
preco: money
total: money = (preco * quantidade)
business comercial.saida "Saidas"
produtos: item[]
total: money = (sum(produtos.total))
record cliente
tipoPessoa: tipoPessoa
cpf?: cpf (tipoPessoa == FISICA)
cnpj?: cnpj (tipoPessoa == JURIDICA)Precedência de validação:
- se o campo declara
validators.<funcao>, ele prevalece. - se o campo não declara, herda validação do
typealias (quando houver).
Precedência de fill:
- se o campo declara
fills.<funcao>(...), ele prevalece. - se o campo não declara, herda fill do
typealias (quando houver).
Campos calculados: funções e BNF
Funções suportadas:
sum(caminho)soma os valores resolvidos no caminho (inclui coleção).avg(caminho)média aritmética dos valores resolvidos.min(caminho)menor valor numérico do caminho.max(caminho)maior valor numérico do caminho.coalesce(a, b, c, ...)retorna o primeiro argumento não nulo/vazio.
BNF específica de expressão calculada:
<calculated-clause> ::= "=" "(" <calc-expr> ")"
<calc-expr> ::= <calc-term> (("+" | "-") <calc-term>)*
<calc-term> ::= <calc-unary> (("*" | "/") <calc-unary>)*
<calc-unary> ::= ("+" | "-") <calc-unary> | <calc-primary>
<calc-primary> ::= NUMBER
| <path>
| "(" <calc-expr> ")"
| <calc-func>
<calc-func> ::= ("sum" | "avg" | "min" | "max") "(" <path> ")"
| "coalesce" "(" <calc-args>? ")"
<calc-args> ::= <calc-expr> ("," <calc-expr>)*
<path> ::= IDENT ("." IDENT)*3.4 Business e records
@scope conta
business financeiro.contaReceber "Contas a Receber"
numero: string
cliente: clientebusinessrecebe campo sintéticoid(UUID, roleprimary_key).- Records 1-nível sem arrays são candidatos a
embedded_record(achatado em colunas prefixadas no pai). - Records com arrays caem para
TEXT JSON. - Arrays de records → tabela filha com
parent_id+item_index.
3.5 Services
service modulo.nome "Label"
description "..."
input
campo: tipo
output
campo: tipo
throws
CODIGO "Mensagem"
implementation code # handler TS escrito a mão
# ou
implementation script # mini-linguagem transpilada
...3.6 Libraries
library modulo.nome "Label"
function nome
input
output3.7 Validadores e fills declarativos
O parser aceita validators.<funcao> como sufixo de type alias escalar e de campo.
Também aceita fills.<funcao>(campo1,campo2,...) como sufixo de type alias escalar e de campo.
O semantic resolve a referência para uma library e função reais e valida:
- library inexistente (
SEM019) - função inexistente na library (
SEM020) - ambiguidade de resolução da library (
SEM021)
O semantic resolve referências de fill e valida:
- library inexistente (
SEM022) - função inexistente na library (
SEM023) - ambiguidade de resolução da library (
SEM024)
Após resolução, o compilador propaga para o modelo resolvido:
validate?: { library: string; fn: string }
fill?: { library: string; fn: string; targets: readonly string[] }
mask?: string
visibleWhen?: stringEssa metadata segue para o objects-map.json e é consumida no runtime/UI para:
- validação assíncrona no client (blur);
- preenchimento automático no client (blur);
- validação server-side no CRUD antes da persistência.
Diagrama do fluxo:
flowchart LR
A[LUHN: type/campo com validators.funcao e fills.funcao] --> B[Parser]
B --> C[Semantic resolve library + funcao]
C --> D{Valido?}
D -- nao --> E[Diagnostics SEM019-SEM024]
D -- sim --> F[ResolvedField/ResolvedType com validate fill mask]
F --> G[CanonicalMapGenerator]
G --> H[objects-map.json com field.validate field.fill field.mask field.visibleWhen]
H --> I[Runtime Describe]
I --> J[UI: async validator e autofill no blur]
H --> K[Runtime CRUD create/update/patch]3.8 Máscaras no formato Delphi
Quando o parser encontra um sufixo STRING contendo tokens de máscara Delphi,
o valor é tratado como máscara do campo/tipo (sem @mask e sem keyword extra).
Exemplo:
type cpf string 14 validators.cpf "000.000.000-00"
record endereco
cep: string 9 fills.cep(logradouro,bairro,cidade,estado) "00000-000"Tokens suportados:
9: número opcional0: número obrigatórioL: letra obrigatórial: letra opcionalA: letra ou númeroa: letra ou número opcionalC: qualquer caracterec: qualquer caractere opcional#: número com sinal (+/-)- literais fixos:
:,/,-,.(e demais símbolos do template)
Diagrama do fluxo de máscara:
flowchart LR
A[Campo com string de mascara Delphi] --> B[Parser detecta mask template]
B --> C[Semantic aplica heranca de alias quando necessario]
C --> D[ResolvedField ou ResolvedType com mask]
D --> E[CanonicalMapGenerator]
E --> F[field.mask no objects-map]
F --> G[Runtime describe]
G --> H[UI aplica formatacao no input]3.9 Mini-linguagem implementation script
Statements: let, return, throw CODIGO, log, call modulo.library.funcao [with expr],
if (inline), for ... from ... to ... (inline ou bloco), push array expr, expression statement.
Expressões: literais, acesso qualificado a.b.c, métodos obj.metodo(arg), arrays e object literals
(suportam multiline via balanceamento), operadores aritméticos + - * /, comparação == != < <= > >=,
lógicos and or not, call como expressão (transpilada com await). Comentários // e #.
Validações: throw CODIGO só aceita códigos declarados no bloco throws. Transpila para TypeScript
chamando defineService(...) do runtime.
3.10 Gramática (BNF)
BNF simplificada da linguagem LUHN (baseada em src/lexer.ts + src/parser.ts).
Observações léxicas relevantes:
- blocos usam
INDENT/DEDENT(não usa{}); - comentários de linha:
// ...e# ...; - strings usam aspas duplas;
ScriptBodyé token especial capturado apósimplementation script.
<program> ::= <statement>* EOF
<statement> ::= <annotations>? (
<application>
| <import>
| <enum>
| <type-alias>
| <record>
| <business>
| <mixin>
| <service>
| <library>
)
<annotations> ::= <annotation>+
<annotation> ::= "@" <qualified-name> <annotation-value>?
<annotation-value> ::= STRING | NUMBER | BOOLEAN | <qualified-name>
<application> ::= "application" IDENT <application-block>?
<application-block> ::= INDENT <application-item>* DEDENT
<application-item> ::=
"title" STRING
| "description" STRING
| "version" STRING
| "database" IDENT
| "types" STRING
| "modules" <qualified-name-list>
| <application-module-decl>
<application-module-decl> ::= "module" <qualified-name> <module-block>?
<module-block> ::= INDENT <module-item>* DEDENT
<module-item> ::=
"title" STRING
| "owner" BOOLEAN
| "enabled" BOOLEAN
| "types" STRING
| "depends" "on" <qualified-name-list>
<import> ::= "import" <qualified-name> ("as" IDENT)?
<enum> ::= "enum" IDENT INDENT <enum-entry>+ DEDENT
<enum-entry> ::= NUMBER ":" IDENT
<type-alias> ::= "type" IDENT (
INDENT <field>* DEDENT
| <type-ref> NUMBER? <type-alias-suffix>*
)
<type-alias-suffix> ::= "preferred" | <validator-ref> | <fill-ref> | <mask-template>
<record> ::= "record" IDENT STRING? <fields-block>?
<business> ::= "business" IDENT STRING? <fields-block>?
<mixin> ::= "mixin" <qualified-name> <fields-block>?
<fields-block> ::= INDENT <field>* DEDENT
<service> ::= "service" <service-name> STRING? <service-block>?
<service-name> ::= IDENT | IDENT "." IDENT
<service-block> ::= INDENT <service-item>* DEDENT
<service-item> ::=
"describe"
| "description" STRING
| "input" <parameters-block>
| "output" <parameters-block>
| "throws" <throws-block>
| "implementation" ("code" | "script" ScriptBody?)
<library> ::= "library" IDENT STRING? <library-block>?
<library-block> ::= INDENT <library-item>* DEDENT
<library-item> ::= "description" STRING | <library-function>
<library-function> ::= <annotations>? "function" IDENT <library-function-block>?
<library-function-block> ::= INDENT <library-function-item>* DEDENT
<library-function-item> ::=
"description" STRING
| "input" <parameters-block>
| "output" <parameters-block>
<parameters-block> ::= INDENT <parameter>* DEDENT
<parameter> ::= <annotations>? IDENT "?"? ":" <type-ref> <string-size>? <array-suffix>? <default-clause>?
<throws-block> ::= INDENT <throw-item>* DEDENT
<throw-item> ::= IDENT STRING?
<field> ::= <annotations>? IDENT "?"? ":" <type-ref> <array-suffix>? <string-size>? (<default-clause> | <calculated-clause>)? <field-suffix>*
<field-suffix> ::= STRING | "preferred" | <validator-ref> | <fill-ref> | <mask-template> | <visibility-expr>
<validator-ref> ::= "validators" "." IDENT
<fill-ref> ::= "fills" "." IDENT "(" <identifier-list> ")"
<mask-template> ::= STRING ; quando STRING contiver tokens Delphi de máscara
<visibility-expr> ::= "(" <boolean-expr> ")"
<boolean-expr> ::= <boolean-term> ("or" <boolean-term>)*
<boolean-term> ::= <boolean-factor> ("and" <boolean-factor>)*
<boolean-factor> ::= "not" <boolean-factor> | "(" <boolean-expr> ")" | <comparison>
<comparison> ::= IDENT "==" IDENT | IDENT "==" "true" | IDENT "==" "false" | IDENT
<type-ref> ::= IDENT ("." IDENT)*
<qualified-name> ::= IDENT ("." IDENT)*
<qualified-name-list> ::= <qualified-name> ("," <qualified-name>)*
<identifier-list> ::= IDENT ("," IDENT)*
<array-suffix> ::= "[" "]"
<string-size> ::= NUMBER
<default-clause> ::= "=" <literal>
<calculated-clause> ::= "=" "(" <calc-expr> ")"
<literal> ::= STRING | NUMBER | BOOLEAN | <qualified-name>
<calc-expr> ::= <calc-term> (("+" | "-") <calc-term>)*
<calc-term> ::= <calc-unary> (("*" | "/") <calc-unary>)*
<calc-unary> ::= ("+" | "-") <calc-unary> | <calc-primary>
<calc-primary> ::= NUMBER | <qualified-name> | "(" <calc-expr> ")" | <calc-func>
<calc-func> ::= ("sum" | "avg" | "min" | "max") "(" <qualified-name> ")"
| "coalesce" "(" (<calc-expr> ("," <calc-expr>)*)? ")"
BOOLEAN ::= "true" | "false"
IDENT ::= /[A-Za-z_][A-Za-z0-9_]*/
NUMBER ::= /-?[0-9]+(\.[0-9]+)?/Recorte BNF específico de fills e máscara
Para deixar o recurso de autofill explícito na gramática, este é o recorte mínimo:
<type-alias-suffix> ::= "preferred" | <validator-ref> | <fill-ref> | <mask-template>
<field-suffix> ::= STRING | "preferred" | <validator-ref> | <fill-ref> | <mask-template> | <visibility-expr>
<fill-ref> ::= "fills" "." IDENT "(" <fill-target-list> ")"
<fill-target-list> ::= IDENT ("," IDENT)*
<mask-template> ::= STRING ; tokens Delphi: 9 0 L l A a C c #
<visibility-expr> ::= "(" <boolean-expr> ")"Diagrama dedicado do fluxo de fills no compilador:
flowchart LR
A[DSL: fills.funcao targets] --> B[Parser reconhece fill-ref]
B --> C[Semantic resolve library e funcao]
C --> D{Referencia valida?}
D -- nao --> E[SEM022 SEM023 SEM024]
D -- sim --> F[ResolvedField ou ResolvedType com fill]
F --> G[CanonicalMapGenerator]
G --> H[field.fill com library fn targets]Diagrama estrutural da gramática:
flowchart TD
P[program] --> S[statement*]
S --> A[application]
S --> I[import]
S --> E[enum]
S --> T[type-alias]
S --> R[record]
S --> B[business]
S --> M[mixin]
S --> SV[service]
S --> L[library]
T --> TR[type-ref]
T --> F[field*]
T --> TS[type-alias-suffix*]
TS --> PREF[preferred]
TS --> VR[validator-ref]
R --> FB[fields-block]
B --> FB
M --> FB
FB --> F
F --> TR
F --> ARR[array-suffix?]
F --> SZ[string-size?]
F --> DEF[default-clause?]
F --> FS[field-suffix*]
FS --> DISP[display string]
FS --> PREF
FS --> VR
FS --> FR[fill-ref]
SV --> SB[service-block]
SB --> DESC[describe?]
SB --> SD[description?]
SB --> IN[input parameters]
SB --> OUT[output parameters]
SB --> THR[throws]
SB --> IMPL[implementation code ou script]
L --> LB[library-block]
LB --> LD[description?]
LB --> LF[library-function*]
LF --> IN
LF --> OUT
VR --> VLIB[validators]
VR --> VFN[function name]
FR --> FLIB[fills]
FR --> FFN[function name]
FR --> FTG[target fields]Primitivos reconhecidos em <type-ref> (quando não resolvidos como tipo nomeado):
uuid,string,text,number,smallint,float,money,boolean,date,timestamp,json,xml,url,image,crypto,vector,file,storage,any.
4. Geradores
4.1 TypeScriptGenerator
Por entidade ou business:
*.interface.ts— interface TS pura.*.enum.ts— uniões de string.*.schema.ts— schema Zod (validação de entrada).*.model.ts— DTO + helpers de conversão snake_case ↔ camelCase.
O CRUD HTTP genérico (insert/update/delete/get/list/
queryOData) vive 100% no runtime, empackages/runtime/src/http. Personalizações por entidade são feitas via hooksbefore/afterregistrados emcreateLuhnServer({ hooks })ou peloPluginInstallContext.registerHook(...). Não se gera mais*.service.tsnem*.controller.ts.
Por service:
services/<service>.handle.ts— descritor (defineService) tipado pelo input/output declarados.services/<service>.script.ts— quandoimplementation script, contém o handler transpilado.
Por library:
libraries/<library>.handle.ts.
Barrels:
services-scripts.ts— importa todos os.script.tspara auto-registro no boot.
Recursos adicionais:
- síntese de relações de multitenancy via
setup.multitenancy. - conversão automática de relações em colunas
*_idno service SQL. - ordenação determinística dos campos (
id, tenancy, demais alfabéticos). exactOptionalPropertyTypesrespeitado: opcionais saem com| undefinedexplícito.
4.2 SqlGenerator (Postgres)
Por módulo, emite generated/database/schema.sql:
CREATE SCHEMA IF NOT EXISTS <modulo>.CREATE TABLEpor entidade/business.- FKs para relações detectadas.
- Tabelas filhas para arrays (records ou escalares) com
parent_id+item_index. - Defaults SQL para literais.
CHECKpara enums.- Índices automáticos em FKs.
- Flatten de aliases estruturados em colunas.
- Computed columns quando
calculated+ expressão suportada. gen_random_uuid()aplicado apenas em colunas comrole: 'primary_key';relation_keyem PK composta (ex.:pedido_venda_itens.pedido_venda_id) não recebe default.
Estratégias de armazenamento (storageStrategy):
| Estratégia | Quando | Resultado físico |
|-------------------|--------------------------------------------|------------------|
| column | escalar simples | coluna direta |
| relation_fk | referência a outra entidade | *_id + FK |
| child_table | array de records ou escalares | tabela filha |
| embedded_record | record 1-nível sem arrays | colunas prefixadas (endereco_logradouro, endereco_cidade, ...) |
Drivers configuráveis: 'postgres' (completo) e fallback 'generic'.
4.3 OpenApiGenerator
Por módulo, emite generated/openapi/openapi.json com:
- schemas de entities, enums, records e aliases estruturados.
- paths CRUD por recurso (GET list, GET id, POST, PUT, PATCH, DELETE).
- paths de services (
POST /api/<modulo>/<service>). - componentes de input/output de service.
- extensões
x-luhn-service,x-luhn-application,x-luhn-module. - tags por entidade e por grupo de services do módulo.
- exemplos OData prontos por entidade (com base em campos reais).
4.4 SchemaGenerator e CanonicalMap
DatabaseSchemaGeneratorproduz um JSON canônico das tabelas (consumido por@agility-solucoes/luhn-databaseno diff/migrate).CanonicalMapGeneratoremiteobjects-map.jsonpor módulo: mapa de FQCN → metadados resolvidos. É lido em runtime para introspecção (MCP, OpenAPI servido, etc.).- campos com validador são emitidos com
field.validate = { library, fn }no mapa canônico. - campos com autofill são emitidos com
field.fill = { library, fn, targets }no mapa canônico. - campos com máscara são emitidos com
field.mask = "template"no mapa canônico.
4.5 MetadataGenerator
JSON leve por módulo (application, module, lista de entities com campos básicos). Usado por dashboards.
5. Loader de Arquivos
loadApplicationInputs(dir):
- procura arquivo com
applicationno nome (fallback: primeiro.luhnda raiz). - lista recursivamente todos os
.luhn. - extrai módulos do
applicationpormodules: [...]ou por declaraçõesmodule <nome>. - filtra arquivos por nome de módulo no path.
- injeta automaticamente
module <pasta-raiz>em arquivos sem declaração explícita.
6. Watcher
createWatcher({ dir, onRecompile, ... }):
- observa
.luhnrecursivamente. - debounce 100ms.
- recompila e dispara
onRecompile(result). - em erro de FS/compile, devolve diagnostic
WATCH.
Usado pelo server-playground em --watch para recompilar + reregistrar services sem reiniciar o servidor.
7. Setup de Geração
LuhnGenerationSetup é o ponto de extensão do consumidor:
const setup: LuhnGenerationSetup = {
database: {
driver: 'postgres' // ou 'generic'
},
runtime: {
importPath: '@agility-solucoes/luhn-runtime' // ou './src' em dev
},
multitenancy: {
// opcional: ambos os blocos são injetados conforme @scope da business
conta: { fieldName: 'conta', typeFqcn: 'plataforma.conta', keyFieldName: 'id' },
unidade: { fieldName: 'unidade', typeFqcn: 'plataforma.unidade', keyFieldName: 'id' }
}
};Escopo das business
O conceito antigo de entity foi removido. Toda declaração de domínio é uma business, e o escopo de multitenancy é declarado por anotação:
| Anotação | Comportamento |
| ------------------- | -------------------------------------------------------------------------------- |
| @scope unidade | Padrão. Injeta conta e unidade (entidade isolada por unidade da conta). |
| @scope conta | Injeta apenas conta (compartilhado entre unidades da mesma conta). |
| @scope global | Não injeta nada (ex.: plataforma.conta, entidades do IAM). |
Exemplo:
@scope conta
business cliente "Clientes"
nome: string 150
documento: string 20Importante: o pacote IAM não define multitenancy própria — todas as suas businesses usam
@scope global. Quem decide o setup é o app hospedeiro.
8. Protocolo de Plugins do Compilador
src/plugin.ts define a forma como pacotes externos (ex.: luhn-iam) podem contribuir com
diretórios .luhn adicionais para a compilação do app. O runtime, ao iniciar o servidor, agrega os luhnDirs
de todos os plugins ativos e os passa ao compilador como inputs combinados.
9. Diagnósticos
Cada Diagnostic traz:
{
code: string; // ex.: 'SEM001', 'PARSE042', 'TS_GEN'
severity: 'error' | 'warning';
message: string;
span: { filePath: string; startLine: number; startColumn: number; endLine: number; endColumn: number };
}Categorias principais: LEX, PARSE, SEM (semantic), TS_GEN, SQL_GEN, OPENAPI_GEN, SCRIPT, WATCH.
10. Scripts de Build
pnpm --filter @luhn/compiler build # tsc -p tsconfig.json11. Fluxo Completo do Compilador LUHN
Esta secao resume o pipeline real do compilador, da leitura dos arquivos .luhn ate a geracao dos artefatos consumidos pelo runtime, pelo banco e pelas UIs.
11.1 Visao de ponta a ponta
Pipeline executado por compile(dir, options):
loadApplicationInputs(dir)localiza o arquivo deapplication, descobre os modulos declarados, carrega os arquivos.luhnrelevantes e injeta entradas auxiliares detypesquando declaradas.parseProgram(inputs)tokeniza cada arquivo com o lexer, constroi a AST (ProgramNode) e acumula diagnosticos lexicos e sintaticos.resolveProgram(program)executa a analise semantica: resolve simbolos, imports, aliases, enum refs, records, relations, mixins, escopos, validadores, fills, mascaras e expressoes calculadas.compileInternal(...)monta oGenerationContextcomresolved,diagnosticsconsolidados esetupopcional.- Os geradores habilitados percorrem o
resolvede emitem artefatos em memoria. - O
CompileResultretornaprogram,resolved,diagnosticseartifacts; a gravacao fisica dos arquivos e responsabilidade da camada consumidora do compilador.
Arquivos principais envolvidos no fluxo:
src/compiler.ts- ponto de entrada assíncrono.src/file-loader.ts- descoberta e carga dos arquivos da aplicacao.src/lexer.ts- tokenizacao e controle deINDENT/DEDENT.src/parser.ts- AST e regras sintaticas.src/semantic.ts- resolucao semantica e validacoes estruturais.src/compiler-internal.ts- orquestracao da compilacao e dos geradores.src/generator/*- emissao dos artefatos finais.
11.2 Fases detalhadas
Fase 1. Descoberta e carga de arquivos
Responsavel: loadApplicationInputs(dir).
O loader:
- encontra o arquivo principal da aplicacao;
- extrai os modulos declarados em
application.modulesou nos blocosmodule; - lista recursivamente todos os
.luhndo diretorio; - filtra apenas os arquivos relevantes para os modulos ativos;
- carrega arquivos auxiliares declarados em
types "..."; - devolve
SourceFileInput[]compathrelativo etextUTF-8.
Essa fase define o universo de entrada da compilacao. Se um arquivo nao entrar aqui, ele nao participa da AST nem da geracao.
Fase 2. Analise lexica
Responsavel: tokenize(filePath, source) em src/lexer.ts.
O lexer:
- normaliza quebras de linha;
- converte texto em tokens (
Identifier,String,Number, palavras reservadas e simbolos); - controla blocos por indentacao com tokens
INDENTeDEDENT; - captura
ScriptBodyaposimplementation script; - gera diagnosticos como string nao terminada, caractere inesperado e uso invalido de
{/}.
Resultado da fase:
- stream de tokens por arquivo;
- diagnosticos
LEX*.
Fase 3. Analise sintatica
Responsavel: parseProgram(inputs) em src/parser.ts.
O parser:
- consome os tokens produzidos pelo lexer;
- monta a AST bruta da linguagem (
ProgramNode); - reconhece declaracoes top-level como
application,module,import,enum,type,record,business,configuration,mixin,serviceelibrary; - preserva spans para diagnosticos e tooling;
- interpreta campos, parametros, annotations, defaults, arrays, calculados, validators, fills, mascaras e
implementation script.
Resultado da fase:
- AST bruta (
program); - diagnosticos
PARSE*.
Fase 4. Analise semantica e resolucao
Responsavel: resolveProgram(program) em src/semantic.ts.
A analise semantica transforma a AST bruta em um modelo resolvido e consistente. Ela:
- resolve imports e simbolos referenciados;
- valida a unicidade e a coerencia de
applicationemodule; - detecta ciclos entre modulos;
- normaliza tipos primitivos e aliases;
- expande e valida mixins;
- identifica relacoes entre entidades;
- resolve
@scopee regras de multitenancy; - valida defaults, enums e tipos de campo;
- resolve
validators.<fn>efills.<fn>contralibrary.function; - propaga metadata como
validate,fill,maskevisibleWhen; - resolve expressoes calculadas e funcoes agregadoras suportadas.
Resultado da fase:
resolved(ResolvedProgram);- diagnosticos
SEM*; - IR pronta para geracao.
Fase 5. Montagem do contexto de geracao
Responsavel: compileInternal(inputs, options) em src/compiler-internal.ts.
Nessa fase o compilador:
- combina diagnosticos do parser com diagnosticos do semantic;
- injeta
setupopcional (database,runtime,multitenancy); - decide quais geradores vao rodar com base em
CompileOptions.
Selecao atual de geradores:
TypeScriptGeneratorOpenApiGeneratorDatabaseSchemaGenerator(habilitado quandodatabaseSchema !== falseousql)MetadataGeneratorCanonicalMapGenerator
Fase 6. Geracao de artefatos
Cada gerador recebe o mesmo GenerationContext e produz GeneratedArtifact[].
Artefatos principais:
- TypeScript: interfaces, enums, schemas Zod, models, handles de services e libraries.
- OpenAPI:
openapi.jsonpor modulo. - Database schema: JSON canonico do banco.
- SQL/Postgres: DDL derivada do modelo resolvido.
- Metadata: resumo leve por modulo.
- Canonical map:
objects-map.jsonpara introspeccao e runtime.
Observacao importante:
DatabaseSchemaGeneratorreaproveita o mapa canonico para consolidar objetos, enums e relacionamentos antes de montar a visao do banco.- o compilador monta os artefatos em memoria; quem grava em disco ou publica no runtime e a camada chamadora.
11.3 Palavras reservadas da linguagem
As palavras reservadas abaixo sao reconhecidas diretamente pelo lexer atual (KEYWORDS em src/lexer.ts). Elas nao devem ser usadas como identificadores soltos quando o contexto esperar nome de simbolo.
Top-level e estrutura:
applicationmoduleimportenumtyperecordbusinessconfigurationmixinservicelibraryfunction
Metadados e organizacao:
titleownerenableddependsonasdescriptiondescribeversiondatabasemodulestypes
Campos, parametros e comportamento:
preferredcalculatedinputoutputthrowsimplementationcodescript
Literais booleanos:
truefalse
Observacoes praticas:
validatorsefillsnao aparecem como palavras reservadas do lexer; elas entram como identificadores qualificados interpretados pelo parser/semantic.- funcoes de calculo como
sum,avg,min,maxecoalescetambem nao sao keywords lexicas; sao reconhecidas no parse de expressoes calculadas.
11.4 Diagrama do fluxo completo
flowchart TD
A[Diretorio da aplicacao LUHN] --> B[loadApplicationInputs]
B --> C[SourceFileInput[]]
C --> D[tokenize em src/lexer.ts]
D --> E[Tokens + diagnostics LEX]
E --> F[parseProgram em src/parser.ts]
F --> G[ProgramNode AST + diagnostics PARSE]
G --> H[resolveProgram em src/semantic.ts]
H --> I[ResolvedProgram + diagnostics SEM]
I --> J[compileInternal]
J --> K[GenerationContext]
K --> L[TypeScriptGenerator]
K --> M[OpenApiGenerator]
K --> N[DatabaseSchemaGenerator]
K --> O[MetadataGenerator]
K --> P[CanonicalMapGenerator]
L --> Q[Artefatos TS]
M --> R[openapi.json]
N --> S[schema.json e/ou SQL]
O --> T[metadata.json]
P --> U[objects-map.json]
Q --> V[CompileResult.artifacts]
R --> V
S --> V
T --> V
U --> V11.5 Fluxo resumido de analise ate geracao de codigo
Em uma frase: o compilador carrega os arquivos da aplicacao, tokeniza o texto, monta a AST, resolve semanticamente o dominio, consolida diagnosticos e so entao gera artefatos tipados e canônicos para TypeScript, OpenAPI, banco, metadata e runtime.
Resumo operacional:
- Entrada: arquivos
.luhneCompileOptions. - Analise lexica: tokens e erros de formato.
- Analise sintatica: AST completa.
- Analise semantica: resolucao de simbolos, tipos, relations, mixins e regras.
- IR resolvida: estrutura pronta para geracao.
- Geracao: artefatos derivados por modulo e por entidade.
- Saida:
CompileResultcom AST, IR resolvida, diagnosticos e artefatos.
Como main: "src/index.ts" e type: "commonjs", o build com tsc produz .js+.d.ts ao lado do .ts
(sem outDir). Em dev, os consumidores podem importar direto o .ts via ts-node.
11. Limitações Atuais
- Não há REPL nem CLI próprio (use
apps/cliou os scripts doapps/playground). - A mini-linguagem de
implementation scripté intencionalmente restrita — para lógicas complexas, useimplementation code(handler TS). - Generators emitem para Postgres; outros bancos caem no fallback
'generic'(sem FK/CHECK/índices). - Watcher é por polling do
fs.watchnativo do Node — pode perder eventos em filesystems remotos.
