fix(ffi): replace silent .void arg-type fallback with loud .unresolved (issue 0074)
Four FFI call-arg lowering sites resolved an argument's IR type via `getRefIRType(arg_ref) orelse .void` — a silent fallback to the load-bearing real type `.void`. A failed lookup there is a codegen invariant violation, but `.void` is treated by downstream `toLLVMType` → `abiCoerceParamType` → `coerceArg` as a legitimate void-typed foreign argument, corrupting the call ABI with no diagnostic. Add one shared resolver `LLVMEmitter.argIRTypeOrFail` that returns the dedicated `.unresolved` sentinel on a failed lookup — never `.void`/`.s64` — so the failure cannot masquerade as a real type and trips `toLLVMType`'s existing hard `@panic` tripwire at the call site. Route all four sites through it: - src/ir/emit_llvm.zig JNI constructor (NewObject) arg loop - src/backend/llvm/ops.zig objc_msgSend arg loop - src/backend/llvm/ops.zig JNI non-virtual call arg loop - src/backend/llvm/ops.zig JNI Call<Type>Method arg loop Happy path is byte-identical (every real arg already has a resolved type); FFI examples stay green with zero snapshot churn. Regression test (fail-before/pass-after) in src/ir/emit_llvm.test.zig asserts an unresolvable FFI arg ref now yields `.unresolved`, not the old silent `.void`.
This commit is contained in:
@@ -514,7 +514,7 @@ pub const Ops = struct {
|
||||
// coercion applied so structs / strings decay the
|
||||
// same way they do for any C foreign call.
|
||||
for (msg.args, 0..) |arg_ref, i| {
|
||||
const raw_ty = self.e.getRefIRType(arg_ref) orelse .void;
|
||||
const raw_ty = self.e.argIRTypeOrFail(arg_ref);
|
||||
const raw_llvm = self.e.toLLVMType(raw_ty);
|
||||
const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm);
|
||||
param_types[i + 2 + sret_off] = coerced_ty;
|
||||
@@ -728,7 +728,7 @@ pub const Ops = struct {
|
||||
call_args_nv[2] = cls;
|
||||
call_args_nv[3] = mid_val;
|
||||
for (msg.args, 0..) |arg_ref, i| {
|
||||
const raw_ty = self.e.getRefIRType(arg_ref) orelse .void;
|
||||
const raw_ty = self.e.argIRTypeOrFail(arg_ref);
|
||||
const raw_llvm = self.e.toLLVMType(raw_ty);
|
||||
const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm);
|
||||
call_param_types_nv[i + 4] = coerced_ty;
|
||||
@@ -758,7 +758,7 @@ pub const Ops = struct {
|
||||
call_args[1] = target;
|
||||
call_args[2] = mid;
|
||||
for (msg.args, 0..) |arg_ref, i| {
|
||||
const raw_ty = self.e.getRefIRType(arg_ref) orelse .void;
|
||||
const raw_ty = self.e.argIRTypeOrFail(arg_ref);
|
||||
const raw_llvm = self.e.toLLVMType(raw_ty);
|
||||
const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm);
|
||||
call_param_types[i + 3] = coerced_ty;
|
||||
|
||||
@@ -1097,3 +1097,50 @@ test "emit: ERR E3.0 — no DWARF without a debug context (unit-test default)" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, ir_str, "!dbg") == null);
|
||||
}
|
||||
|
||||
// ── issue 0074: FFI arg-type lookup must fail loudly, never silently `.void` ──
|
||||
// `argIRTypeOrFail` backs the four FFI call-arg lowering sites (objc_msgSend,
|
||||
// JNI Call<Type>Method / non-virtual / constructor). A ref it cannot resolve is
|
||||
// a codegen invariant violation; it must surface the dedicated `.unresolved`
|
||||
// tripwire sentinel (which `toLLVMType` hard-panics on) rather than the old
|
||||
// silent `.void` default that would emit a void-typed foreign-call argument.
|
||||
test "emit: argIRTypeOrFail surfaces .unresolved for an unresolvable FFI arg ref (issue 0074)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
var b = Builder.init(&module);
|
||||
|
||||
// func ffifn(a: s64, b: f64) -> void { <entry> }
|
||||
const fid = b.beginFunction(str(&module, "ffifn"), &[_]Function.Param{
|
||||
.{ .name = str(&module, "a"), .ty = .s64 },
|
||||
.{ .name = str(&module, "b"), .ty = .f64 },
|
||||
}, .void);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
b.retVoid();
|
||||
b.finalize();
|
||||
|
||||
var emitter = LLVMEmitter.init(alloc, &module, "test_ffi_argty", .{});
|
||||
defer emitter.deinit();
|
||||
emitter.current_func_idx = fid.index();
|
||||
|
||||
// Happy path: a real arg ref (param 0 / param 1) resolves byte-identically
|
||||
// to its declared IR type — the FFI fast path is unchanged.
|
||||
try std.testing.expectEqual(TypeId.s64, emitter.argIRTypeOrFail(Ref.fromIndex(0)));
|
||||
try std.testing.expectEqual(TypeId.f64, emitter.argIRTypeOrFail(Ref.fromIndex(1)));
|
||||
|
||||
// A ref past every param and instruction is unresolvable.
|
||||
const bogus = Ref.fromIndex(100_000);
|
||||
try std.testing.expectEqual(@as(?TypeId, null), emitter.getRefIRType(bogus));
|
||||
|
||||
// Fail-before: the old `getRefIRType(arg) orelse .void` would silently
|
||||
// yield `.void` here — a real, load-bearing type that downstream ABI
|
||||
// coercion treats as a legitimate (void-typed) foreign argument.
|
||||
try std.testing.expectEqual(TypeId.void, emitter.getRefIRType(bogus) orelse TypeId.void);
|
||||
|
||||
// Pass-after: the helper returns the dedicated `.unresolved` sentinel,
|
||||
// never `.void`, so the failure cannot masquerade as a real type.
|
||||
try std.testing.expectEqual(TypeId.unresolved, emitter.argIRTypeOrFail(bogus));
|
||||
try std.testing.expect(emitter.argIRTypeOrFail(bogus) != .void);
|
||||
}
|
||||
|
||||
@@ -2239,6 +2239,16 @@ pub const LLVMEmitter = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resolve the IR type of a foreign-call argument ref. Every FFI arg ref is
|
||||
/// a real function param or block instruction result, so a `null` here is a
|
||||
/// codegen invariant violation, not a recoverable case: return the dedicated
|
||||
/// `.unresolved` sentinel — never `.void`/`.s64` — so the failure cannot be
|
||||
/// mistaken for a real type and trips `toLLVMType`'s hard tripwire at the call
|
||||
/// site instead of silently emitting a void-typed foreign argument.
|
||||
pub fn argIRTypeOrFail(self: *LLVMEmitter, arg_ref: Ref) TypeId {
|
||||
return self.getRefIRType(arg_ref) orelse .unresolved;
|
||||
}
|
||||
|
||||
|
||||
/// Coerce both binary operands to match the instruction's result type.
|
||||
/// E.g. if result is i64 but one operand is i32, sext it.
|
||||
@@ -2460,7 +2470,7 @@ pub const LLVMEmitter = struct {
|
||||
call_args[1] = cls;
|
||||
call_args[2] = mid;
|
||||
for (msg.args, 0..) |arg_ref, i| {
|
||||
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
|
||||
const raw_ty = self.argIRTypeOrFail(arg_ref);
|
||||
const raw_llvm = self.toLLVMType(raw_ty);
|
||||
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
|
||||
call_param_types[i + 3] = coerced_ty;
|
||||
|
||||
Reference in New Issue
Block a user