Skip to content

fix(inline): map full→local function index so imported calls don't break inlining — closes #153 (v1.1.5)#154

Merged
avrabe merged 1 commit into
mainfrom
fix/153-inline-imported-call-index
May 30, 2026
Merged

fix(inline): map full→local function index so imported calls don't break inlining — closes #153 (v1.1.5)#154
avrabe merged 1 commit into
mainfrom
fix/153-inline-imported-call-index

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 30, 2026

gale #153 — inline a local callee even when the caller calls an import

Bug-fix release v1.1.5. On v1.1.4, inline_functions reverted the inline of a local callee whenever the caller also called an imported function — blocking the gale C↔Rust seam (z_impl_k_sem_give inlines gale_k_sem_give_decide but also calls 5 env:: kernel imports).

Root cause — a function-index-space confusion (not a verifier gap)

Call(func_idx) uses the full WebAssembly index space (imports 0..N, locals after). But the inliner built function_sizes keyed by local index while call_counts keyed by full index, and indexed the local-only all_functions with the full func_idx. With an import present, the import's index collides with a local function's slot — so the inliner treated a void imported call as a call to local function 0, emitted a local.set to bind params with no argument on the stack, and produced a malformed body. The verifier correctly rejected it (Stack underflow in LocalSet) → the whole inline reverted.

(The report hypothesised a missing verifier model for imports; ground-truthing showed the real bug was upstream — the inliner emitting nonsense. The verifier was right to reject it.)

Fix

  • Compute num_imported_funcs once per iteration.
  • Key function_sizes + the candidate gate by the full index (matches call_counts).
  • Exclude imported indices from candidates — they have no body to inline.
  • Map full→local (func_idx - num_imported_funcs) when indexing the local-function table; an imported index yields None → keep the original call untouched. Thread the offset through the Block/Loop/If recursion.
  • Modules with no imports are unaffected (offset 0) — all existing inline behaviour preserved.

Validation

Known-red gates (pre-existing)

Closes #153.

🤖 Generated with Claude Code

…eak inlining — closes #153

gale follow-up (#153): on v1.1.4, `inline_functions` reverted the inline
of a local callee whenever the CALLER also called an imported function.
This blocked the gale C↔Rust seam (z_impl_k_sem_give inlines
gale_k_sem_give_decide but also calls 5 env:: kernel imports).

Root cause: a function-INDEX-SPACE confusion, not a verifier gap.
`Call(func_idx)` uses the FULL WebAssembly index space — imported
functions occupy 0..num_imported_funcs, local functions follow. But the
inliner:
- built `function_sizes` keyed by LOCAL index (module.functions, from 0)
  while `call_counts` (from count_calls_recursive) keys by FULL index;
- indexed `all_functions` (local-only) with the FULL `func_idx`.
With an import present the spaces diverge: the import's index (0)
collides with local function 0, so the inliner treated a VOID imported
call as a call to local function 0, emitted a `local.set` to bind its
params with NO argument on the stack, and produced a malformed body.
The verifier correctly rejected the bad encode ("Stack underflow in
LocalSet") → the whole inline reverted. (The reporter hypothesised a
missing verifier model for imports; the real bug was upstream in the
inliner emitting nonsense.)

Fix (loom-core/src/lib.rs, inline_functions / inline_calls_in_block):
- compute num_imported_funcs once per iteration;
- key `function_sizes` by FULL index (local idx + offset) to match
  call_counts;
- exclude imported indices (< offset) from inline candidates — they have
  no body to inline;
- in inline_calls_in_block, map func_idx → local via checked_sub(offset);
  an imported index yields None → keep the original call untouched;
  thread the offset through the Block/Loop/If recursion.
Modules with no imported functions are unaffected (offset 0), so all
existing inline behaviour is preserved.

Regression test test_inline_caller_with_imported_call: a caller that
calls an import AND a local i64 callee now inlines the local callee
(call removed, verified) while preserving the import call. 386 lib tests
pass; #151 i64/i32 repros still inline; gale wasm + full pipeline dogfood
clean. The 7 pre-existing LICM/DCE integration failures (#150) are
unrelated and unchanged.

Closes #153.

Trace: REQ-8
@avrabe avrabe merged commit 95a5f98 into main May 30, 2026
15 of 21 checks passed
@avrabe avrabe deleted the fix/153-inline-imported-call-index branch May 30, 2026 18:56
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.

inline_functions reverts when the CALLER contains an imported-function call (by-body modeling has no body for imports) — v1.1.4

1 participant