ffi: more inferExprType silent-default holes — null_coalesce, struct const, reflection builtins

Three additional arms that previously silently fell through to
`.s64`:

- `.null_coalesce`: `lhs ?? rhs` now returns the inner type of
  lhs's optional (when applicable), else the rhs's inferred type.
  Without this, `print("{}\n", iw ?? 0.0)` for `iw: ?f32`
  inferred as s64 and the float value got truncated through the
  pack-mono's Any boxing.
- `.field_access` struct constant: `Phys.GRAVITY` (a `Struct.CONST`
  declaration) now consults `struct_const_map` for the resolved
  field type. Previously the path hit only `lowerFieldAccess`'s
  constant-resolution shortcut, not the AST-level `inferExprType`,
  so pack-fn callers misinferred the const's type as `.s64`.
- Reflection builtins (`type_name`, `type_eq`, `has_impl`,
  `field_count`, `field_index`, `field_name`, `is_flags`,
  `type_of`, `field_value`): their return types live outside
  `resolveBuiltin`'s table (they dispatch via
  `tryLowerReflectionCall` instead). Recognise them directly in
  the `inferExprType` call arm so pack-fn callers mangle the
  results with the right tag (.bool for `type_eq` / `has_impl` /
  `is_flags`, .string for `type_name` / `field_name`, etc).

All three holes surfaced while attempting the print/format
`..$args` migration; the fixes themselves are general
improvements and stand independently. 218/218.
This commit is contained in:
agra
2026-05-28 08:03:22 +03:00
parent ce77867566
commit b7c6ec24b0

View File

@@ -11868,6 +11868,20 @@ pub const Lowering = struct {
else => .s64,
};
}
// Reflection builtins live outside `resolveBuiltin`'s
// table (their lowering goes through
// `tryLowerReflectionCall`, not the `BuiltinId`
// dispatch). Recognize them here so pack-fn callers
// mangle their results with the right tag.
if (std.mem.eql(u8, bare_name, "type_name")) return .string;
if (std.mem.eql(u8, bare_name, "type_eq")) return .bool;
if (std.mem.eql(u8, bare_name, "has_impl")) return .bool;
if (std.mem.eql(u8, bare_name, "field_count")) return .s64;
if (std.mem.eql(u8, bare_name, "field_index")) return .s64;
if (std.mem.eql(u8, bare_name, "field_name")) return .string;
if (std.mem.eql(u8, bare_name, "is_flags")) return .bool;
if (std.mem.eql(u8, bare_name, "type_of")) return .any;
if (std.mem.eql(u8, bare_name, "field_value")) return .any;
// Check if it's a generic function — infer return type via type bindings
if (self.fn_ast_map.get(name)) |fd| {
if (fd.type_params.len > 0) {
@@ -12008,6 +12022,17 @@ pub const Lowering = struct {
if (ppc.contains(fa.object.data.identifier.name)) return .s64;
}
}
// Struct constant access: `Struct.CONST` — mirrors the
// lowerFieldAccess intercept (line 3851). Without this,
// `Phys.GRAVITY` (f64) inferred as s64 and pack-fn
// callers boxed the float into the int slot.
if (fa.object.data == .identifier) {
const obj_name = fa.object.data.identifier.name;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch fa.field;
if (self.struct_const_map.get(qualified)) |info| {
if (info.ty) |t| return t;
}
}
// M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
if (std.mem.eql(u8, fa.field, "class")) {
if (self.isObjcClassPointer(self.inferExprType(fa.object))) {
@@ -12172,6 +12197,20 @@ pub const Lowering = struct {
return .s64;
},
.chained_comparison => .bool,
.null_coalesce => |nc| blk: {
// `opt ?? default` — result is the inner type when lhs is
// optional (the unwrap path's value), else falls back to
// the rhs's type. Without this arm pack-fn callers
// misinferred float-optional coalesces as s64 and the
// pack mono mangled the arg as int — the actual f64 value
// got truncated through Any boxing.
const lhs_ty = self.inferExprType(nc.lhs);
if (!lhs_ty.isBuiltin()) {
const info = self.module.types.get(lhs_ty);
if (info == .optional) break :blk info.optional.child;
}
break :blk self.inferExprType(nc.rhs);
},
// Statements don't produce values
.assignment, .var_decl, .const_decl, .fn_decl, .return_stmt,
.defer_stmt, .push_stmt, .multi_assign, .destructure_decl,