ir: make inferExprType honest (.unresolved, not .s64) + fix its consumers
inferExprType now returns .unresolved when it genuinely cannot infer a type, instead of silently guessing .s64. To keep codegen correct, every consumer that turns inference into a concrete type was fixed to resolve it properly rather than lean on the fake s64: - pack-fn mono: value-pack params type from the lowered Ref (getRefType); comptime ..$args prefers inference (int-literal default is s64) and falls back to the lowered type only when inference cannot tell. - if-expr / match merge result type: fall back to the contextual target_type when the branch/arm type is not statically inferable; a statement match with non-value arms stays void (do not let a leaked target_type make it a value). - inferExprType call arm: resolve a not-yet-lowered function return type from fn_ast_map (void for a return-less fn) instead of falling through. - lowerBinaryOp: type the result from the lowered LHS when inference is unresolved (e.g. #objc_call(...) * 2). - null comparison (x == null): lower the non-null side first and take the null type from it, never a guess. A consequence: `xx enum` with no target type now boxes as Any (prints the variant name) instead of the silent-s64 int -- examples/52 snapshot updated to the honest output. 236 examples + unit tests green.
This commit is contained in:
107
src/ir/lower.zig
107
src/ir/lower.zig
@@ -2565,12 +2565,20 @@ pub const Lowering = struct {
|
||||
const null_on_rhs = bop.rhs.data == .null_literal;
|
||||
const null_on_lhs = bop.lhs.data == .null_literal;
|
||||
if (null_on_rhs or null_on_lhs) {
|
||||
const other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs);
|
||||
if (other_ty != .void) {
|
||||
var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs);
|
||||
// Lower the non-null side first when its type isn't statically
|
||||
// inferable, and take the null's type from the lowered value —
|
||||
// never a guess.
|
||||
var pre_lowered: ?Ref = null;
|
||||
if (other_ty == .unresolved) {
|
||||
pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs);
|
||||
other_ty = self.builder.getRefType(pre_lowered.?);
|
||||
}
|
||||
if (other_ty != .void and other_ty != .unresolved) {
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = other_ty;
|
||||
const lv = self.lowerExpr(bop.lhs);
|
||||
const rv = self.lowerExpr(bop.rhs);
|
||||
const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?;
|
||||
const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?;
|
||||
self.target_type = saved_tt;
|
||||
const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } };
|
||||
return self.builder.emit(cmp_op, .bool);
|
||||
@@ -2578,8 +2586,13 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
var lhs = self.lowerExpr(bop.lhs);
|
||||
// Set target_type from LHS so enum literals on RHS resolve correctly
|
||||
const lhs_ty = self.inferExprType(bop.lhs);
|
||||
// Set target_type from LHS so enum literals on RHS resolve correctly.
|
||||
// When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use
|
||||
// the lowered operand's concrete type rather than a guess.
|
||||
const lhs_ty = blk: {
|
||||
const it = self.inferExprType(bop.lhs);
|
||||
break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it;
|
||||
};
|
||||
const saved_tt = self.target_type;
|
||||
if (lhs_ty != .void) {
|
||||
if (!lhs_ty.isBuiltin()) {
|
||||
@@ -2874,11 +2887,16 @@ pub const Lowering = struct {
|
||||
// Infer result type from then branch for value if-exprs
|
||||
// If then_branch is null/void, try else_branch (e.g., `if cond then null else val`)
|
||||
const result_type: TypeId = if (is_value) blk: {
|
||||
const then_ty = self.inferExprType(ie.then_branch);
|
||||
if (then_ty == .void and ie.else_branch != null) {
|
||||
break :blk self.inferExprType(ie.else_branch.?);
|
||||
var t = self.inferExprType(ie.then_branch);
|
||||
if ((t == .void or t == .unresolved) and ie.else_branch != null) {
|
||||
t = self.inferExprType(ie.else_branch.?);
|
||||
}
|
||||
break :blk then_ty;
|
||||
// Branch type not statically inferable (e.g. `null` / a bare enum
|
||||
// literal) — use the contextually expected type rather than a guess.
|
||||
if (t == .unresolved) {
|
||||
if (self.target_type) |tt| t = tt;
|
||||
}
|
||||
break :blk t;
|
||||
} else .void;
|
||||
|
||||
const then_bb = self.freshBlock("if.then");
|
||||
@@ -3446,8 +3464,16 @@ pub const Lowering = struct {
|
||||
// Determine if the match produces a value (has non-void arms)
|
||||
// For type-category matches (inside any_to_string), only produce value when force_block_value
|
||||
// For regular enum/optional matches, always produce value if arms are non-void
|
||||
const inferred_result = self.inferMatchResultType(me);
|
||||
const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or inferred_result != .void);
|
||||
var inferred_result = self.inferMatchResultType(me);
|
||||
// Arms not statically inferable (bare enum literals etc.): only a
|
||||
// value-position match (`force_block_value`) needs a concrete result —
|
||||
// use the contextually expected type. A statement match with non-value
|
||||
// arms is a side-effect (void); don't let a leaked `target_type` turn
|
||||
// it into a value match.
|
||||
if (inferred_result == .unresolved) {
|
||||
inferred_result = if (self.force_block_value) (self.target_type orelse .unresolved) else .void;
|
||||
}
|
||||
const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved));
|
||||
const result_type: TypeId = if (is_value) inferred_result else .void;
|
||||
const merge_params: []const TypeId = if (is_value and result_type != .void) &.{result_type} else &.{};
|
||||
const merge_bb = self.freshBlockWithParams("match.merge", merge_params);
|
||||
@@ -8977,8 +9003,16 @@ pub const Lowering = struct {
|
||||
}
|
||||
for (call_node.args[pack_start..]) |a| {
|
||||
if (pack_is_comptime) {
|
||||
args.append(self.alloc, self.lowerExpr(a)) catch return self.builder.constInt(0, .void);
|
||||
pack_arg_types.append(self.alloc, self.inferExprType(a)) catch return self.builder.constInt(0, .void);
|
||||
// A comptime `..$args` arg's intended type follows the
|
||||
// language default (e.g. an int literal is s64) which
|
||||
// `inferExprType` encodes; the lowered value may be narrower
|
||||
// (s32). Prefer inference; fall back to the lowered value's
|
||||
// type only when inference genuinely can't tell.
|
||||
const r = self.lowerExpr(a);
|
||||
args.append(self.alloc, r) catch return self.builder.constInt(0, .void);
|
||||
const it = self.inferExprType(a);
|
||||
const ty = if (it == .unresolved) self.builder.getRefType(r) else it;
|
||||
pack_arg_types.append(self.alloc, ty) catch return self.builder.constInt(0, .void);
|
||||
} else {
|
||||
const r = self.lowerExpr(a);
|
||||
args.append(self.alloc, r) catch return self.builder.constInt(0, .void);
|
||||
@@ -10230,6 +10264,7 @@ pub const Lowering = struct {
|
||||
// If we skip null_literal arms and find a concrete type T, and there
|
||||
// were null arms, the result is ?T (optional).
|
||||
var has_null = false;
|
||||
var saw_unresolved = false;
|
||||
for (me.arms) |arm| {
|
||||
const last_node = if (arm.body.data == .block) blk: {
|
||||
if (arm.body.data.block.stmts.len > 0) {
|
||||
@@ -10243,14 +10278,21 @@ pub const Lowering = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
// First non-null arm determines the type (same as old behavior)
|
||||
// First arm with a statically-inferable type determines the result.
|
||||
// An arm whose type isn't inferable from the AST alone (e.g. a bare
|
||||
// enum literal) doesn't decide — keep looking; the caller falls back
|
||||
// to the contextual target type if none of the arms resolve.
|
||||
const arm_ty = self.inferExprType(last_node);
|
||||
if (arm_ty == .unresolved) {
|
||||
saw_unresolved = true;
|
||||
continue;
|
||||
}
|
||||
if (has_null and arm_ty != .void) {
|
||||
return self.module.types.optionalOf(arm_ty);
|
||||
}
|
||||
return arm_ty;
|
||||
}
|
||||
return .void;
|
||||
return if (saw_unresolved) .unresolved else .void;
|
||||
}
|
||||
|
||||
fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool {
|
||||
@@ -12711,12 +12753,12 @@ pub const Lowering = struct {
|
||||
.unary_op => |uop| switch (uop.op) {
|
||||
.not => .bool,
|
||||
.negate => self.inferExprType(uop.operand),
|
||||
.xx => self.target_type orelse .s64,
|
||||
.xx => self.target_type orelse .unresolved,
|
||||
.address_of => blk: {
|
||||
const inner = self.inferExprType(uop.operand);
|
||||
break :blk self.module.types.ptrTo(inner);
|
||||
},
|
||||
else => .s64,
|
||||
else => .unresolved,
|
||||
},
|
||||
.if_expr => |ie| {
|
||||
// If-else: infer from then branch
|
||||
@@ -12753,8 +12795,8 @@ pub const Lowering = struct {
|
||||
break :blk TypeId.f64;
|
||||
},
|
||||
.size_of, .align_of => .s64,
|
||||
.cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .s64,
|
||||
else => .s64,
|
||||
.cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .unresolved,
|
||||
else => .unresolved,
|
||||
};
|
||||
}
|
||||
// Reflection builtins live outside `resolveBuiltin`'s
|
||||
@@ -12781,6 +12823,13 @@ pub const Lowering = struct {
|
||||
if (self.resolveFuncByName(name)) |fid| {
|
||||
return self.module.functions.items[@intFromEnum(fid)].ret;
|
||||
}
|
||||
// Not lowered yet (lazy lowering): take the return type from
|
||||
// the declared AST. A void/return-less fn is void — not an
|
||||
// `.unresolved` guess.
|
||||
if (self.fn_ast_map.get(name)) |fd| {
|
||||
if (fd.return_type) |rt| return self.resolveType(rt);
|
||||
return .void;
|
||||
}
|
||||
// Check if callee is a local closure variable — extract return type
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(bare_name)) |binding| {
|
||||
@@ -12898,9 +12947,9 @@ pub const Lowering = struct {
|
||||
}
|
||||
} else if (c.callee.data == .enum_literal) {
|
||||
// .Variant(args) — dot-shorthand enum construction
|
||||
return self.target_type orelse .s64;
|
||||
return self.target_type orelse .unresolved;
|
||||
}
|
||||
return .s64;
|
||||
return .unresolved;
|
||||
},
|
||||
.field_access => |fa| {
|
||||
// Pack-arity intercept: `<pack_name>.len` is s64. Mirrors
|
||||
@@ -13008,7 +13057,7 @@ pub const Lowering = struct {
|
||||
if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty;
|
||||
}
|
||||
}
|
||||
return .s64;
|
||||
return .unresolved;
|
||||
},
|
||||
.identifier => |id| {
|
||||
if (self.scope) |scope| {
|
||||
@@ -13033,7 +13082,7 @@ pub const Lowering = struct {
|
||||
// builtin primitive) referenced in expression position
|
||||
// is a Type value — IR type `.any`.
|
||||
if (self.isKnownTypeName(id.name)) return .any;
|
||||
return .s64;
|
||||
return .unresolved;
|
||||
},
|
||||
.type_expr => |te| {
|
||||
// type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type)
|
||||
@@ -13045,11 +13094,11 @@ pub const Lowering = struct {
|
||||
// A bare type name in expression position (e.g. `s64`,
|
||||
// `Point`, `*u8`) is a Type value — IR type `.any`.
|
||||
if (self.isKnownTypeName(te.name)) return .any;
|
||||
return .s64;
|
||||
return .unresolved;
|
||||
},
|
||||
.enum_literal => {
|
||||
// Enum literals depend on context — use target_type if available
|
||||
return self.target_type orelse .s64;
|
||||
return self.target_type orelse .unresolved;
|
||||
},
|
||||
.struct_literal => |sl| {
|
||||
if (sl.struct_name) |name| {
|
||||
@@ -13057,7 +13106,7 @@ pub const Lowering = struct {
|
||||
return self.module.types.findByName(name_id) orelse
|
||||
self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
|
||||
}
|
||||
return self.target_type orelse .s64;
|
||||
return self.target_type orelse .unresolved;
|
||||
},
|
||||
.tuple_literal => |tl| {
|
||||
var field_types = std.ArrayList(TypeId).empty;
|
||||
@@ -13105,7 +13154,7 @@ pub const Lowering = struct {
|
||||
const info = self.module.types.get(ptr_ty);
|
||||
if (info == .pointer) return info.pointer.pointee;
|
||||
}
|
||||
return .s64;
|
||||
return .unresolved;
|
||||
},
|
||||
.chained_comparison => .bool,
|
||||
.null_coalesce => |nc| blk: {
|
||||
@@ -13126,7 +13175,7 @@ pub const Lowering = struct {
|
||||
.assignment, .var_decl, .const_decl, .fn_decl, .return_stmt,
|
||||
.defer_stmt, .push_stmt, .multi_assign, .destructure_decl,
|
||||
=> .void,
|
||||
else => .s64,
|
||||
else => .unresolved,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ok: floating is null
|
||||
ok: left h=0
|
||||
ok: center h=1
|
||||
ok: top_right h=2 v=0
|
||||
ok: left h=.leading
|
||||
ok: center h=.center
|
||||
ok: top_right h=.trailing v=.top
|
||||
rect
|
||||
text
|
||||
|
||||
Reference in New Issue
Block a user