Skip to content

fix(scan): propagate OOM as a hard failure + plug leak-on-OOM (#540) - #555

Merged
apotema merged 1 commit into
mainfrom
fix/scan-oom
Jul 5, 2026
Merged

apotema merged 1 commit into
mainfrom
fix/scan-oom

Conversation

@apotema

@apotema apotema commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #540.

Pre-existing latent robustness issues in the flow/script scan (surfaced during the #539 split, present verbatim on main before it): three paths swallowed error.OutOfMemory, violating the module's documented "OOM is a hard failure" goal. All three are behavior changes on the OOM path only — normal codegen output is byte-identical (golden suite unchanged).

The three sites

  1. src/codegen/scan/flow_decls.zig — the plugin + game-script passes did scanFlowDeclsInSource(...) catch continue, masking an allocation failure as a tolerated per-file parse failure. A swallowed OOM silently leaves the generated flow registry incomplete → wrong codegen with no error. Fix: catch |err| switch (err) { error.OutOfMemory => return err } at both call sites.

    • Note discovered while implementing: the scan's inferred error set is exactly error{OutOfMemory} — std.zig.Ast.parse records parse errors in the AST rather than raising them, so there was never a genuine per-file parse error to tolerate; the old continue only ever swallowed OOM. The switch is therefore exhaustive (no else); if the scan ever grows a tolerable error it will stop compiling and force a deliberate else => continue.
  2. src/codegen/scan/promote.zig — a freshly-duped rel_path leaked if out.append(...) OOMed before ownership transferred to the list (the function-scope errdefer walks out.items, which doesn't yet include the pending entry). Fix: errdefer allocator.free(rel_path); right after the dupe.

  3. src/root/game_shim.zig (from the refactor: split root.zig into focused sub-modules (behavior-preserving) #542 root split) — arr_list.toOwnedSlice(allocator) leaked the buffer on OOM: toArrayList() reset the writer so the function-scope errdefer aw.deinit() covers nothing. Fix: errdefer arr_list.deinit(allocator); before the toOwnedSlice (safe — the reset writer's deinit is a no-op).

Tests

FailingAllocator regression tests (the toolkit's failing-allocator idiom) for each site:

  • discoverPluginFlowDecls: OOM propagates, never a silent partial scan — drives a game script with a real FlowNodes block through a FailingAllocator at every alloc index; asserts each forced failure surfaces as error.OutOfMemory and never returns a value with a truncated node set (exactly what the old catch continue produced). Balances freed==allocated at every point.
  • collectPromotedScripts: no allocator leak at any OOM point
  • generateGameShim: no allocator leak at any OOM point

Bar

  • zig build + zig build test green.
  • Golden suite unchanged (zero diff — only OOM paths changed; no output-producing code touched).
  • zig fmt --check clean.

https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of out-of-memory failures so discovery and generation steps now fail cleanly instead of producing partial results.
    • Fixed a memory leak in script promotion when allocation fails during list updates.
    • Fixed a memory leak during game shim creation when final buffer conversion fails.
  • Tests

    • Added regression coverage for out-of-memory scenarios, including checks that cleanup fully releases allocated memory.

The flow/script scan swallowed error.OutOfMemory on three paths,
violating the module's "OOM is a hard failure" design goal:

1. src/codegen/scan/flow_decls.zig — the plugin + game-script passes
   did `scanFlowDeclsInSource(...) catch continue`, masking an OOM as a
   tolerated per-file parse failure and silently leaving the generated
   flow registry incomplete (wrong codegen, no error). The scan's
   inferred error set is in fact *exactly* error{OutOfMemory}
   (std.zig.Ast.parse records parse errors in the AST rather than
   raising them), so both call sites now use an exhaustive
   `catch |err| switch (err) { error.OutOfMemory => return err }`.

2. src/codegen/scan/promote.zig — a freshly-duped `rel_path` leaked if
   `out.append(...)` OOMed before ownership transferred to the list;
   added `errdefer allocator.free(rel_path)` right after the dupe.

3. src/root/game_shim.zig — `arr_list.toOwnedSlice(allocator)` leaked
   the buffer on OOM after `toArrayList()` reset the writer; added
   `errdefer arr_list.deinit(allocator)` (a no-op on the reset writer).

Adds FailingAllocator regression tests for all three sites: OOM
propagation for the scan (never a silent partial), and freed==allocated
balance at every allocation index for the two leak sites.

Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes out-of-memory handling in three scan/shim-generation modules: flow_decls.zig now propagates error.OutOfMemory instead of swallowing it in catch continue, promote.zig adds an errdefer to prevent a rel_path leak on append failure, and game_shim.zig adds an errdefer to prevent an arr_list leak on toOwnedSlice failure. Each fix is accompanied by a FailingAllocator-based regression test.

Changes

OOM propagation and leak fixes

Layer / File(s) Summary
Flow decl scan: propagate OOM instead of catch continue
src/codegen/scan/flow_decls.zig
Plugin and game-script scan loops rethrow error.OutOfMemory via exhaustive switch instead of catch continue; a new test forces allocation failures at every index and asserts either complete results or error.OutOfMemory, with balanced allocated/freed bytes.
Promote scan: fix rel_path leak on append OOM
src/codegen/scan/promote.zig
Adds errdefer allocator.free(rel_path) after allocation, before out.append; a new test forces allocation failures at every index and asserts no leaks.
Game shim generation: fix arr_list leak on toOwnedSlice OOM
src/root/game_shim.zig
Adds errdefer arr_list.deinit(allocator) before toOwnedSlice; a new test forces allocation failures at every index and asserts balanced allocated/freed bytes, accepting error.OutOfMemory or error.WriteFailed.

Estimated code review effort: 2 (Simple) | ~12 minutes

Poem

Three small burrows, each with a leak,
now sealed tight so OOM can't sneak.
catch continue? Not on my watch,
errdefer stands as the guarding notch.
Hop, hop, hooray — memory's safe today! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning src/root/game_shim.zig adds an extra OOM leak fix and test that are not part of linked issue #540. Either move the game_shim OOM fix into a separate PR or add it to the linked issue scope with explicit justification.
✅ Passed checks (4 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: hard-failing OOM handling and a leak fix in scan paths.
Linked Issues check ✅ Passed The PR addresses #540 by propagating OOM in flow-decls, freeing rel_path on append OOM, and adding failing-allocator regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/scan-oom

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 fixes several out-of-memory (OOM) handling issues across the codebase. In flow_decls.zig, OOM errors are now correctly propagated as hard failures instead of being silently swallowed. In promote.zig and game_shim.zig, local errdefer blocks have been added to prevent memory leaks when OOM occurs during list appending or slice conversion. Regression tests using FailingAllocator have been added for each of these cases to ensure proper OOM propagation and leak prevention. There are no review comments, and I have no feedback to provide.

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/codegen/scan/flow_decls.zig (2)

255-262: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate extractConstructsString allocation failures. allocator.dupe(...) catch null still turns error.OutOfMemory into “no .constructs field,” so the scanner can silently drop a real value on memory pressure. Make this helper return !?[]u8 and try the duplicate so OOM bubbles up like the rest of the scan.

🤖 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/scan/flow_decls.zig` around lines 255 - 262,
`extractConstructsString` is swallowing allocation failures by converting
`allocator.dupe(...)` errors into `null`, which makes a real `.constructs` value
look absent under memory pressure. Update this helper to return `!?[]u8`, use
`try` for the duplicate, and propagate `error.OutOfMemory` through the caller
chain in `scan/flow_decls.zig` so the scan fails consistently instead of
silently dropping the value.

510-512: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

cache.resolvePlugin(...) catch continue should not swallow OOM src/cache/resolve.zig allocates and joins paths here, so error.OutOfMemory is reachable; let this fail hard instead of silently skipping the plugin from the flow registry.

🤖 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/scan/flow_decls.zig` around lines 510 - 512, The plugin
resolution loop is swallowing allocator failures by using catch continue on
cache.resolvePlugin, which can hide error.OutOfMemory. Update the flow registry
logic in the for (cfg.plugins) loop around plugin_dir so only expected non-fatal
resolution errors are skipped, and let OutOfMemory propagate instead of
continuing silently; use the cache.resolvePlugin and plugin_dir handling in this
block to locate the fix.
🤖 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.

Outside diff comments:
In `@src/codegen/scan/flow_decls.zig`:
- Around line 255-262: `extractConstructsString` is swallowing allocation
failures by converting `allocator.dupe(...)` errors into `null`, which makes a
real `.constructs` value look absent under memory pressure. Update this helper
to return `!?[]u8`, use `try` for the duplicate, and propagate
`error.OutOfMemory` through the caller chain in `scan/flow_decls.zig` so the
scan fails consistently instead of silently dropping the value.
- Around line 510-512: The plugin resolution loop is swallowing allocator
failures by using catch continue on cache.resolvePlugin, which can hide
error.OutOfMemory. Update the flow registry logic in the for (cfg.plugins) loop
around plugin_dir so only expected non-fatal resolution errors are skipped, and
let OutOfMemory propagate instead of continuing silently; use the
cache.resolvePlugin and plugin_dir handling in this block to locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f1d3bb0-02d4-4231-a557-441c261c13e3

📥 Commits

Reviewing files that changed from the base of the PR and between 1142b69 and 6f6f9dd.

📒 Files selected for processing (3)
  • src/codegen/scan/flow_decls.zig
  • src/codegen/scan/promote.zig
  • src/root/game_shim.zig

@apotema
apotema merged commit 5e8a219 into main Jul 5, 2026
4 checks passed
@apotema
apotema deleted the fix/scan-oom branch July 5, 2026 17:49
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.

scan: OOM silently swallowed by catch continue (flow-decls) + leak-on-OOM (promote) — violates 'OOM is a hard failure' goal

1 participant