ffi M5.A.next.2a.D: inline-return uses CFG terminator, not block_terminated
Fixes the regression locked in by 2a.C (commit 6b7a66b).
issue-0045's original fix set `block_terminated = true` after
each inline `return X;` to skip dead code in the inlined body.
But the flag leaked past structured control flow — an `if cond
{ return X; }` whose merge block continued to subsequent
statements would short-circuit the trailing code at the
`lowerBlockValue` loop's `if (self.block_terminated) return
null;` check.
Switched to the classical SSA "return-done block" shape:
- `InlineReturnInfo` carries a third field `done_bb: BlockId`
— a fresh basic block allocated by `lowerComptimeCall` per
comptime-call instance.
- `lowerReturn`'s inline path stores into the slot, drains
defers, and emits `br done_bb`. The basic block's terminator
is what carries the "no fall-through" signal; the
`block_terminated` flag is no longer touched.
- `lowerComptimeCall` allocates the slot + done_bb, lowers the
body, then switches to done_bb and loads the slot. Tail-
expression bodies that fall through (rare when has_return is
true) get a synthetic store + br so the CFG is well-formed.
For `if cond { return 42; }; return -1;`:
- cond=true: then's `return 42` stores 42, br done_bb. Merge
block has only the false predecessor, doesn't run the
trailing return. Load done_bb → 42.
- cond=false: condBr skips to merge. Merge runs `return -1;`
→ store -1, br done_bb. Load → -1.
`examples/157-pack-if-return.sx` flips from `8354116000` (the
uninitialised slot load on the false path) to `-1`. A
three-way `classify(..$args)` smoke confirms multi-path
inline-return works for any of the three branches.
Dead-code-after-return inside the inlined body still trips the
LLVM verifier (same shape as a regular `return X; print("dead");`
which also crashes today). Acceptable consistency — user code
shouldn't write unreachable code in either context.
196/196 example tests + `zig build test` green.
This commit is contained in:
@@ -216,7 +216,7 @@ pub const Lowering = struct {
|
|||||||
span: ast.Span,
|
span: ast.Span,
|
||||||
};
|
};
|
||||||
|
|
||||||
const InlineReturnInfo = struct { slot: Ref, ret_ty: TypeId };
|
const InlineReturnInfo = struct { slot: Ref, ret_ty: TypeId, done_bb: BlockId };
|
||||||
|
|
||||||
/// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`.
|
/// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`.
|
||||||
/// Matches any concrete closure source whose first `prefix_len` param types
|
/// Matches any concrete closure source whose first `prefix_len` param types
|
||||||
@@ -1590,8 +1590,17 @@ pub const Lowering = struct {
|
|||||||
self.target_type = old_target;
|
self.target_type = old_target;
|
||||||
|
|
||||||
// Inlined-comptime-body return: store into the slot the inliner
|
// Inlined-comptime-body return: store into the slot the inliner
|
||||||
// gave us instead of emitting a `ret` into the caller's LLVM
|
// gave us and branch to the inliner's "return-done" basic block.
|
||||||
// function. The inliner reads the slot once the body finishes.
|
// The branch is the basic block's terminator — so subsequent
|
||||||
|
// dead code in the same block trips the LLVM verifier (the
|
||||||
|
// SAME behaviour as a regular `return X;` followed by code).
|
||||||
|
//
|
||||||
|
// We DO NOT set `block_terminated = true`: that flag would
|
||||||
|
// leak past structured control flow (e.g. an `if cond { return
|
||||||
|
// X; }` whose merge block continues to subsequent statements)
|
||||||
|
// and incorrectly skip the trailing statements. CFG-level
|
||||||
|
// termination is what we actually want — let the basic-block
|
||||||
|
// terminator do its job.
|
||||||
if (self.inline_return_target) |iri| {
|
if (self.inline_return_target) |iri| {
|
||||||
if (ret_val) |ref| {
|
if (ret_val) |ref| {
|
||||||
const val_ty = self.builder.getRefType(ref);
|
const val_ty = self.builder.getRefType(ref);
|
||||||
@@ -1604,7 +1613,7 @@ pub const Lowering = struct {
|
|||||||
// Drain block-scoped defers up to the inlined-body base so
|
// Drain block-scoped defers up to the inlined-body base so
|
||||||
// they fire on this return path the same as a real fn return.
|
// they fire on this return path the same as a real fn return.
|
||||||
self.emitBlockDefers(self.func_defer_base);
|
self.emitBlockDefers(self.func_defer_base);
|
||||||
self.block_terminated = true;
|
self.builder.br(iri.done_bb, &.{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7297,29 +7306,52 @@ pub const Lowering = struct {
|
|||||||
const ret_ty = self.resolveReturnType(fd);
|
const ret_ty = self.resolveReturnType(fd);
|
||||||
if (ret_ty != .void) {
|
if (ret_ty != .void) {
|
||||||
// Detect whether the body might use `return X;` statements.
|
// Detect whether the body might use `return X;` statements.
|
||||||
// If so, set up the inline-return slot so they store into it
|
// If so, set up the inline-return slot AND a dedicated
|
||||||
// instead of emitting `ret` into the caller's basic block.
|
// "return-done" basic block so each `return X;` stores to
|
||||||
// Pure tail-expression bodies (arrow form, or a block whose
|
// the slot and branches to ret_done. After the body lowers,
|
||||||
// last stmt is an expression) skip the slot — keeps the
|
// we switch to ret_done and load. Pure tail-expression
|
||||||
// common `format`/`#insert`-style path unchanged.
|
// bodies (arrow form, or a block whose last stmt is an
|
||||||
|
// expression) skip the slot+block — keeps the common
|
||||||
|
// `format`/`#insert`-style path unchanged.
|
||||||
const has_return = fnBodyHasReturn(fd.body);
|
const has_return = fnBodyHasReturn(fd.body);
|
||||||
if (has_return) {
|
if (has_return) {
|
||||||
const ret_slot = self.builder.alloca(ret_ty);
|
const ret_slot = self.builder.alloca(ret_ty);
|
||||||
|
const ret_done_bb = self.freshBlock("ct.ret_done");
|
||||||
const saved_iri = self.inline_return_target;
|
const saved_iri = self.inline_return_target;
|
||||||
self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty };
|
self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty, .done_bb = ret_done_bb };
|
||||||
const saved_term = self.block_terminated;
|
defer self.inline_return_target = saved_iri;
|
||||||
self.block_terminated = false;
|
|
||||||
defer {
|
// Lower body. Tail-expression bodies (rare here since
|
||||||
self.inline_return_target = saved_iri;
|
// has_return == true) produce a tail value we still
|
||||||
self.block_terminated = saved_term;
|
// route through the slot so the load in ret_done picks
|
||||||
|
// it up. Block-statement bodies whose last stmt is
|
||||||
|
// `return X;` already br to ret_done from inside
|
||||||
|
// lowerReturn.
|
||||||
|
if (self.lowerBlockValue(fd.body)) |val| {
|
||||||
|
if (!self.currentBlockHasTerminator()) {
|
||||||
|
const v_ty = self.builder.getRefType(val);
|
||||||
|
const coerced = if (v_ty != ret_ty)
|
||||||
|
self.coerceToType(val, v_ty, ret_ty)
|
||||||
|
else
|
||||||
|
val;
|
||||||
|
self.builder.store(ret_slot, coerced);
|
||||||
|
self.builder.br(ret_done_bb, &.{});
|
||||||
|
}
|
||||||
|
} else if (!self.currentBlockHasTerminator()) {
|
||||||
|
// Body fell through without producing a tail value
|
||||||
|
// AND without branching to ret_done — this only
|
||||||
|
// happens for bodies whose last stmt is a void
|
||||||
|
// statement (e.g. side-effecting). Slot is
|
||||||
|
// uninitialised on this path; safer to br anyway
|
||||||
|
// so the CFG is well-formed. The load in ret_done
|
||||||
|
// will read uninit, which is the same garbage
|
||||||
|
// behaviour the regular fn-body lowering would
|
||||||
|
// produce for a missing return.
|
||||||
|
self.builder.br(ret_done_bb, &.{});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.lowerBlockValue(fd.body)) |val| {
|
self.builder.switchToBlock(ret_done_bb);
|
||||||
return val;
|
return self.builder.load(ret_slot, ret_ty);
|
||||||
}
|
|
||||||
if (self.block_terminated) {
|
|
||||||
return self.builder.load(ret_slot, ret_ty);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (self.lowerBlockValue(fd.body)) |val| {
|
if (self.lowerBlockValue(fd.body)) |val| {
|
||||||
return val;
|
return val;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
8354116000
|
-1
|
||||||
42
|
42
|
||||||
|
|||||||
Reference in New Issue
Block a user