fix: gate implicit optional unwrap on flow narrowing (issue 0179)
Optional (?T) operands were implicitly unwrapped without proof of
presence, silently miscompiling a NULL ?T to garbage. Unwraps in
binary ops and other expression positions are now gated on flow
narrowing: a ?T value is only auto-unwrapped where control flow has
established it is non-null (the narrowed_refs set). Outside a narrowed
region, an implicit unwrap is rejected rather than producing garbage.
Touches the lowering pipeline (lower.zig + lower/{call,closure,coerce,
comptime,control_flow,expr,ffi,generic,pack,stmt}.zig). Adds optionals
examples 0919-0923 and closures example 0312 covering flow narrowing,
binop narrowing, no-implicit-unwrap rejection, and no closure leak of
narrowed state. Updates specs.md and readme.md.
This commit is contained in:
@@ -563,6 +563,9 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
const ty_info = self.module.types.get(binding.ty);
|
||||
if (ty_info == .closure) {
|
||||
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
|
||||
// Coerce user args to the closure's param types
|
||||
// (issue 0186) — a `?T` param must wrap the arg.
|
||||
coerceClosureCallArgs(self, args.items, ty_info.closure.params);
|
||||
// Closure trampolines carry `__sx_ctx` at
|
||||
// slot 0; emit_llvm's `call_closure` builds
|
||||
// the call as [ctx, env, user_args], so we
|
||||
@@ -656,6 +659,17 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
const bti = self.module.types.get(binding.ty);
|
||||
break :blk if (bti == .function) bti.function.ret else .i64;
|
||||
} else .i64;
|
||||
// Coerce user args to the fn-pointer's param types (issue
|
||||
// 0186) — same as the closure-value and global-fn-pointer
|
||||
// paths. The arg loop already applied implicit address-of
|
||||
// for `*T` params (resolveCallParamTypes now surfaces the
|
||||
// `.function` param types), so this completes value
|
||||
// coercions like a `?T` wrap. Without it a concrete arg to a
|
||||
// `?T` fn-ptr param reaches `call_indirect` unconverted.
|
||||
if (!binding.ty.isBuiltin()) {
|
||||
const bti = self.module.types.get(binding.ty);
|
||||
if (bti == .function) coerceClosureCallArgs(self, args.items, bti.function.params);
|
||||
}
|
||||
var final_args = std.ArrayList(Ref).empty;
|
||||
defer final_args.deinit(self.alloc);
|
||||
if (self.fnPtrTypeWantsCtx(binding.ty)) {
|
||||
@@ -965,6 +979,8 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
agg = self.builder.load(obj, oi.pointer.pointee);
|
||||
}
|
||||
const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty);
|
||||
// Coerce user args to the closure's param types (issue 0186).
|
||||
coerceClosureCallArgs(self, args.items, fti.closure.params);
|
||||
// Prepend ctx for sx-side closure call ABI.
|
||||
const owned = if (self.implicit_ctx_enabled) blk: {
|
||||
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
||||
@@ -1368,6 +1384,8 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
const cti = self.module.types.get(callee_ty);
|
||||
if (cti == .closure) {
|
||||
const callee_ref = self.lowerExpr(c.callee);
|
||||
// Coerce user args to the closure's param types (issue 0186).
|
||||
coerceClosureCallArgs(self, args.items, cti.closure.params);
|
||||
// Prepend implicit ctx for the sx-side closure call ABI
|
||||
// (emit_llvm builds the call as [ctx, env, user_args]).
|
||||
const owned = if (self.implicit_ctx_enabled) blk: {
|
||||
@@ -2635,6 +2653,22 @@ pub fn userParamTypes(self: *Lowering, func: *const Function) []TypeId {
|
||||
/// nothing as nominal names in that module: without this call's inferred
|
||||
/// `$T → concrete` bindings the pin would resolve `T` as an undeclared
|
||||
/// type in a non-main module and diagnose it unknown.
|
||||
/// Coerce already-lowered closure-call arguments to the closure's declared
|
||||
/// parameter types (issue 0186). The arg-lowering loop only sets `target_type`
|
||||
/// (which steers literal lowering) but does NOT itself coerce, so a concrete
|
||||
/// `7` flowing into a `?i64` param would reach `call_closure` as a bare `i64`
|
||||
/// (read ABSENT by the callee) and a `null` as a bare pointer (LLVM verifier
|
||||
/// failure). `args` are the USER args (no implicit ctx); `params` the closure's
|
||||
/// user-visible param types. Coerces in place.
|
||||
fn coerceClosureCallArgs(self: *Lowering, args: []Ref, params: []const TypeId) void {
|
||||
const n = @min(args.len, params.len);
|
||||
for (0..n) |i| {
|
||||
if (args[i].isNone()) continue; // spread placeholder
|
||||
const at = self.builder.getRefType(args[i]);
|
||||
if (at != params[i]) args[i] = self.coerceToType(args[i], at, params[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn astCalleeParamTypes(self: *Lowering, fd: *const ast.FnDecl, args: []const *const Node) []const TypeId {
|
||||
const saved_bindings = self.type_bindings;
|
||||
defer self.type_bindings = saved_bindings;
|
||||
@@ -2788,6 +2822,22 @@ pub fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*
|
||||
}
|
||||
if (c.callee.data != .identifier) return &.{};
|
||||
const bare_name = c.callee.data.identifier.name;
|
||||
// Closure / fn-pointer VALUE bound in scope (`g := () => ...; g(args)`):
|
||||
// type each arg against the callee value's declared parameter types so a
|
||||
// `?T` param wraps the argument (issue 0186) — without this the args lower
|
||||
// with no target type and reach `call_closure` unconverted (a concrete arg
|
||||
// arrives as a bare payload that reads ABSENT; `null` reaches a `{T,i1}`
|
||||
// slot as a bare pointer → LLVM verifier failure). A local value shadows a
|
||||
// same-named function, so this precedes the function-name resolution below.
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(bare_name)) |binding| {
|
||||
if (!binding.ty.isBuiltin()) {
|
||||
const bti = self.module.types.get(binding.ty);
|
||||
if (bti == .closure) return bti.closure.params;
|
||||
if (bti == .function) return bti.function.params;
|
||||
}
|
||||
}
|
||||
}
|
||||
const name = blk: {
|
||||
const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
||||
if (self.program_index.ufcs_alias_map.get(bare_name)) |target| {
|
||||
|
||||
Reference in New Issue
Block a user