Skip to content

feat(codegen): verify external backend against core contracts (#386 Phase 6b) - #410

Merged
apotema merged 1 commit into
mainfrom
feat/386-phase6b-contract-verify
Jun 29, 2026
Merged

apotema merged 1 commit into
mainfrom
feat/386-phase6b-contract-verify

Conversation

@apotema

@apotema apotema commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

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 @compileError from inside engine wiring.

Generated main.zig now emits a module-root guard for external backends only:

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 — rather than deep in engine internals.

Approach

Picked the comptime-guard-in-generated-code approach (vs. an assembler-side zig-shelling harness or a verify-backend build step). It's small, robust, reuses core's proven assert{Backend,Window,Input}, adds no assembler↔zig coupling, and backend_gfx/backend_window/backend_input + labelle-core are already root deps of main.zig so 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 null backend, 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 guard
  • built-in backend emits NO contract guard (byte-identical)
  • Full zig build test green.

End-to-end verification

Conformed the fixture to the window contract — github.com/labelle-toolkit/labelle-nullfixture-backend@v0.2.0 (added width/height/frameDuration/requestQuit):

  • Pass: the conformant backend generates → zig build clean → runs to a bounded frame count and exits.
  • Reject: a copy with width() removed fails the build with Window impl does not satisfy the window contract — missing decl(s): width at the generated assertWindow(@import("backend_window")) call.

Follow-ups

  • Signature/behavior conformance suites (RFC cross-cutting workstream).
  • Conform the in-tree 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

    • Generated output now includes backend contract checks when using external backends.
    • The generated main file also includes a clear marker and verification block for required backend modules.
  • Bug Fixes

    • Built-in backends no longer emit the external backend verification block, keeping generated output clean and appropriate for each setup.
  • Tests

    • Added coverage for both external and built-in backend generation paths.

…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.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e84f59ac-4008-4a59-9e9d-fe36a7807f24

📥 Commits

Reviewing files that changed from the base of the PR and between 91ab9e4 and 8ff3b7d.

📒 Files selected for processing (4)
  • src/codegen/blocks/imports.zig
  • src/codegen/context.zig
  • src/codegen/main_template.zig
  • test/main_zig_tests.zig

📝 Walkthrough

Walkthrough

Adds a writeBackendContractCheck method to the codegen ImportsMixin that emits a comptime block asserting backend/window/input module contracts for external backends. The method is re-exported on Codegen and called in generateMainZigFromTemplate. Two tests verify the guard's presence for external backends and absence for built-in backends.

Changes

Backend contract check codegen

Layer / File(s) Summary
writeBackendContractCheck implementation and wiring
src/codegen/blocks/imports.zig, src/codegen/context.zig, src/codegen/main_template.zig
New writeBackendContractCheck method in ImportsMixin emits a module-root comment and comptime assertions (assertBackend, assertWindow, assertInput) for external backends; no-op for built-ins. Re-exported on Codegen and called after writeHookImportsBlock in the template emitter.
Tests for external and built-in backend guard
test/main_zig_tests.zig
Two new tests: one asserts the contract guards appear in generated output for an external backend_package; the other asserts the guards are absent for built-in backends.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐇 A comptime guard hops into place,
Asserting backends with elegant grace.
External or built-in, the check will know—
assertWindow, assertInput, watch the contracts glow!
No mismatched module shall slip past my nose. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: codegen now verifies external backends against core contracts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/386-phase6b-contract-verify

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +78 to +83
\\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"));
\\}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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"));
                \\}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@apotema
apotema merged commit 023bc16 into main Jun 29, 2026
6 checks passed
@apotema
apotema deleted the feat/386-phase6b-contract-verify branch June 29, 2026 18:52
apotema added a commit that referenced this pull request Jun 30, 2026
… (#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant