A non-entry -> Void raises function with no trailing return can report an error it never raised. On linux-x64 it happens whenever the last value left in rax has non-zero high 32 bits.
Repro on main @ 7e1a64d and v0.3.4 (linux-x64):
fn f() -> Void raises [Zero] {
let x: i64 = 4294967296
}
pub fn main(world: World) -> Void raises {
check f()
check world.out.write("ok\n")
}Expected ok. Actual is no output and exit 1. With return at the end of f it prints ok.
Only the high 32 bits matter. 1, 2^31 and 2^32-1 pass, while 2^32, 2^33, 2^63-1, -1 and -2^32 fail (sweep with a helper that ends in a mutref field store).
Cause. Callers read the error code from rax >> 32 (emit_elf64.c:429-433). An explicit return clears it with xor rax, rax (emit_elf64.c:3899), but the fall-through path at emit_elf64.c:4041 only emits the epilogue. f compiles to movabs rax,0x100000000; mov [rbp-0x8],rax; leave; ret. Lowering only adds a synthetic return for the hosted main (program_graph_mir.c:6298-6308, ir.c:6303-6313).
Mach-O x86-64 (emit_macho_x64.c:2437) and arm64 (emit_macho64.c:3572) have the same shape. I checked those in the emitted object code only (no Mac). On arm64 the stale register is x0, so pass the value in as a parameter to reproduce. COFF and AArch64 ELF/COFF reject check/raise today, so they can't hit it yet.
The patch below clears the error channel on fall-through when fun->raises, like the explicit return does (+17 -3). On linux-x64 the repro and the sweep go from wrong to correct, and a real raise still propagates. I didn't run the conformance suite (needs node 24 and pnpm).
patch against 7e1a64d
--- a/native/zero-c/src/emit_elf64.c
+++ b/native/zero-c/src/emit_elf64.c
@@ -4038,7 +4038,13 @@
abi_slot += slots;
}
if (!elf_emit_instrs(text, fun, fun->instrs, fun->instr_len, ctx, diag)) return false;
- if (fun->instr_len == 0 || fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN) elf_emit_epilogue(text, fun, ctx);
+ if (fun->instr_len == 0 || fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN) {
+ /* Falling off a fallible Void body is an implicit `return`: clear the
+ packed error channel (rax[63:32]) like the explicit return path does,
+ instead of leaving the last emitted value in rax. */
+ if (fun->raises) z_x64_emit_xor_rax_rax(text);
+ elf_emit_epilogue(text, fun, ctx);
+ }
return true;
}
--- a/native/zero-c/src/emit_macho_x64.c
+++ b/native/zero-c/src/emit_macho_x64.c
@@ -2434,7 +2434,11 @@
abi_slot++;
}
if (!machx64_emit_instrs(text, fun, fun->instrs, fun->instr_len, ctx, diag)) return false;
- if (fun->instr_len == 0 || (fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN && fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RAISE)) z_x64_emit_epilogue(text);
+ if (fun->instr_len == 0 || (fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN && fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RAISE)) {
+ /* Implicit `return` from a fallible Void body: clear the error channel. */
+ if (fun->raises) z_x64_emit_xor_rax_rax(text);
+ z_x64_emit_epilogue(text);
+ }
return true;
}
--- a/native/zero-c/src/emit_macho64.c
+++ b/native/zero-c/src/emit_macho64.c
@@ -3569,7 +3569,11 @@
abi_slot += slots;
}
if (!macho_emit_instrs(text, fun, fun->instrs, fun->instr_len, frame_size, seed_process_args, ctx, diag)) return false;
- if (fun->instr_len == 0 || (fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN && fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RAISE)) macho_emit_epilogue(text, frame_size, seed_process_args);
+ if (fun->instr_len == 0 || (fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RETURN && fun->instrs[fun->instr_len - 1].kind != IR_INSTR_RAISE)) {
+ /* Implicit `return` from a fallible Void body: clear the error channel. */
+ if (fun->raises) z_aarch64_emit_movz_x(text, 0, 0);
+ macho_emit_epilogue(text, frame_size, seed_process_args);
+ }
return true;
}
Activity
0No activity yet.