fix: optional-chain getter/field correctness from 0160 adversarial review

Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the
touched optional-chain / optional-coercion code; all fixed here:

1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never
   called directly first) panicked with "unresolved type reached LLVM emission":
   a cold instance method is absent from resolveFuncByName, so the getter's
   return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain
   and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered)
   before querying its return type. (The 0907 test passed only by luck — List(i64)
   is warmed by stdlib use; 0907 now also exercises a cold user generic.)

2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted
   the pointer bits as the field (silent garbage) — the some-branch real-field
   path didn't load through the pointer. It now derefs `?*T` before the field
   access. (Pre-existing — the else-branch predates 0160 — but it's the same
   function and a silent miscompile, so fixed here.)

3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the
   lowerVarDecl optional arm wrapped the raw array. It now coerces the value to
   the optional's child type (array→slice) before wrapping.

Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING
bugs the reviews surfaced in untouched subsystems are filed as issues 0161
(struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163
(untagged-union payload-binding match).
This commit is contained in:
agra
2026-06-22 18:55:41 +03:00
parent 1b0c857b91
commit ff9e448f8c
10 changed files with 214 additions and 4 deletions

View File

@@ -859,6 +859,16 @@ pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess,
break :blk rn;
} else null;
// Warm a generic-instance getter's monomorph BEFORE typing the chain: a
// cold instance method is absent from `resolveFuncByName`, so `resultType`
// would resolve the getter to `.unresolved` → a `?unresolved` merge type
// that panics at LLVM emission. Lowering it now binds its type parameter so
// both the type query and the some-branch call see the concrete signature.
if (getter_recv != null) {
const tn = self.formatTypeName(deref_inner);
if (self.genericInstanceMethod(tn, fa.field)) |gm| _ = self.ensureGenericInstanceMethodLowered(gm);
}
// 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)
@@ -895,7 +905,21 @@ pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess,
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);
} else blk: {
// Real-field read. For a `?*T` the unwrapped value is the POINTER, so
// load through it before the struct-field access — `lowerFieldAccessOnType`
// does not auto-deref, and a `structGet` on the raw pointer would read
// the pointer bits as the field (silent garbage). A `?T` value optional
// accesses the unwrapped value directly.
var fobj = unwrapped;
var fty = inner_ty;
if (!fty.isBuiltin() and self.module.types.get(fty) == .pointer) {
const pointee = self.module.types.get(fty).pointer.pointee;
fobj = self.builder.load(unwrapped, pointee);
fty = pointee;
}
break :blk self.lowerFieldAccessOnType(fobj, fty, 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});
@@ -971,6 +995,10 @@ pub fn getterReturnTypeOnDeref(self: *Lowering, deref_ty: TypeId, field: []const
if (deref_ty.isBuiltin()) return null;
if (self.getAccessorFor(deref_ty, field) == null) return null;
const s = self.scope orelse return null;
// Warm a generic-instance getter's monomorph so its return type resolves
// (cold instance methods are absent from `resolveFuncByName` → `.unresolved`).
if (self.genericInstanceMethod(self.formatTypeName(deref_ty), field)) |gm|
_ = self.ensureGenericInstanceMethodLowered(gm);
var buf: [40]u8 = undefined;
const nm = std.fmt.bufPrint(&buf, "$oc_ty_{d}", .{self.block_counter}) catch "$oc_ty";
self.block_counter += 1;