fix: struct-literal → optional coercion + #get through optional chain (issue 0160)
Two fixes for optional interactions surfaced by the #set/#get review. The
original issue 0160 mis-diagnosed (A) as an optional-chain bug; the chain works
fine for real fields. The actual bugs:
(A) A bare struct literal `.{ ... }` against an optional target `?T` was built
into the optional's {payload, has_value} layout instead of the inner T, then
re-wrapped — corrupting the value (a multi-field payload's first field clobbered
by the has_value flag, or a `?T` arg silently null) or failing LLVM
verification. lowerStructLiteral now builds the inner T, materializes it, and
wraps via coerceToType; lowerVarDecl's previously-UNCONDITIONAL optional wrap is
guarded so an already-`?T` value isn't double-wrapped. Fixed across var-decl,
arg, return, nested field, reassignment, and array-element contexts.
(B) `#get` accessors are now reachable through an optional chain (`obj?.getter`):
lowerOptionalChain dispatches the getter via a synthetic receiver, and
expr_typer types `obj?.getter` through a shared getterReturnTypeOnDeref helper
(handles `?T` and `?*T`, value and pointer optionals, and generic-instance
getters like List.len). The `#set` write side through `?.` is intentionally left
matching real-field behavior (optional-chain assignment unsupported).
Regression tests: examples/optionals/0906 (struct-literal → optional) and 0907
(accessor through chain). issues/0160 marked RESOLVED with the corrected root
cause.
This commit is contained in:
@@ -95,6 +95,34 @@ pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: a
|
||||
return self.lowerUnionLiteral(sl, ty, span);
|
||||
}
|
||||
|
||||
// A bare struct literal against an optional target `?T` builds the INNER
|
||||
// `T` and wraps it once. Without this `ty` is the optional itself, so the
|
||||
// literal is lowered into the optional's `{payload, has_value}` layout and
|
||||
// then re-wrapped — corrupting the value (a `?T` arg silently reads as
|
||||
// null) or failing LLVM verification on the double wrap (issue 0160).
|
||||
// Only the bare `.{ ... }` form reaches here with an optional `ty` (a named
|
||||
// / generic literal resolves `ty` from its own type, never the target).
|
||||
if (!ty.isBuiltin() and self.module.types.get(ty) == .optional) {
|
||||
const child = self.module.types.get(ty).optional.child;
|
||||
// Build the inner `T` (targeting it so nested literals resolve), then
|
||||
// wrap to `?T`. Building into `ty` (the optional) directly would fill
|
||||
// its {payload, has_value} layout and corrupt the value / fail LLVM
|
||||
// verification. Wrapping the raw struct_init SSA aggregate ALSO mislays
|
||||
// a multi-field payload, so round-trip through memory first — the wrap
|
||||
// then sees a loaded value, the same shape the working `T -> ?T` value
|
||||
// coercion wraps. Returning a fully-built `?T` makes EVERY caller
|
||||
// context correct, including array/struct-literal element slots that
|
||||
// don't re-coerce (issue 0160).
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = child;
|
||||
const inner = self.lowerStructLiteral(sl, span);
|
||||
self.target_type = saved_tt;
|
||||
const slot = self.builder.alloca(child);
|
||||
self.builder.store(slot, inner);
|
||||
const reloaded = self.builder.load(slot, child);
|
||||
return self.coerceToType(reloaded, child, ty);
|
||||
}
|
||||
|
||||
// Get struct field types for coercion and ordering
|
||||
const struct_fields = self.getStructFields(ty);
|
||||
|
||||
@@ -802,8 +830,37 @@ pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess,
|
||||
break :blk if (info == .optional) info.optional.child else obj_ty;
|
||||
} else obj_ty;
|
||||
|
||||
// Get the field type on the inner type
|
||||
const field_ty = self.resolveFieldType(inner_ty, fa.field);
|
||||
// `#get` accessor through `?.`: if the unwrapped (and pointer-deref'd)
|
||||
// receiver has a getter for this field, the some-branch dispatches the
|
||||
// getter instead of a struct-field read. A synthetic receiver local (typed
|
||||
// `inner_ty`) lets the existing getter intercept in `lowerFieldAccess` do
|
||||
// the deref / address-of; we bind it type-only here for the return-type
|
||||
// query, then fill its ref in the some-branch (issue 0160).
|
||||
var deref_inner = inner_ty;
|
||||
if (!deref_inner.isBuiltin() and self.module.types.get(deref_inner) == .pointer)
|
||||
deref_inner = self.module.types.get(deref_inner).pointer.pointee;
|
||||
const getter_recv: ?[]const u8 = if (self.scope != null and self.getAccessorFor(deref_inner, fa.field) != null) blk: {
|
||||
var buf: [40]u8 = undefined;
|
||||
const nm = std.fmt.bufPrint(&buf, "$oc_recv_{d}", .{self.block_counter}) catch "$oc_recv";
|
||||
self.block_counter += 1;
|
||||
const owned = self.alloc.dupe(u8, nm) catch break :blk null;
|
||||
// Type-only binding for the return-type query: type it as a POINTER to
|
||||
// the struct so inference routes through the (working) pointer-deref
|
||||
// getter path for both `?T` and `?*T`. The some-branch re-binds it to
|
||||
// the actual unwrapped receiver before lowering.
|
||||
self.scope.?.put(owned, .{ .ref = Ref.none, .ty = self.module.types.ptrTo(deref_inner), .is_alloca = false });
|
||||
break :blk owned;
|
||||
} else null;
|
||||
const read_node: ?*Node = if (getter_recv) |nm| blk: {
|
||||
const id = self.alloc.create(Node) catch break :blk null;
|
||||
id.* = .{ .span = span, .data = .{ .identifier = .{ .name = nm } } };
|
||||
const rn = self.alloc.create(Node) catch break :blk null;
|
||||
rn.* = .{ .span = span, .data = .{ .field_access = .{ .object = id, .field = fa.field } } };
|
||||
break :blk rn;
|
||||
} else null;
|
||||
|
||||
// Get the field type on the inner type (the getter's return type, if any).
|
||||
const field_ty = if (read_node) |rn| self.inferExprType(rn) else self.resolveFieldType(inner_ty, fa.field);
|
||||
// If field is already optional, flatten (don't double-wrap)
|
||||
const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false;
|
||||
const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty);
|
||||
@@ -821,7 +878,24 @@ pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess,
|
||||
// Some: unwrap, access field (already ?FieldType if flattened, else wrap)
|
||||
self.builder.switchToBlock(some_bb);
|
||||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty);
|
||||
const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span);
|
||||
const field_val = if (read_node) |rn| blk: {
|
||||
// Re-bind the synthetic receiver to the unwrapped value, then dispatch
|
||||
// the getter through the normal field-access intercept. A `?*T` unwraps
|
||||
// to the pointer receiver directly; a `?T` unwraps to a value that the
|
||||
// getter (`self: *T`) needs to address, so materialize it into an alloca
|
||||
// and bind it like an ordinary `T` local (is_alloca).
|
||||
if (!inner_ty.isBuiltin() and self.module.types.get(inner_ty) == .pointer) {
|
||||
self.scope.?.put(getter_recv.?, .{ .ref = unwrapped, .ty = inner_ty, .is_alloca = false });
|
||||
} else {
|
||||
// Materialize the value and bind the alloca POINTER as a `*T` value
|
||||
// (not an is_alloca `T`), so the receiver path is identical to the
|
||||
// `?*T` case — a plain pointer receiver the getter intercept derefs.
|
||||
const slot = self.builder.alloca(inner_ty);
|
||||
self.builder.store(slot, unwrapped);
|
||||
self.scope.?.put(getter_recv.?, .{ .ref = slot, .ty = self.module.types.ptrTo(inner_ty), .is_alloca = false });
|
||||
}
|
||||
break :blk self.lowerExpr(rn);
|
||||
} else self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span);
|
||||
const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty);
|
||||
self.builder.br(merge_bb, &.{some_result});
|
||||
|
||||
@@ -886,6 +960,29 @@ pub fn getAccessorFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const as
|
||||
/// effective `field$set` name (so a same-name `#get` keeps the plain `field`),
|
||||
/// and a REAL field of the same name wins over it (parallels the `#get` rule).
|
||||
/// `ty` must be the dereferenced (non-pointer) receiver type.
|
||||
/// The return type of a `#get` accessor named `field` on `deref_ty` (a
|
||||
/// dereferenced struct type), or null when there is no such getter (or no scope
|
||||
/// to resolve through). Resolves the type the SAME way a real read does — via a
|
||||
/// synthetic `*deref_ty` receiver local routed through inference — so a generic
|
||||
/// instance getter (`List(T).len`) binds its type parameter exactly as the call
|
||||
/// path would. Shared by `lowerOptionalChain` and the optional-chain inference
|
||||
/// in `expr_typer`, so `obj?.getter` types identically to how it lowers.
|
||||
pub fn getterReturnTypeOnDeref(self: *Lowering, deref_ty: TypeId, field: []const u8) ?TypeId {
|
||||
if (deref_ty.isBuiltin()) return null;
|
||||
if (self.getAccessorFor(deref_ty, field) == null) return null;
|
||||
const s = self.scope orelse return null;
|
||||
var buf: [40]u8 = undefined;
|
||||
const nm = std.fmt.bufPrint(&buf, "$oc_ty_{d}", .{self.block_counter}) catch "$oc_ty";
|
||||
self.block_counter += 1;
|
||||
const owned = self.alloc.dupe(u8, nm) catch return null;
|
||||
s.put(owned, .{ .ref = Ref.none, .ty = self.module.types.ptrTo(deref_ty), .is_alloca = false });
|
||||
const id = self.alloc.create(Node) catch return null;
|
||||
id.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = owned } } };
|
||||
const rn = self.alloc.create(Node) catch return null;
|
||||
rn.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .field_access = .{ .object = id, .field = field } } };
|
||||
return self.inferExprType(rn);
|
||||
}
|
||||
|
||||
pub fn getSetterFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const ast.FnDecl {
|
||||
if (ty.isBuiltin()) return null;
|
||||
// A REAL field of this name wins over a same-name `#set` (a setter must not
|
||||
|
||||
Reference in New Issue
Block a user