Skip to content
Zever
Home

Compiler boundary

Why runtime packages never import the DSL compiler, how module layering enforces it, and the separate schema-module rule.

2 min readEdit on GitHub

Runtime packages must never import dsl. The compiler turns a .zen schema into Go source once; a running service imports only generated output, the container, and adapters. Linking the frontend would bloat every binary and let request-time code special-case schema logic.

source text -> lexer -> parser -> ast -> resolver -> ir.Schema
-> compile -> backend outputs

Everything on that line is compile-time only; runtime imports only its output (typed ORM, HTTP routes, gRPC stubs).

  • Compile time diag, token, lexer, ast, parser: bytes to a best-effort syntax tree (never panics, depth guard, recovery).
  • Compile time naming, resolver, ir: multi-pass []*ast.File to *ir.Schema, keep-going diagnostics.
  • Compile time format, breaking, compile, backend: formatter, Change taxonomy, filename-sorted build, Backend interface.
  • Compile time dsl/gengrammar via tools/gengrammar: renders editors/nvim and editors/vscode.
  • Runtime container, config, orm, adapters: load, resolve, query, close.
  • Tooling cmd/zever, tools/zever-lsp: CLI and editor support (LSP is its own module).

Keep compile input and runtime config apart

Section titled “Keep compile input and runtime config apart”

The compiler reads schema files and backend flags, never runtime config (env only). Schema errors come from .zen files; service errors (adapters, DSNs, secrets) come from config.FromEnv.

Compile, then run against generated output

Section titled “Compile, then run against generated output”
Terminal window
zever check schema/todo.zen
zever fmt -l schema/todo.zen
zever compile schema/todo.zen --backend=zenorm,openapi,atlas --out generated
zever db migrate --dry-run --adapter=sqlite schema/todo.zen
go run ./examples/todo/cmd/server -addr :8080

check validates; fmt -l lists unformatted files; compile emits ORM, OpenAPI, and DDL into generated/; migrate --dry-run previews SQL. The server imports generated/ and resolves through the container, never dsl (Container).

Regenerate with zever compile; never hand-edit generated/. Entry points are parser.New(file, src).ParseFile() then resolver.Resolve(files), or compile.Compile(files, backends...); compile/testdata/app.zen is the canonical fixture. After touching token.Keywords or resolver.ScalarTypeNames, run make generate (go run ./tools/gengrammar).

zever check-boundaries checks a different rule: cross-schema-module references, where a relation, RPC return type, or permission resource reaches across .zen module directories. The resolver enforces it every compile; check-boundaries reports the Resolve() diagnostics (Zen syntax).

dsl is a top-level module, so Go’s internal/ visibility does not apply; an accidental import fails review. The parser never panics, so failures surface as diagnostics.

go build fails importing dsl

A runtime package reached the compiler. Drop the import; use generated/.

check-boundaries reports cross-module references

A relation or RPC type crosses .zen module directories. Move it.

compile prints diagnostics but writes no output

Resolve errors block backends. Fix the .zen position; run zever check.

The editor misses new keywords after a grammar change

token.Keywords or resolver.ScalarTypeNames changed. Run make generate and commit editors/.

Edits in generated/ vanish

Compile overwrites generated/. Edit the .zen source.