Skip to content
Zever
Home

Resolver and IR

How the compiler frontend resolves .zen schemas into an IR and reports every diagnostic in one run.

3 min readEdit on GitHub

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.

source text → lexer → parser → ast → resolver → ir.Schema
↘ compile → backend outputs

No 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 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:

  1. 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.
  2. Bind types. Resolve enums, entities, and message fields against scalars plus declared types. Unknown names diagnose and continue.
  3. Relations and indexes. Verify both sides of has_many / belongs_to agree on the foreign key, and resolve indexes.
  4. Services and RPCs. Validate HTTP templates against params, @validate expressions, and the ErrorCode table.
  5. 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:

Terminal window
go test ./dsl/backend/gogen/ -run TestGoldenFiles -v

Expected output:

--- PASS: TestGoldenFiles (0.00s)
PASS
ok github.com/zenta-dev/zever/dsl/backend/gogen

The test parses the fixture, resolves it, runs gogen, and diffs against the golden package. Use -update only after a deliberate fixture change.

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

breaking diffs old and new schemas. Wire it into CI so a removed field or renamed RPC fails the build before clients break:

Terminal window
zever breaking schema-old/*.zen -- schema/*.zen

Full 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 -- schema
  • 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.Keywords or resolver.ScalarTypeNames, run make generate. Editor grammars derive from those tables via tools/gengrammar, and a parity test fails the build on drift.
  • Fuzz tests run seed corpora as unit tests. Add seeds for new syntax.
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.