feat(declare): generic .languages declare reader — native rust declares (#619/#774) - #621
Conversation
…ares (#619/#774) Implement the RFC-LANGUAGE-PLUGINS rev 17 §7 declare invocation contract: a GENERIC reader of the plugin manifest's `.languages` capability rows, so a native language (rust) declares components/events through the IDENTICAL `.declare = { .tool, .dir, .events }` shape an embedded language uses — no new field, no per-mechanism discriminant, zero cargo knowledge in the assembler. - plugin_manifest/plugin.zig: parse the `.languages` table (LanguageRow + DeclareCapability + LanguageKind), a `languageRow(name)` lookup, deinit, and the barrel re-exports. Unknown row keys (a future `.transpile`) ride the manifest-wide ignore_unknown_fields (forward-compat test included). - scripting_declare.zig: `runPhase` gains a generic branch taken ONLY for a language absent from the hardcoded DECLARE_RUNNERS table (rust). It resolves the plugin package, reads the `.languages` declare row, gates events by the self-describing `.events` flag (no version table), builds the tool via the same `zig build <.tool>` machinery every runner uses, and runs it with a PERSISTENT `--cache-dir <output>/declare-tool/<tool>-cache` + the declaration files — HASHING the inputs (SHA-256 of path+contents) to skip re-invoking the tool when unchanged (rev-17 invariant 1; a cached schema JSON is reused). The hardcoded lua/ruby (and ts/crystal) rows are untouched — they short-circuit the branch and keep the table as fallback. The declare-phase tail (events-none gate, collisions, renders) is extracted to a shared `finalizeSchema` both paths call; `runDeclareTool` splits into a raw `execDeclareTool` (optional `--cache-dir`) + parse. A no-declare-capability language falls through cleanly even on a degenerate plugin setup (the typescript-skip invariant). - root.zig: collect `components/*.<ext>` + `events/*.<ext>` for the NATIVE family too (previously embed-only, so a native splice never reached declare), routed to `runPhase` while `s.scripts` stays empty — gameplay scripts are compiler-staged, never fed to the declare probe. Verified: assembler compiles + `zig build test` baseline-relative clean (the touched suites' e2e failures are byte-identical to origin/main's pre-existing Windows env failures; the new `.languages` parse tests pass; the typescript unit-skip test stays green). The generic-path e2e (fake-tool, hash-skip cache assertion) runs on CI (POSIX-sh fixture, Windows-skipped like its siblings). The CI rust-example assertion flip stays in the migration PR (the rust-example still uses .zig components — zero .rs decl files — so the phase no-ops there). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1zt5TCHYqUJBjswJKjnCo
|
Warning Review limit reached
Next review available in: 18 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements generic language capability rows in the plugin manifest according to RFC-LANGUAGE-PLUGINS rev 17, enabling languages like Rust to declare capabilities generically. It introduces manifest parsing for .languages rows, integrates a generic declare phase with input hashing to skip redundant tool execution, and shares schema finalization logic. Feedback recommends safely unwrapping optional component_embeds and event_embeds slices in src/root.zig to avoid potential runtime panics, and catching parsing errors from the cached schema.json in src/scripting_declare.zig to gracefully fall back to executing the declare tool fresh if the cache is corrupted.
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.
| const nd = try allocator.alloc(scripting_splice.EmbedScript, component_embeds.?.len + event_embeds.?.len); | ||
| @memcpy(nd[0..component_embeds.?.len], component_embeds.?); | ||
| @memcpy(nd[component_embeds.?.len..], event_embeds.?); | ||
| native_decl_embeds = nd; |
There was a problem hiding this comment.
Using .? on component_embeds and event_embeds is unsafe because these optional slices can be null if no component or event files are found. To prevent potential runtime panics, use safe optional unwrapping with if or orelse to determine the lengths and copy the slices defensively.
const comp_len = if (component_embeds) |ce| ce.len else 0;
const event_len = if (event_embeds) |ee| ee.len else 0;
const nd = try allocator.alloc(scripting_splice.EmbedScript, comp_len + event_len);
if (component_embeds) |ce| @memcpy(nd[0..comp_len], ce);
if (event_embeds) |ee| @memcpy(nd[comp_len..], ee);
native_decl_embeds = nd;
| if (cwd.readFileAlloc(io, json_path, allocator, .limited(MAX_SCHEMA_BYTES))) |json| { | ||
| defer allocator.free(json); | ||
| return parseSchema(allocator, json); | ||
| } else |_| {} |
There was a problem hiding this comment.
If the cached schema.json is corrupted or invalid, parseSchema will fail and propagate the error, causing the entire generation phase to fail. Since the cache is best-effort, it is more robust to catch any parsing errors from the cached file and fall back to executing the declare tool fresh.
if (cwd.readFileAlloc(io, json_path, allocator, .limited(MAX_SCHEMA_BYTES))) |json| {
defer allocator.free(json);
if (parseSchema(allocator, json)) |schema| {
return schema;
} else |_| {}
} else |_| {}
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50d7b44bd9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const json_path = try std.fs.path.join(allocator, &.{ skip_dir, "schema.json" }); | ||
| defer allocator.free(json_path); | ||
|
|
||
| const digest = try declInputsDigestHex(allocator, opts.target_dir, opts.script_files); |
There was a problem hiding this comment.
Include tool identity in the declare cache key
When a project upgrades or changes the pinned scripting plugin/labelle-declare-rs tool while the declaration files are unchanged, this digest still matches the previous run, so lines below read .assembler-skip/schema.json and never invoke the newly built tool. That can leave generated components/events stale, or keep reusing a cached invalid schema after a tool/parser fix; include the resolved tool/package identity (or otherwise invalidate the cache on tool changes) in the skip key.
Useful? React with 👍 / 👎.
Open the assembler's language VOCABULARY so a language plugin can add a brand-new language (the RFC's "python") through a `plugin.labelle` `.languages` capability row alone — zero assembler changes. This closes the last per-language hardcode the row-driven declare/transpile/embed/ native machinery (already on main via #621–#624) sat on top of: the closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy gate, the shared-dir scan, the plugin-event consumption scan, and `requires_language` validation all consulted, and which rejected any language it had never heard of before the row machinery ever ran. What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"): - `language_policy.Vocabulary` — the project's OPEN language set: the frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a frozen language shadows its extension set); a row language the tables never heard of is admitted exactly like a built-in. `EMPTY` (no rows) reproduces the pre-#619 closed-vocabulary behavior bit for bit. - `validateDeclaredLanguage` — the vocabulary gate, re-homed from `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy` where the declaring plugin's manifest rows are in hand. Self-describing capabilities replace the closed table: a pointed error naming BOTH the built-ins and the manifest rows, no version compare. - `resolveProjectLanguage` / manifest-load `requires_language` now gate on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one constraint the generated `.language = .<name>` enum literal genuinely imposes); the vocabulary check runs at generate where rows are visible. - `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary` and police row languages by the row's extensions (frozen ∪ rows). - The plugin-event consumption scan gains the active splice's script extensions (`filterConsumedEvents(extra_extensions)`) so a row language's sources (`.py`) are not invisible to the scan and its subscriptions silently elided — the comptime `scanned_extensions` covers only the frozen built-ins. Migration map (hardcoded path → row-driven): - closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage` → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage` - `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf` (row extensions primary, frozen fallback) - manifest-load closed-table `requires_language` reject → shape-only reject + generate-time vocabulary check - (already on main, unchanged) declare → `runGenericDeclarePhase` rows; embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS` / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay FROZEN FALLBACKS for row-less (pre-migration) manifests. Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice — frozen fallback (no row) vs an equivalent `.languages` row — and asserts the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows shifts nothing. Every existing golden/e2e suite is unchanged. Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language "quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row generates end-to-end (embed registration, `.language = .quokka` dep arg, the four splice touchpoints), a foreign frozen-language file (`.rb`) in the same project still errors (combined vocabulary polices both ways), and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the mechanical proof the generate rode rows, not a hidden branch. Closes #619 Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
Open the assembler's language VOCABULARY so a language plugin can add a brand-new language (the RFC's "python") through a `plugin.labelle` `.languages` capability row alone — zero assembler changes. This closes the last per-language hardcode the row-driven declare/transpile/embed/ native machinery (already on main via #621–#624) sat on top of: the closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy gate, the shared-dir scan, the plugin-event consumption scan, and `requires_language` validation all consulted, and which rejected any language it had never heard of before the row machinery ever ran. What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"): - `language_policy.Vocabulary` — the project's OPEN language set: the frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a frozen language shadows its extension set); a row language the tables never heard of is admitted exactly like a built-in. `EMPTY` (no rows) reproduces the pre-#619 closed-vocabulary behavior bit for bit. - `validateDeclaredLanguage` — the vocabulary gate, re-homed from `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy` where the declaring plugin's manifest rows are in hand. Self-describing capabilities replace the closed table: a pointed error naming BOTH the built-ins and the manifest rows, no version compare. - `resolveProjectLanguage` / manifest-load `requires_language` now gate on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one constraint the generated `.language = .<name>` enum literal genuinely imposes); the vocabulary check runs at generate where rows are visible. - `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary` and police row languages by the row's extensions (frozen ∪ rows). - The plugin-event consumption scan gains the active splice's script extensions (`filterConsumedEvents(extra_extensions)`) so a row language's sources (`.py`) are not invisible to the scan and its subscriptions silently elided — the comptime `scanned_extensions` covers only the frozen built-ins. Migration map (hardcoded path → row-driven): - closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage` → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage` - `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf` (row extensions primary, frozen fallback) - manifest-load closed-table `requires_language` reject → shape-only reject + generate-time vocabulary check - (already on main, unchanged) declare → `runGenericDeclarePhase` rows; embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS` / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay FROZEN FALLBACKS for row-less (pre-migration) manifests. Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice — frozen fallback (no row) vs an equivalent `.languages` row — and asserts the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows shifts nothing. Every existing golden/e2e suite is unchanged. Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language "quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row generates end-to-end (embed registration, `.language = .quokka` dep arg, the four splice touchpoints), a foreign frozen-language file (`.rb`) in the same project still errors (combined vocabulary polices both ways), and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the mechanical proof the generate rode rows, not a hidden branch. Closes #619 Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
#643) * feat: language capability rows — assembler language-agnosticism (#619) Open the assembler's language VOCABULARY so a language plugin can add a brand-new language (the RFC's "python") through a `plugin.labelle` `.languages` capability row alone — zero assembler changes. This closes the last per-language hardcode the row-driven declare/transpile/embed/ native machinery (already on main via #621–#624) sat on top of: the closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy gate, the shared-dir scan, the plugin-event consumption scan, and `requires_language` validation all consulted, and which rejected any language it had never heard of before the row machinery ever ran. What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"): - `language_policy.Vocabulary` — the project's OPEN language set: the frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a frozen language shadows its extension set); a row language the tables never heard of is admitted exactly like a built-in. `EMPTY` (no rows) reproduces the pre-#619 closed-vocabulary behavior bit for bit. - `validateDeclaredLanguage` — the vocabulary gate, re-homed from `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy` where the declaring plugin's manifest rows are in hand. Self-describing capabilities replace the closed table: a pointed error naming BOTH the built-ins and the manifest rows, no version compare. - `resolveProjectLanguage` / manifest-load `requires_language` now gate on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one constraint the generated `.language = .<name>` enum literal genuinely imposes); the vocabulary check runs at generate where rows are visible. - `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary` and police row languages by the row's extensions (frozen ∪ rows). - The plugin-event consumption scan gains the active splice's script extensions (`filterConsumedEvents(extra_extensions)`) so a row language's sources (`.py`) are not invisible to the scan and its subscriptions silently elided — the comptime `scanned_extensions` covers only the frozen built-ins. Migration map (hardcoded path → row-driven): - closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage` → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage` - `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf` (row extensions primary, frozen fallback) - manifest-load closed-table `requires_language` reject → shape-only reject + generate-time vocabulary check - (already on main, unchanged) declare → `runGenericDeclarePhase` rows; embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS` / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay FROZEN FALLBACKS for row-less (pre-migration) manifests. Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice — frozen fallback (no row) vs an equivalent `.languages` row — and asserts the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows shifts nothing. Every existing golden/e2e suite is unchanged. Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language "quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row generates end-to-end (embed registration, `.language = .quokka` dep arg, the four splice touchpoints), a foreign frozen-language file (`.rb`) in the same project still errors (combined vocabulary polices both ways), and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the mechanical proof the generate rode rows, not a hidden branch. Closes #619 Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM * fix(#619): reject unsafe/extensionless language rows; preserve built-in metadata on shadow (codex #643 P2) Three review fixes on the capability-row vocabulary: 1. Reject language names that break build.zig emission. isLanguageIdentifier now also rejects Zig keywords (`error`/`fn`/`test`/…): the generated `.language = .<name>` enum literal would be a syntax error otherwise. Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and in resolveProjectLanguage/requires_language. (Primitives like i32 stay admissible — `.i32` is a legal enum literal; only keywords break syntax.) 2. Preserve built-in metadata when a row shadows a built-in. Vocabulary.build now UNIONS the frozen scriptExtensions into a shadowing row's extensions (row order first, deduped), so a migrated typescript row declaring only authored `.ts` keeps the frozen emitted `.js`. scanUnitLanguageDirs keeps the built-in's legacy-dir mapping for shadowed languages instead of dropping it. (Splice/declare/transpile metadata — TSC_PLATFORMS, staging geometry — is read directly from the manifest LanguageRow, unaffected.) 3. Reject rows without source extensions. A `.languages` row with empty `.extensions` is rejected at manifest load with a pointed diagnostic — the shared-dir/consumption scans find sources by extension, so an extensionless row would silently elide every source it declares. Tests: keyword-named row rejected, extensionless row rejected, well-formed row loads, shadow-row unions frozen extensions, keyword identifiers rejected. Baseline green (flow_catalog env lanes tolerated). Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM * feat(#619): row-ify #644's csharp runtime-output + toolchain-probe decisions Migrate the two `language == "csharp"` DECISIONS #644 added into capability rows, per #619's design (the module doc #644 shipped explicitly anticipated this): - New LanguageRow capabilities `runtime_output` (the link-less build outputs ARE the runtime payload — stage beside the binary + set the run step's assembly-dir env) and `probe_tools` (opt into the generate-time PATH probe of the language's steps). - scripting_csharp.stagesRuntimeOutputs now takes the resolved `runtime_output` capability instead of hardcoding `== "csharp"`; the ensureStepToolsOnPath call is gated on the resolved `probe_tools`. - The csharp knowledge is DEMOTED to a frozen fallback (scripting_csharp.frozenRuntimeOutput/frozenProbeTools), the same #619 pattern as DECLARE_RUNNERS/EMBED_LANGUAGES/NATIVE_LANGUAGES — the shipped csharp manifest (which predates these capabilities) keeps working byte-for-byte; a NEW runtime-loaded language rides its row alone. - ScriptingSplice.detect resolves both capabilities (row PRIMARY ∪ frozen) and threads them onto the splice; root.zig consumes them. csharp's EMBED compile wiring itself needed NO migration — it was already fully row-driven (csharp has no frozen-table fallback; it resolves entirely via its manifest .languages native row + plugin-declared .language_builds). Only the runtime-output-staging + toolchain-probe seams were csharp-named. Residue (documented, RFC honest boundary): the runtime assembly-dir env var NAME (LABELLE_CS_ASSEMBLY_DIR) stays an assembler-owned constant, and the dev-.csproj IDE aid stays csharp-keyed MSBuild codegen — analogous to the assembler-owned tsconfig codegen; neither is needed for the language-agnostic litmus (which adds an EMBEDDED language). Tests: runtime_output/probe_tools row parse (+ default-false), frozen fallback (csharp opts in, others out), the capability-driven stagesRuntimeOutputs. csharp_splice_tests (#644's e2e) stay green via the frozen fallback — the back-compat proof. Baseline green (flow_catalog lanes). Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
What
Implements the RFC-LANGUAGE-PLUGINS rev 17 §7 declare invocation contract (landed in labelle-engine#777): a generic reader of the plugin manifest's
.languagescapability rows. A native language (rust #774) now declares components/events through the identical.declare = { .tool, .dir, .events }shape an embedded language uses — no new field, no per-mechanism discriminant, zero cargo knowledge in the assembler.Assembler half of the rust declare lane; the
labelle-declare-rstool + the.languagesrust row landed in labelle-scripting#31.How
plugin_manifest/plugin.zig— parse the.languagestable:LanguageRow+DeclareCapability+LanguageKind, alanguageRow(name)lookup, deinit, barrel re-exports. Unknown row keys (a future.transpile) ride the manifest-wideignore_unknown_fields(forward-compat unit test included).scripting_declare.zig—runPhasegains a generic branch taken only for a language absent from the hardcodedDECLARE_RUNNERStable (rust): resolve package + read the.languagesdeclare row, gate events by the self-describing.eventsflag (rev-17's replacement forevents_min_pin), build via the samezig build <.tool>machinery every runner uses, run with a persistent--cache-dir <output>/declare-tool/<tool>-cache+ the declaration files, and hash the inputs (SHA-256 of path+contents) to skip re-invoking when unchanged (rev-17 invariant 1; cached schema JSON reused). lua/ruby (+ ts/crystal) rows are untouched — they short-circuit the branch, table stays as fallback. SharedfinalizeSchematail;runDeclareToolsplits into rawexecDeclareTool(optional--cache-dir) + parse.root.zig— collectcomponents/*.<ext>+events/*.<ext>for the native family too (previously embed-only, so a native splice never reached declare), routed torunPhasewhiles.scriptsstays empty — gameplay scripts are compiler-staged, never fed to the declare probe.Design decisions (from existing conventions)
<output>/declare-tool/. Opaque to the assembler.std.crypto.hash.sha2.Sha256over each declaration file's path+bytes; cached JSON + digest under<cache>/.assembler-skip/(best-effort).Verification
zig build testbaseline-relative clean — the touched suites' e2e failures are byte-identical toorigin/main's pre-existing Windows env failures (AppDataDirUnavailable); the new.languagesparse tests pass; the typescript unit-skip invariant stays green.Scope
The CI rust-example assertion flip stays in the migration PR (rust-example still uses
.zigcomponents → phase no-ops). Closed PR #620's assembler-hardcoded cargo stays closed: rev 17 puts cargo in the tool.🤖 Generated with Claude Code