Declare: rust cargo-probe runner + native-family declaration collection - #620
Declare: rust cargo-probe runner + native-family declaration collection#620apotema wants to merge 1 commit into
Conversation
The native scripting family declares components/events in `.rs` the same way the embed family declares them in `.rb`/`.lua` — but rust has no interpreter, so its DECLARE_RUNNERS entry cannot be a prebuilt exe run over the sources. It is a new `.cargo_probe` runner kind (labelle-engine#774, route (a) — the compile-and-run probe, settled with a spike on the ticket): the assembler GENERATES a probe crate from the game's components/*.rs + events/*.rs, `cargo build`s it against a persistent target dir, runs it, and captures its schema JSON — feeding the SAME `parseSchema` consumer the lua/ruby runners feed (byte-identical schema across every runner). ## The mechanism (scripting_declare.zig) - `DeclareRunner.kind: RunnerKind` discriminant (`exe_over_files` | `cargo_probe`), defaulted so the lua/ruby rows read unchanged; rust is the sole `.cargo_probe` row. Kept a single field so assembler#619's fold of the table into plugin.labelle capability rows stays mechanical. - `runCargoProbe`: capability-probes the plugin's shipped macro module (native/src/labelle.rs — the `component!`/`event!` macros + the pure-Rust `%.14g` emitter, labelle-scripting PR #29; absent → the SAME graceful skip the lua/ruby tool-dir probe gives), generates + builds the probe, runs it. - The generated crate mirrors tools/declare-rs/src/main.rs: `pub mod labelle` recomposed from the shipped file via `#[path]`, one `mod decl_NNNN;` per game declaration file. The emitter recovers declaration order from `(file!(), line!())`, so each file is COPIED into the probe's `src/` under an ordering-encoded name (components first, then events, alphabetical within each — the order the lua/ruby argv carries); `file!()` then sorts on the prefix and reproduces the cross-runner order. Write-if-different keeps the warm re-generate a no-op cargo build. - The absent-tool skip + the events-floor gate are shared across both kinds (`declareToolAbsent`); the lua/ruby `.exe_over_files` branch is left byte-for-byte as it shipped. - `declare_probe_override`: the hermetic-test seam (a prebuilt fake probe run with no args), the rust twin of `declare_tool_override` — the suite never needs a `cargo` on PATH or a network crate fetch. The real generate+build is labelle-scripting's `rust-example` CI. ## The collection (root.zig) `components/*.<ext>` + `events/*.<ext>` now collect for BOTH families (only SCRIPTS differ: embed embeds+registers them, native stages+compiles them). For the native family they feed the declare phase's runner but are NOT concatenated onto `s.scripts` (nothing embeds). A non-declaring native game (no `.rs` declarations) still no-ops at the phase's zero-files gate. ## Tests Hermetic unit tests pin the probe-crate generation (main.rs recomposition + ordered decl modules, the Windows backslash→forward-slash `#[path]` rule, the Cargo.toml feature/dep wiring) and the runner-kind table. An e2e test drives the real `generate` through the cargo-probe override: a `components/hunger.rs` declaration codegens `scripting_components.zig`; a non-declaring native game generates nothing. Part of labelle-engine#774 (Phase 2). Follow-up (release-gated): the examples/rust-game migration to `.rs` + the CI purity variant, which needs a released assembler carrying this row. 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: 55 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 (3)
✨ 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 introduces support for Rust declarations in the scripting system by implementing a new .cargo_probe runner kind. This runner generates a temporary Cargo probe crate from the game's .rs component and event declaration files, builds it, and runs it to extract the schema JSON. The reviewer's feedback suggests simplifying the probeBinName function to be buffer-free and allocation-free, which would also eliminate the need for a temporary buffer in buildRustProbe. Additionally, the reviewer recommends propagating critical errors like OutOfMemory in writeFileAbsIfDifferent instead of silently swallowing them.
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.
| fn probeBinName(buf: []u8) []const u8 { | ||
| if (builtin.os.tag == .windows) | ||
| return std.fmt.bufPrint(buf, "labelle-declare-rs.exe", .{}) catch "labelle-declare-rs.exe"; | ||
| return "labelle-declare-rs"; | ||
| } |
There was a problem hiding this comment.
The probeBinName function takes a buffer and uses std.fmt.bufPrint to format a static string literal. Since both "labelle-declare-rs.exe" and "labelle-declare-rs" are compile-time string literals that coerce directly to []const u8, we can simplify this function to be completely allocation-free and buffer-free.
fn probeBinName() []const u8 {
if (builtin.os.tag == .windows)
return "labelle-declare-rs.exe";
return "labelle-declare-rs";
}
| var exe_buf: [64]u8 = undefined; | ||
| const bin = try std.fs.path.join(allocator, &.{ target_dir, "debug", probeBinName(&exe_buf) }); |
| if (cwd.readFileAlloc(io, abs_path, allocator, .limited(4 * 1024 * 1024))) |existing| { | ||
| defer allocator.free(existing); | ||
| if (std.mem.eql(u8, existing, body)) return; | ||
| } else |_| {} |
There was a problem hiding this comment.
In writeFileAbsIfDifferent, cwd.readFileAlloc is called and any error is caught and ignored with else |_| {}. While ignoring errors like FileNotFound is expected, critical errors like OutOfMemory should be propagated rather than silently swallowed.
if (cwd.readFileAlloc(io, abs_path, allocator, .limited(4 * 1024 * 1024))) |existing| {
defer allocator.free(existing);
if (std.mem.eql(u8, existing, body)) return;
} else |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => {},
}
|
Superseded by the epic owner's architecture decision (2026-07-12): the rust declare lane goes full plugin-declared capability (#619-style, declare slice), not assembler-hardcoded cargo machinery. Per RFC-LANGUAGE-PLUGINS rev 16 §7 ("The language-agnostic assembler"), per-language declare knowledge moves OUT of the assembler into Closing in favor of that approach (tracked under #619). The validated findings here are salvaged into the follow-up: the multi-file ordering (copy each decl file to |
Part of the language-plugins epic (labelle-engine#237); implements the assembler side of labelle-engine#774 (Phase 2). The scripting-side foundation (the
component!/event!macros + the probe crate + the cross-runner golden) merged in labelle-scripting PR #29; this adds the assembler's runner + collection so a rust game can declare components/events in.rs.Why this is machinery, not a table row
The lua/ruby declare runners are prebuilt exes RUN OVER the game's script files (
argv= the sources). Rust has no interpreter, so — per the recorded decision on #774 (route (a), the compile-and-run probe, spiked with byte-parity + timing evidence) — its runner must GENERATE a probe crate from the game'scomponents/*.rs+events/*.rs,cargo buildit against a persistent target dir, run it, and capture the schema JSON — feeding the sameparseSchemaconsumer the lua/ruby runners feed.What lands
DeclareRunner.kind: RunnerKind(exe_over_files|cargo_probe) — defaulted, so the lua/ruby rows read unchanged; rust is the sole.cargo_proberow. A single discriminant field so assembler#619's fold of this table intoplugin.labellecapability rows stays a mechanical move (this PR deliberately does not touch #619 or refactor the table).runCargoProbecapability-probes the plugin's shipped macro module (native/src/labelle.rs, PR #29 — absent → the SAME graceful skip the lua/ruby tool-dir probe gives), then generates + builds + runs the probe. The generated crate mirrorstools/declare-rs/src/main.rs:pub mod labellerecomposed from the shipped file via#[path], onemod decl_NNNN;per game declaration file. The emitter recovers declaration order from(file!(), line!()), so each file is copied into the probe'ssrc/under an ordering-encoded name (components first, then events, alphabetical within each — the order the lua/ruby argv carries);file!()then sorts on the prefix and reproduces the cross-runner order. Write-if-different keeps a warm re-generate a no-op cargo build (external target dir, the rust twin of the lua/ruby content-keyed zig cache).The shared skip + events floor (
declareToolAbsent) serve both kinds; the lua/ruby.exe_over_filesbranch is left byte-for-byte as it shipped.declare_probe_overrideis the hermetic-test seam (a prebuilt fake probe run with no args), the rust twin ofdeclare_tool_override.Collection (root.zig):
components/*.<ext>+events/*.<ext>now collect for both families (only SCRIPTS differ: embed embeds+registers them, native stages+compiles them). For the native family they feed the declare runner but are NOT concatenated ontos.scripts(nothing embeds). A non-declaring native game still no-ops at the phase's zero-files gate.Verification
zig build+zig build test(Windows, Zig 0.16): the new hermetic unit tests (probe-crate generation, ordered decl modules, the Windows backslash→forward-slash#[path]rule, the Cargo.toml feature/dep wiring, the runner-kind table) pass; no regression vs the pre-existing (CRLF/shell-exec) Windows baseline — ubuntu CI is the authority for the fmt + shell-exec e2e paths.generatethrough the cargo-probe override: acomponents/hunger.rsdeclaration codegensscripting_components.zig; a non-declaring native game generates nothing. The real probe generation + cargo build is exercised by labelle-scripting'srust-exampleCI (a released assembler over the real macros), exactly as the lua/ruby tools' behavior is pinned by that repo's goldens, not here.Follow-up (release-gated)
The
examples/rust-gamemigration to.rs+ the CI purity variant + the ordered token-transcript diff needs a released assembler carrying this row (bumpASSEMBLER_VERSION) — that is where therust-exampleCI's "native splices skip declare silently" invariant deliberately flips.🤖 Generated with Claude Code
https://claude.ai/code/session_01D1zt5TCHYqUJBjswJKjnCo