Skip to content

feat(#461): select callback-lifecycle shape from manifest data, not the backend enum - #529

Merged
apotema merged 1 commit into
mainfrom
feat/461-lifecycle-shape-manifest
Jul 3, 2026
Merged

apotema merged 1 commit into
mainfrom
feat/461-lifecycle-shape-manifest

Conversation

@apotema

@apotema apotema commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

What

The generated main.zig's callback-lifecycle shape (runner / cleanup / allocator / gui-event / imgui-dispatch / preview / Android-register) was chosen by cfg.backend == .sokol / is_bgfx_android enum branches in lifecycle/render.zig — the last per-backend codegen keyed off the closed Backend enum. This moves the selection to manifest data, so the enum stops driving codegen.

This is PR 1 of the "make the Backend enum stop driving codegen" follow-on to the completed v2-manifest cutover (#461 steps 1–11). It lands the mechanism and proves it byte-identical in isolation; production behavior is unchanged (the external backend manifests don't declare .lifecycle yet — that's the next PR).

Changes

  • Schema (manifest_v2.zig): extend PlatformEntry.Lifecycle with imgui_dispatch, preview (none/callback_basic/sokol_readback), android_register (none/bgfx_shell). Additive + defaulted → every existing manifest parses unchanged; an undeclared callback backend keeps the safe third-party shape.
  • Selection (render.zig): build LifecycleShape from the manifest lifecycle_ovr first; the cfg.backend == .sokol / is_bgfx_android literals remain only as a byte-identical fallback for a built-in whose (older) manifest predates these fields — deleted in the final Epic: build-graph manifest v2 + build hook (#453 item 3) #461 step.
  • Privilege gate: preview = .sokol_readback and android_register = .bgfx_shell emit backend-private Zig, so they're privileged — a new backend_registry.assertLifecyclePrivilege (wired into v2 validation in root.zig) rejects any non-labelle.* provider that declares them, preserving the third-party foot-gun protection the enum implicitly gave.
  • Fixtures: declare the full shape in backends/sokol (all 4 callback platforms) + backends/bgfx_v2 (android) so codegen tests exercise the manifest path, not the fallback.

Proof

  • Differential tests (main_zig_tests.zig): generate sokol-desktop and bgfx-android main.zig twice, varying ONLY lifecycle_override (null → enum branch; the decl → manifest branch), assert byte-identical.
  • Mutation-verified: temporarily corrupting the enum fallback makes the differential test fail — confirming the two paths are genuinely distinct and the manifest path is live (not silently falling back).
  • Gate tests: a third-party (someone.vulkan) declaring a privileged block is rejected; a labelle.* provider is accepted.
  • Existing goldens/anchors unchanged. zig build + zig build test green.

Not in this PR (the cascade)

  1. add the .lifecycle shape to the external labelle-sokol/labelle-bgfx v2 manifests + release → 3. bump the assembler pins (production built-ins now declare their shape) → 4. delete the enum fallback branches. Refs Epic: build-graph manifest v2 + build hook (#453 item 3) #461.

https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog

Summary by CodeRabbit

  • New Features

    • Added manifest-driven lifecycle settings for multiple platforms, including GUI dispatch, preview/readback handling, and Android callback registration.
    • Expanded support for platform-specific callback behavior in generated output.
  • Bug Fixes

    • Lifecycle overrides now take precedence over built-in fallback behavior, so generated app code matches the manifest more reliably.
    • Added validation to block privileged lifecycle options unless they come from an approved backend namespace.

…ot the backend enum

The generated main.zig's callback-lifecycle shape (runner / cleanup /
allocator / gui-event / imgui-dispatch / preview / Android-register) was
chosen by `cfg.backend == .sokol` / `is_bgfx_android` enum branches in
`lifecycle/render.zig` — the last per-backend codegen the assembler keys off
the closed `Backend` enum. Move the SELECTION to manifest data so the enum
stops driving codegen (#461, the literal-agnosticism follow-on to the
completed v2-manifest cutover).

- Extend `PlatformEntry.Lifecycle` (manifest_v2) with `imgui_dispatch`,
  `preview` (none/callback_basic/sokol_readback) and `android_register`
  (none/bgfx_shell). Additive + defaulted, so every existing manifest parses
  unchanged and an undeclared callback backend keeps the safe third-party
  shape.
- `render.zig` now builds `LifecycleShape` from the manifest `lifecycle_ovr`
  FIRST; the `cfg.backend == .sokol` / `is_bgfx_android` literals remain only
  as a byte-identical FALLBACK for a built-in whose (older) manifest predates
  these fields — deleted in the final #461 step once the external manifests
  declare their shape.
- `preview = .sokol_readback` and `android_register = .bgfx_shell` emit
  backend-PRIVATE Zig, so they are PRIVILEGED: a new
  `backend_registry.assertLifecyclePrivilege` (wired into the v2 validation in
  root.zig) rejects any non-`labelle.*` provider that declares them, preserving
  the third-party foot-gun protection the enum implicitly gave.
- Declare the full shape in the in-tree fixtures (`backends/sokol` all four
  callback platforms; `backends/bgfx_v2` android), so codegen tests exercise
  the manifest path — not the fallback.

Proof: new differential tests generate the sokol-desktop and bgfx-android
main.zig twice, varying ONLY `lifecycle_override` (null → enum branch, the
decl → manifest branch), and assert byte-identical output. Mutation-verified
the two paths are genuinely distinct + live. Existing goldens/anchors
unchanged. `zig build` + `zig build test` green. Production behavior is
untouched (external manifests don't declare `.lifecycle` yet — that's the next
PR); this lands the mechanism + proves it byte-identical in isolation.

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces manifest-declared privileged lifecycle configuration for backend callback shapes (preview readback, Android registration). It extends the v2 manifest schema, updates bgfx/sokol manifests with lifecycle data, modifies codegen render logic to prefer manifest-declared shapes, and adds namespace-based validation restricting privileged declarations to labelle.*-owned backends.

Changes

Privileged manifest lifecycle declarations

Layer / File(s) Summary
Lifecycle schema and privilege detection
src/codegen/manifest_v2.zig
Adds imgui_dispatch, preview, android_register fields with Preview/AndroidRegister enums to Lifecycle, and adds declaresPrivilegedLifecycle() to detect privileged selections across platforms.
Backend manifest data declarations
backends/bgfx_v2/backend.manifest.v2.zon, backends/sokol/backend.manifest.v2.zon
Adds .lifecycle blocks to bgfx Android and sokol desktop/ios/android/wasm platforms declaring runner, cleanup, allocator, GUI, imgui dispatch, and preview/android_register settings.
Codegen render precedence
src/codegen/lifecycle/render.zig
Reorders callback lifecycle shape selection so manifest-declared lifecycle_ovr data takes precedence over built-in sokol/bgfx-android enum branches, mapping preview/android/imgui_dispatch directly from manifest fields.
Privilege validation and wiring
src/backend_registry.zig, src/root.zig
Adds assertLifecyclePrivilege enforcing that privileged lifecycle declarations require a canonical .id under the labelle.* namespace, wires it into validateProviderContracts, and adds unit tests.
Regression fixtures and generation tests
test/helpers.zig, test/main_zig_tests.zig
Adds lifecycle declaration fixtures and #461 regression tests verifying byte-identical generated output between enum-path and manifest-declared lifecycle for sokol and bgfx Android.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Root as root.zig
    participant Registry as backend_registry.zig
    participant Manifest as manifest_v2.zig
    participant Render as render.zig

    Root->>Manifest: parse backend.manifest.v2.zon
    Manifest-->>Root: declaresPrivilegedLifecycle() result
    Root->>Registry: assertLifecyclePrivilege(cfg, declares_privileged, manifest_id)
    alt declares_privileged and id not under labelle.*
        Registry-->>Root: error.PrivilegedLifecycleRequiresReservedNamespace
    else allowed or bundled backend
        Registry-->>Root: ok
        Root->>Render: generate main.zig using lifecycle_ovr
        Render-->>Root: LifecycleShape (runner, preview, android, imgui_dispatch)
    end
Loading

Possibly related issues

Possibly related PRs

Poem

A rabbit hopped through manifest glades,
Found lifecycle blocks in tidy shades,
"labelle.* only!" the registry cheers,
Privileged previews, no more fears. 🐇
Byte-for-byte, the tests agree —
Same main.zig, new pedigree! ✨

🚥 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: lifecycle shape selection now comes from manifest data instead of backend enum branches.
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/461-lifecycle-shape-manifest

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

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

🧹 Nitpick comments (3)
test/main_zig_tests.zig (1)

255-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Differential tests look correct; consider extending coverage to other sokol platforms.

Both tests correctly isolate the lifecycle_override threadlocal with defer, and the byte-identity assertions plus sanity substring checks (_sokol_preview, android_app.setInitCallback(&gameInit)) are appropriate anchors for the enum→manifest migration.

Per the PR objectives, the sokol manifest additions cover desktop/iOS/Android/wasm platforms, but this differential guard only exercises sokol desktop. If the other sokol platform lifecycle declarations (mobile/wasm) diverge from their enum-path shape, that regression wouldn't be caught here.

Example extension sketch (sokol Android, mirroring the desktop test)
+    test "sokol android: manifest-declared lifecycle == enum-path main (byte-identical, `#461`)" {
+        const enum_main = try generate.generateMainZigFromTemplate(std.testing.allocator, engine_template, .{ .y_axis = .up,
+            .name = "test-game",
+            .backend = .sokol,
+            .platform = .android,
+            .ecs = .mock,
+        }, sokol_mobile_lifecycle, empty_entries, empty_names, empty_names, empty_scene_manifests, empty_names, empty_names, empty_names, empty_names, empty_names, empty_names, empty_names, empty_plugin_events, empty_plugin_flow_nodes, empty_plugin_pin_styles, empty_plugin_coercions);
+        defer std.testing.allocator.free(enum_main);
+
+        generate.main_template.lifecycle_override = h.sokol_android_lifecycle_decl; // new fixture
+        defer generate.main_template.lifecycle_override = null;
+        const manifest_main = try generate.generateMainZigFromTemplate(std.testing.allocator, engine_template, .{ .y_axis = .up,
+            .name = "test-game",
+            .backend = .sokol,
+            .platform = .android,
+            .ecs = .mock,
+        }, sokol_mobile_lifecycle, empty_entries, empty_names, empty_names, empty_scene_manifests, empty_names, empty_names, empty_names, empty_names, empty_names, empty_names, empty_names, empty_plugin_events, empty_plugin_flow_nodes, empty_plugin_pin_styles, empty_plugin_coercions);
+        defer std.testing.allocator.free(manifest_main);
+
+        try std.testing.expectEqualStrings(enum_main, manifest_main);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main_zig_tests.zig` around lines 255 - 300, The differential coverage
only verifies sokol desktop, so extend the same byte-identical enum-path vs
manifest-declared lifecycle check in main_zig_tests.zig to the other sokol
platform cases covered by the PR (especially mobile/wasm variants). Reuse
generate.generateMainZigFromTemplate and the lifecycle_override threadlocal
pattern, and add platform-specific sanity anchors alongside the existing
sokol_lifecycle test so regressions in those lifecycle declarations are also
caught.
src/codegen/manifest_v2.zig (1)

288-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM overall; consider adding direct test coverage for the new fields.

The new imgui_dispatch, preview, android_register fields and their defaults are consistent with the documented privilege model. However, synthetic_v2 (the fixture exercised by this file's own tests) is not updated to include these fields, so declaresPrivilegedLifecycle() and the new enum fields have no direct unit test within this file — coverage currently relies entirely on downstream backend_registry.zig tests and the real fixture manifests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codegen/manifest_v2.zig` around lines 288 - 339, Add direct unit-test
coverage for the new Lifecycle fields in manifest_v2.zig by updating the
synthetic_v2 fixture so it declares imgui_dispatch, preview, and
android_register with the documented defaults and privilege combinations. Then
extend the tests around declaresPrivilegedLifecycle() and the Lifecycle enum
handling to assert the fixture exercises both privileged and unprivileged cases,
instead of relying only on backend_registry.zig and external manifests.
src/codegen/lifecycle/render.zig (1)

306-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale comment: lifecycle_ovr is now matched first, not sokol/bgfx-android.

This block still states "Sokol/bgfx-android are matched FIRST, so the declared branch only ever covers a genuine non-enum-tag callback backend." After this change the if (lifecycle_ovr) |decl| branch (Line 316) is evaluated before the cfg.backend == .sokol / is_bgfx_android branches, so a built-in whose v2 manifest declares its lifecycle now takes the declared branch too. The inline comment at Lines 317-327 already describes the new precedence correctly, so this header contradicts it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codegen/lifecycle/render.zig` around lines 306 - 315, Update the stale
block comment in render.zig so it matches the actual precedence in the callback
lifecycle selection logic: the `lifecycle_ovr` declared branch is now checked
before the `cfg.backend == .sokol` and `is_bgfx_android` branches. Adjust the
wording near `use_callback_lifecycle` to reflect that built-ins with a v2
manifest lifecycle can also take the declared branch, and remove the outdated
claim that Sokol/bgfx-android are matched first.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/codegen/lifecycle/render.zig`:
- Around line 306-315: Update the stale block comment in render.zig so it
matches the actual precedence in the callback lifecycle selection logic: the
`lifecycle_ovr` declared branch is now checked before the `cfg.backend ==
.sokol` and `is_bgfx_android` branches. Adjust the wording near
`use_callback_lifecycle` to reflect that built-ins with a v2 manifest lifecycle
can also take the declared branch, and remove the outdated claim that
Sokol/bgfx-android are matched first.

In `@src/codegen/manifest_v2.zig`:
- Around line 288-339: Add direct unit-test coverage for the new Lifecycle
fields in manifest_v2.zig by updating the synthetic_v2 fixture so it declares
imgui_dispatch, preview, and android_register with the documented defaults and
privilege combinations. Then extend the tests around
declaresPrivilegedLifecycle() and the Lifecycle enum handling to assert the
fixture exercises both privileged and unprivileged cases, instead of relying
only on backend_registry.zig and external manifests.

In `@test/main_zig_tests.zig`:
- Around line 255-300: The differential coverage only verifies sokol desktop, so
extend the same byte-identical enum-path vs manifest-declared lifecycle check in
main_zig_tests.zig to the other sokol platform cases covered by the PR
(especially mobile/wasm variants). Reuse generate.generateMainZigFromTemplate
and the lifecycle_override threadlocal pattern, and add platform-specific sanity
anchors alongside the existing sokol_lifecycle test so regressions in those
lifecycle declarations are also caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c88b9a8-513b-4b5b-b64a-331c169e7fb6

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0f216 and 8637d8e.

📒 Files selected for processing (8)
  • backends/bgfx_v2/backend.manifest.v2.zon
  • backends/sokol/backend.manifest.v2.zon
  • src/backend_registry.zig
  • src/codegen/lifecycle/render.zig
  • src/codegen/manifest_v2.zig
  • src/root.zig
  • test/helpers.zig
  • test/main_zig_tests.zig

@apotema
apotema merged commit b199555 into main Jul 3, 2026
4 checks passed
apotema added a commit that referenced this pull request Jul 3, 2026
…e their lifecycle shape (#530)

labelle-sokol 0.2.1 and labelle-bgfx 0.6.4 declare the callback-lifecycle
SHAPE in their v2 manifests (labelle-sokol#8, labelle-bgfx#28). Bumping the
builtinProvider pins means PRODUCTION generate now selects the sokol/bgfx
callback shape from manifest data (assembler#529's manifest path), not the
`cfg.backend == .sokol` / `is_bgfx_android` enum branches. Those branches
are now dead in production and get deleted in the final #461 step.

Additive/no-behavior-change: the manifest-declared shape is byte-identical to
the enum shape (proven by #529's differential tests + unchanged goldens).

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog
apotema added a commit that referenced this pull request Jul 3, 2026
…#532)

* feat(#461): fully remove the v1/legacy backend codegen path

The v1/enum build-graph splice — the raw-text `build_fragments` renderers and
the `switch (cfg.backend)` template sections — was the dead fallback beneath the
manifest-v2 codegen after #529/#530/#531 made v2 the sole production path. This
deletes it: the v2 `backend.manifest.v2.zon` is now the only codegen input.

- manifest_splice.zig: delete the v1 `BackendManifest` struct + renderers
  (loadManifest/renderBackendDepSection/renderLinkSection/loopStyle/
  mainLoopTemplateRel/…); keep the resolve-time survivors (requireManifestIfExternal,
  manifestPathEnabled, ProviderManifest/loadProviderManifest).
- manifest_v2.zig: parseManifest/loadNamedManifest now return BackendManifestV2
  directly and REJECT manifest_version < 2 (v1 is no longer a codegen input).
- build_files.zig: v2 manifest is mandatory (early guard); delete the enum
  backend-dep/link/wasm-emsdk switches + usesEnumBackendPath/externalUsesEnumPath.
- root.zig: loadBackendTemplate/resolveLoopStyleOverride/validateProviderContracts
  are v2-only.
- lifecycle/render.zig: drop the `cfg.backend == .sokol`/`is_bgfx_android` enum
  residual; run-loop + shape come from the manifest.
- build_zig.txt: delete the 30 enum-only sections (backend_*/link_*/header_ios|
  android|wasm/ios_deps/android_deps/android_link*/android_package/wasm_*).
- packager.zig owns the apk/web fixtures (the enum sections are gone).
- tests: the sokol-desktop byte anchor becomes committed goldens; enum-path and
  v1-fixture tests removed; remove the sdl / sokol_v1only / sokol_v1inv2 fixtures.

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog

* test(external-null): bump nullfixture pin to v0.3.0 (v2 manifest)

The v1/enum codegen path is removed, so the external-null example's nullfixture
backend must ship a v2 manifest. nullfixture-backend v0.3.0 adds one; bump the
pin so the remote-fetch → generate → build integration exercises the v2 path.

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog

* refactor: androidNeedsAppImport is allocation-free (gemini)

The effective root alias equals 'backend_app' iff root_alias is 'backend_app'
or (no root_alias) the name is 'app' — a direct comparison, no allocPrint/dupe.
Drops the allocator param + error return. Addresses the gemini finding on #532.

Claude-Session: https://claude.ai/code/session_01P7B7UzgrWEbBYLT3YBrAog
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