Repository navigation
feat(codegen): verify external backend against core contracts (#386 Phase 6b) - #410
Conversation
…hase 6b) A fetched out-of-tree backend (#386 Phase 6a) has no enum-path codegen to vet it — a malformed one surfaced as a deep @CompileError from inside engine wiring. Generated main.zig now asserts an EXTERNAL backend's modules satisfy labelle-core's render / window / input contracts up front: comptime { const _backend_contract_core = @import("labelle-core"); _backend_contract_core.assertBackend(@import("backend_gfx")); _backend_contract_core.assertWindow(@import("backend_window")); _backend_contract_core.assertInput(@import("backend_input")); } A missing required decl now fails at this call site with a decl-naming message (e.g. "Window impl does not satisfy the window contract — missing decl(s): width") instead of deep in engine internals. - Emitted at module root alongside the hook imports (backend_gfx/window/input + labelle-core are all root deps of main.zig — no extra wiring). External-only (`cfg.isExternal()`), so built-in backends are byte-identical to before. - Scope: decl-NAME (shape) verification — `assert*` are `@hasDecl`-based. Signature/behavior conformance (the RFC's "Conformance suites" workstream) stays a follow-up. Tests: external backend emits the guard; built-in emits nothing. Verified e2e against github.com/labelle-toolkit/labelle-nullfixture-backend (conformed to the window contract in v0.2.0): the conformant backend builds + runs with the guard active; a copy with `width` removed fails with the clean "missing decl(s): width" message at the assertWindow call.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a ChangesBackend contract check codegen
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces an external-backend contract-verification guard to assert that fetched out-of-tree backends satisfy labelle-core's render, window, and input contracts at compile time. This is implemented via a new writeBackendContractCheck function in the codegen blocks, which is integrated into the main template generation and covered by new unit tests. The reviewer suggested simplifying the generated comptime block by chaining the assertions directly on the @import expression to avoid declaring an intermediate local constant.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| \\comptime { | ||
| \\ const _backend_contract_core = @import("labelle-core"); | ||
| \\ _backend_contract_core.assertBackend(@import("backend_gfx")); | ||
| \\ _backend_contract_core.assertWindow(@import("backend_window")); | ||
| \\ _backend_contract_core.assertInput(@import("backend_input")); | ||
| \\} |
There was a problem hiding this comment.
We can simplify the generated comptime block by chaining the assertions directly on the @import expression. This avoids declaring the intermediate local constant _backend_contract_core, keeping the generated code cleaner and completely eliminating any potential shadowing issues or compiler warnings/errors related to unused or local declarations in the generated file.
\\comptime {
\\ @import("labelle-core").assertBackend(@import("backend_gfx"));
\\ @import("labelle-core").assertWindow(@import("backend_window"));
\\ @import("labelle-core").assertInput(@import("backend_input"));
\\}
There was a problem hiding this comment.
Keeping the named _backend_contract_core local. It's comptime-deduplicated either way (no codegen difference), and one binding read three times scans better in the generated file than three repeated @import("labelle-core") expressions — the goal is for a backend author staring at the failing generated main.zig to immediately see "core asserts my three modules." Style preference; declining.
… (#418) Pluggable-backends Phases 5+6 (epic #386): the assembler can now FETCH, verify, and build out-of-tree backend packages, and bgfx is extracted to its own repo. - #409 6a: remote .backend_package cache-fetch (fetched like a plugin) - #410 6b: contract-verify an external backend (assertBackend/Window/Input) - #411: conform raylib + null windows to the canonical window contract - #412 Phase 5: enum-as-shorthand resolution (a built-in tag can resolve to a package) - #413 6c: bgfx extracted -> github.com/labelle-toolkit/labelle-bgfx; opt-in CI-verified - #414: select the GUI bridge by backend name, not the enum (external backends) - #415: labelle-bgfx v0.2.0 — gamepad sources extracted to their own packages - #416/#417: the two flip-blockers (callback-external guard; android enum-fallthrough) Opt-in today via .backend_package; built-in .backend = .bgfx still ships bundled (the default-flip is a follow-up gated on this release). Built-in backends are byte-identical. External bgfx validated on-device (Galaxy Tab A7): builds, runs crash-free, behaves identically to bundled.
Phase 6b — contract-verify the fetched backend
Follows Phase 6a (#409): now that a remote out-of-tree backend can be fetched, verify it satisfies the core contracts instead of letting a malformed one explode as a deep
@compileErrorfrom inside engine wiring.Generated
main.zignow emits a module-root guard for external backends only:A missing required decl now fails at this call site with a decl-naming message — e.g.
Window impl does not satisfy the window contract — missing decl(s): width— rather than deep in engine internals.Approach
Picked the comptime-guard-in-generated-code approach (vs. an assembler-side
zig-shelling harness or averify-backendbuild step). It's small, robust, reuses core's provenassert{Backend,Window,Input}, adds no assembler↔zig coupling, andbackend_gfx/backend_window/backend_input+labelle-coreare already root deps ofmain.zigso no extra wiring is needed.Built-ins unaffected
The guard is gated on
cfg.isExternal(), so generated output for every built-in backend is byte-identical to before (golden suites green).Scope
Decl-name (shape) verification — core's
assert*are@hasDecl-based. Signature/behavior conformance (the RFC's "Conformance suites" workstream — behavior, not just shape) stays a deliberate follow-up.A finding worth flagging
The fixture is cloned from the built-in
nullbackend, whose window module still uses the legacy surface (getScreenWidth/windowShouldClose/getFrameTime) — the canonical window contract (width/height/frameDuration/requestQuit) has only been adopted by bgfx so far (window conformance is an open epic slice). So this guard correctly rejected the fixture until I conformed it. That's the intended behavior: external backends are manifest-driven/canonical-world and must target the canonical contracts. The built-in backends are gated out, so this PR doesn't force their (pending) conformance.Tests
external backend emits the core-contract verification guardbuilt-in backend emits NO contract guard (byte-identical)zig build testgreen.End-to-end verification
Conformed the fixture to the window contract —
github.com/labelle-toolkit/labelle-nullfixture-backend@v0.2.0(addedwidth/height/frameDuration/requestQuit):zig buildclean → runs to a bounded frame count and exits.width()removed fails the build withWindow impl does not satisfy the window contract — missing decl(s): widthat the generatedassertWindow(@import("backend_window"))call.Follow-ups
null/raylib/sokol/sdl/wgpu window surfaces to the canonical contract (separate epic slice) so they could share the canonical templates.Summary by CodeRabbit
New Features
Bug Fixes
Tests