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:
agra
2026-05-30 00:26:51 +03:00
parent a9c116ebb1
commit c6626b4f1a
2 changed files with 81 additions and 32 deletions

View File

@@ -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,
};
}

View File

@@ -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