- Docs
- Concepts
- Resolver and IR
Resolver and IR
How the compiler frontend resolves .zen schemas into an IR and reports every diagnostic in one run.
The frontend turns source text into a resolved *ir.Schema plus diagnostics, surfacing every problem in one run instead of stopping at the first. Use it to debug a schema error or consume the IR from a backend. Runtime packages must never import dsl.
The pipeline
Section titled “The pipeline”source text → lexer → parser → ast → resolver → ir.Schema ↘ compile → backend outputsNo config file drives it. Inputs are []*ast.File plus a backend list, and output order is deterministic (filename-sorted).
The packages (diag, token, lexer, ast, parser, naming, resolver, ir, format, breaking, compile, backend) and their roles are listed in the compiler boundary table. lexer never panics and is fuzz-tested, parser recovers per declaration with a depth guard, format refuses lex-dirty input and is idempotent, and ir carries the gRPC-canonical ErrorCode table.
Parse and resolve a schema
Section titled “Parse and resolve a schema”Parse first, or run the full path:
f, parseDiags := parser.New("schema/app/app.zen", src).ParseFile()schema, diags := resolver.Resolve([]*ast.File{f})// files maps path to source; Result.Outputs maps backend name to files.res, diags := compile.Compile(files, backends...)The resolver runs ordered passes, all keep-going:
- Modules and symbols. Group files into modules by directory, then index entities, enums, messages, services, jobs, schedules, and sagas by name. Duplicates are diagnostics, not panics.
- Bind types. Resolve enums, entities, and message fields against scalars plus declared types. Unknown names diagnose and continue.
- Relations and indexes. Verify both sides of
has_many/belongs_toagree on the foreign key, and resolve indexes. - Services and RPCs. Validate HTTP templates against params,
@validateexpressions, and theErrorCodetable. - Jobs, schedules, sagas. Bind them to their services and jobs.
compile/testdata/app.zen is the canonical fixture. Golden backend outputs are committed, and regeneration is explicit (for example go test ./dsl/backend/gogen/ -run TestGoldenFiles -update; the gogen, atlas, proto, and zenorm tests take -update). To see the pipeline pass end to end:
go test ./dsl/backend/gogen/ -run TestGoldenFiles -vExpected output:
--- PASS: TestGoldenFiles (0.00s)PASSok github.com/zenta-dev/zever/dsl/backend/gogenThe test parses the fixture, resolves it, runs gogen, and diffs against the golden package. Use -update only after a deliberate fixture change.
Read the IR
Section titled “Read the IR”ir.Schema is the contract with backends. Backends read schema.Modules[*].Entities (the zenorm backend ignores services entirely) and never re-parse source.
| IR element | Contents |
|---|---|
Schema.Modules |
Resolved modules keyed by directory-derived name |
| Module entities | Bound field types, primary/unique/default metadata |
| Module services | Bound RPC signatures with HTTP/auth bindings |
ErrorCode table |
gRPC-canonical codes shared by all backends |
Change taxonomy |
Additive vs breaking schema diffs |
Catch breaking changes in CI
Section titled “Catch breaking changes in CI”breaking diffs old and new schemas. Wire it into CI so a removed field or renamed RPC fails the build before clients break:
zever breaking schema-old/*.zen -- schema/*.zenFull command shape: zever breaking <old-files...> -- <new-files...>. Each side takes files, globs, or bare directories (recursively discovered for .zen files). Exit code is non-zero when any breaking change is found, so it gates directly. --json emits a machine-readable envelope (changes, breaking count) instead of the human list. Additions and new @validate rules report as informational and still exit zero.
| Change class | Examples | CI posture |
|---|---|---|
| Additive | New entity, new field, new RPC, new @validate, a field renamed with @renamed_from |
Pass (reported as informational) |
| Breaking | Removed entity, field, or RPC (so renaming an RPC), changed field type, optionality, params, return type, or HTTP method or path | Exit non-zero, fail the build |
Ready-to-copy GitHub workflow (diffs the PR schema against main):
- name: Check for breaking schema changes run: | mkdir -p /tmp/old-schema git fetch origin main --depth=1 for f in schema/*.zen; do git show origin/main:"$f" > "/tmp/old-schema/$(basename "$f")" 2>/dev/null || true done zever breaking /tmp/old-schema -- schemaDesign decisions and trade-offs
Section titled “Design decisions and trade-offs”- The parser and resolver never panic on user input: per-declaration recovery plus a depth guard keep hostile files from crashing the build.
- After touching
token.Keywordsorresolver.ScalarTypeNames, runmake generate. Editor grammars derive from those tables viatools/gengrammar, and a parity test fails the build on drift. - Fuzz tests run seed corpora as unit tests. Add seeds for new syntax.
Troubleshooting
Section titled “Troubleshooting”Resolve reports an unknown type
Cause: the name is not a scalar and no entity declares it in scope. Fix: check module paths for cross-module references.
A relation mismatch diagnostic appears
Cause: the two sides disagree on the foreign key column. Fix: align both @foreign_key(...) spellings.
A golden test fails after a grammar change
Cause: keywords or scalar names changed. Fix: run with -update, review the diff, then make generate.
The schema is empty with no diagnostics
Cause: inputs were not parser-produced []*ast.File, or nothing parsed. Fix: check per-declaration recovery output.