diff --git a/src/ir/expr_typer.test.zig b/src/ir/expr_typer.test.zig new file mode 100644 index 0000000..46db0f0 --- /dev/null +++ b/src/ir/expr_typer.test.zig @@ -0,0 +1,75 @@ +// Tests for expr_typer.zig — focused on the structural (non-call) expression +// shapes ExprTyper owns, reached via the public `Lowering.inferExprType` +// delegation. These cases need no lexical scope / program-index state, so a +// bare `Lowering.init` suffices. + +const std = @import("std"); +const ast = @import("../ast.zig"); +const Node = ast.Node; + +const ir_mod = @import("ir.zig"); +const TypeId = ir_mod.TypeId; +const Lowering = ir_mod.Lowering; + +fn node(data: ast.Node.Data) Node { + return .{ .span = .{ .start = 0, .end = 0 }, .data = data }; +} + +test "expr_typer: literal shapes" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + + var int_n = node(.{ .int_literal = .{ .value = 7 } }); + var float_n = node(.{ .float_literal = .{ .value = 1.5 } }); + var bool_n = node(.{ .bool_literal = .{ .value = true } }); + var str_n = node(.{ .string_literal = .{ .raw = "hi" } }); + + try std.testing.expectEqual(TypeId.s64, l.inferExprType(&int_n)); + try std.testing.expectEqual(TypeId.f64, l.inferExprType(&float_n)); + try std.testing.expectEqual(TypeId.bool, l.inferExprType(&bool_n)); + try std.testing.expectEqual(TypeId.string, l.inferExprType(&str_n)); +} + +test "expr_typer: binary comparison is bool, arithmetic takes lhs type" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + + var lhs = node(.{ .int_literal = .{ .value = 1 } }); + var rhs = node(.{ .int_literal = .{ .value = 2 } }); + + var cmp = node(.{ .binary_op = .{ .op = .eq, .lhs = &lhs, .rhs = &rhs } }); + try std.testing.expectEqual(TypeId.bool, l.inferExprType(&cmp)); + + var add = node(.{ .binary_op = .{ .op = .add, .lhs = &lhs, .rhs = &rhs } }); + try std.testing.expectEqual(TypeId.s64, l.inferExprType(&add)); +} + +test "expr_typer: unary not is bool, negate preserves operand type" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + + var b = node(.{ .bool_literal = .{ .value = false } }); + var not_n = node(.{ .unary_op = .{ .op = .not, .operand = &b } }); + try std.testing.expectEqual(TypeId.bool, l.inferExprType(¬_n)); + + var f = node(.{ .float_literal = .{ .value = 2.0 } }); + var neg_n = node(.{ .unary_op = .{ .op = .negate, .operand = &f } }); + try std.testing.expectEqual(TypeId.f64, l.inferExprType(&neg_n)); +} + +test "expr_typer: deref of a non-pointer is unresolved" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + + var i = node(.{ .int_literal = .{ .value = 0 } }); + var deref_n = node(.{ .deref_expr = .{ .operand = &i } }); + try std.testing.expectEqual(TypeId.unresolved, l.inferExprType(&deref_n)); +} diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig new file mode 100644 index 0000000..efd02e2 --- /dev/null +++ b/src/ir/expr_typer.zig @@ -0,0 +1,336 @@ +const std = @import("std"); +const ast = @import("../ast.zig"); +const types = @import("types.zig"); +const lower = @import("lower.zig"); + +const Node = ast.Node; +const TypeId = types.TypeId; +const Lowering = lower.Lowering; + +/// AST-level expression typing (architecture phase A3.1), extracted from +/// `Lowering.inferExprType`. Owns the structural / non-call expression shapes — +/// literals, unary / binary ops, `try` / `catch`, `if`, block, field access, +/// identifier / type-name, struct / tuple literals, index / slice / deref, +/// null-coalesce, and the statement shapes that produce no value. Call result +/// typing stays in `Lowering` for this step; it converges into `CallResolver` +/// in A3.2. +/// +/// A `*Lowering` facade (Principle 5), like `PackResolver`: expression typing +/// reads live lexical-scope / pack / target-type state and dozens of resolver +/// helpers, so it borrows `Lowering` rather than re-threading every field. The +/// dependency shrinks as later phases lift that state into an explicit context +/// (the plan's `TypeResolver` / `ProgramIndex` / `ResolveEnv` target). +pub const ExprTyper = struct { + l: *Lowering, + + /// Infer the IR type an expression evaluates to (without lowering it). + /// Recurses through `Lowering.inferExprType` so a nested call node is typed + /// by its one owner. + pub fn inferType(self: ExprTyper, node: *const Node) TypeId { + return switch (node.data) { + // Call result typing stays in `Lowering` (A3.2 converges it into + // `CallResolver`); delegate so a call node reaching here is handled + // by the single owner rather than mistyped as `.unresolved`. + .call => self.l.inferExprType(node), + .string_literal => .string, + .int_literal => .s64, + .float_literal => .f64, + .bool_literal => .bool, + .null_literal => .void, + .binary_op => |bop| switch (bop.op) { + .or_op => blk: { + // A failable `or` (value-terminator or chain) yields the + // chain's success type (the error is absorbed/propagated); + // a non-failable `or` is boolean / optional-unwrap → bool. + // Detected structurally — a `try`-chain's operands type as + // non-failable `T`, so a type-only check would miss it. + if (self.l.orIsFailableChain(&bop)) break :blk self.l.orChainSuccessType(&bop); + break :blk .bool; + }, + .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool, + else => self.l.inferExprType(bop.lhs), + }, + .unary_op => |uop| switch (uop.op) { + .not => .bool, + .negate => self.l.inferExprType(uop.operand), + .xx => self.l.target_type orelse .unresolved, + .address_of => blk: { + const inner = self.l.inferExprType(uop.operand); + break :blk self.l.module.types.ptrTo(inner); + }, + else => .unresolved, + }, + // `try X` evaluates to X's success type (the value part). A + // pure-failable operand (`-> !` / `-> !Named`, whose type IS the + // error set) has no value → `void`; a value-carrying `-> (T..., !)` + // operand yields its value part (the lone value, or a value-tuple). + .try_expr => |te| blk: { + const op_ty = self.l.inferExprType(te.operand); + const channel = self.l.errorChannelOf(op_ty) orelse break :blk .unresolved; + if (op_ty == channel) break :blk .void; + break :blk self.l.failableSuccessType(op_ty); + }, + // `expr catch ...` strips the error channel → the success type + // (void for a pure-failable LHS; the value part for value-carrying). + .catch_expr => |ce| blk: { + const op_ty = self.l.inferExprType(ce.operand); + const channel = self.l.errorChannelOf(op_ty) orelse break :blk .unresolved; + if (op_ty == channel) break :blk .void; + break :blk self.l.failableSuccessType(op_ty); + }, + .caller_location => self.l.module.types.findByName(self.l.module.types.internString("Source_Location")) orelse .unresolved, + .if_expr => |ie| { + // If-else types as its branches' unified type. A `noreturn` + // branch (one that diverges — `return` / `raise` / `break` / + // `continue`) unifies away, so the expression takes the other + // branch's type; both diverging → `noreturn` (ERR E1.4c). + if (ie.else_branch) |eb| { + const then_ty = self.l.inferExprType(ie.then_branch); + if (then_ty == .noreturn) return self.l.inferExprType(eb); + return then_ty; + } + return .void; + }, + // Divergence shapes type as `noreturn` — they transfer control and + // produce no value at their site. A block whose last statement is + // one of these propagates `noreturn` (block arm below), which lets + // a `catch` body that ends in `return` / `raise` unify with the + // success type (ERR E1.4c / E1.5). + .return_stmt, .raise_stmt, .break_expr, .continue_expr => .noreturn, + .block => |blk| { + // A block's type is its last expression's type only when it + // produces a value (no trailing `;`); otherwise it is void. + if (blk.produces_value and blk.stmts.len > 0) { + return self.l.inferExprType(blk.stmts[blk.stmts.len - 1]); + } + return .void; + }, + .field_access => |fa| { + // Pack-arity intercept: `.len` is s64. Mirrors + // the lowerFieldAccess intercept so AST-level type + // inference picks the same shape. + if (self.l.pack_param_count) |ppc| { + if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { + 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.l.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch fa.field; + if (self.l.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.l.isObjcClassPointer(self.l.inferExprType(fa.object))) { + return self.l.module.types.ptrTo(.void); + } + } + // M2.2 — `obj.field` for an Obj-C `#property` field returns the field's type. + if (self.l.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { + return self.l.resolveType(prop.field_type); + } + // M1.2 A.3 — sx-defined class state field returns the field's type. + if (self.l.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { + return info.field_ty; + } + + var obj_ty = self.l.inferExprType(fa.object); + // Auto-deref: if object is a pointer, resolve through it (matches lowerFieldAccess behavior) + if (!obj_ty.isBuiltin()) { + const ptr_info = self.l.module.types.get(obj_ty); + if (ptr_info == .pointer) { + obj_ty = ptr_info.pointer.pointee; + } + } + // Optional chaining: ?T.field → ?FieldType (flattened if field is already optional) + const is_opt_chain = fa.is_optional; + if (is_opt_chain and !obj_ty.isBuiltin()) { + const opt_info = self.l.module.types.get(obj_ty); + if (opt_info == .optional) { + obj_ty = opt_info.optional.child; + } + } + if (std.mem.eql(u8, fa.field, "len")) return if (is_opt_chain) self.l.module.types.optionalOf(.s64) else .s64; + if (std.mem.eql(u8, fa.field, "ptr")) { + // .ptr on slice/string → [*]element_type + const elem_ty = self.l.getElementType(obj_ty); + const mp_ty = self.l.module.types.manyPtrTo(elem_ty); + return if (is_opt_chain) self.l.module.types.optionalOf(mp_ty) else mp_ty; + } + if (!obj_ty.isBuiltin()) { + const field_name_id = self.l.module.types.internString(fa.field); + // Check union fields (tagged enum payloads) + promoted struct fields + const info = self.l.module.types.get(obj_ty); + const u_fields2: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { + .@"union" => |u| u.fields, + .tagged_union => |u| u.fields, + else => null, + }; + if (u_fields2) |ufields| { + for (ufields) |f| { + if (f.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(f.ty) else f.ty; + // Check promoted fields from anonymous struct variants + if (!f.ty.isBuiltin()) { + const fi = self.l.module.types.get(f.ty); + if (fi == .@"struct") { + for (fi.@"struct".fields) |sf| { + if (sf.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(sf.ty) else sf.ty; + } + } + } + } + } + // Check vector element access (.x/.y/.z/.w) + if (info == .vector) { + const elem = info.vector.element; + return if (is_opt_chain) self.l.optionalOfFlattened(elem) else elem; + } + // Tuple field access: numeric `t.0` or named `t.x`. + if (info == .tuple) { + const tup = info.tuple; + if (std.fmt.parseInt(usize, fa.field, 10)) |idx| { + if (idx < tup.fields.len) + return if (is_opt_chain) self.l.optionalOfFlattened(tup.fields[idx]) else tup.fields[idx]; + } else |_| {} + if (tup.names) |names| { + for (names, 0..) |nm, i| { + if (nm == field_name_id and i < tup.fields.len) + return if (is_opt_chain) self.l.optionalOfFlattened(tup.fields[i]) else tup.fields[i]; + } + } + } + // Check struct fields + const fields = self.l.getStructFields(obj_ty); + for (fields) |f| { + if (f.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(f.ty) else f.ty; + } + } + return .unresolved; + }, + .identifier => |id| { + if (self.l.scope) |scope| { + if (scope.lookup(id.name)) |binding| { + return binding.ty; + } + } + // `context` is the implicit-ctx identifier; type is Context + // when the program has registered it (i.e. std.sx imported). + if (self.l.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) { + if (self.l.module.types.findByName(self.l.module.types.internString("Context"))) |ty| return ty; + } + // Check global variables (e.g., `context : Context`) + if (self.l.program_index.global_names.get(id.name)) |gi| { + return gi.ty; + } + // Check module-level value constants (e.g., WIDTH :f32: 800) + if (self.l.program_index.module_const_map.get(id.name)) |ci| { + return ci.ty; + } + // A bare type name (alias like `Vec4`, struct name, or + // builtin primitive) referenced in expression position + // is a Type value — IR type `.any`. + if (self.l.isKnownTypeName(id.name)) return .any; + return .unresolved; + }, + .type_expr => |te| { + // type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type) + if (self.l.scope) |scope| { + if (scope.lookup(te.name)) |binding| { + return binding.ty; + } + } + // A bare type name in expression position (e.g. `s64`, + // `Point`, `*u8`) is a Type value — IR type `.any`. + if (self.l.isKnownTypeName(te.name)) return .any; + return .unresolved; + }, + .enum_literal => { + // Enum literals depend on context — use target_type if available + return self.l.target_type orelse .unresolved; + }, + .struct_literal => |sl| { + if (sl.struct_name) |name| { + const name_id = self.l.module.types.internString(name); + return self.l.module.types.findByName(name_id) orelse + self.l.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); + } + return self.l.target_type orelse .unresolved; + }, + .tuple_literal => |tl| { + var field_types = std.ArrayList(TypeId).empty; + defer field_types.deinit(self.l.alloc); + for (tl.elements) |elem| { + field_types.append(self.l.alloc, self.l.inferExprType(elem.value)) catch unreachable; + } + return self.l.module.types.intern(.{ .tuple = .{ + .fields = self.l.alloc.dupe(TypeId, field_types.items) catch unreachable, + .names = null, + } }); + }, + .index_expr => |ie| { + // Pack-arg type lookup: `[]`. + // Read directly from `pack_arg_types` — bypasses the + // synthesized-ident detour in `pack_arg_nodes` which + // would otherwise lose the type when the mono's + // scope isn't set up yet (generic-`$R` pre-inference). + if (self.l.pack_arg_types) |pat| { + if (ie.object.data == .identifier) { + if (pat.get(ie.object.data.identifier.name)) |arg_tys| { + if (self.l.comptimeIndexOf(ie.index)) |raw| { + if (raw >= 0) { + const i: usize = @intCast(raw); + if (i < arg_tys.len) return arg_tys[i]; + } + } + } + } + } + if (self.l.packArgNodeAt(&ie)) |arg_node| { + return self.l.inferExprType(arg_node); + } + const obj_ty = self.l.inferExprType(ie.object); + return self.l.getElementType(obj_ty); + }, + .slice_expr => |se| { + const obj_ty = self.l.inferExprType(se.object); + if (obj_ty == .string) return .string; + return self.l.module.types.sliceOf(self.l.getElementType(obj_ty)); + }, + .deref_expr => |de| { + const ptr_ty = self.l.inferExprType(de.operand); + if (!ptr_ty.isBuiltin()) { + const info = self.l.module.types.get(ptr_ty); + if (info == .pointer) return info.pointer.pointee; + } + return .unresolved; + }, + .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.l.inferExprType(nc.lhs); + if (!lhs_ty.isBuiltin()) { + const info = self.l.module.types.get(lhs_ty); + if (info == .optional) break :blk info.optional.child; + } + break :blk self.l.inferExprType(nc.rhs); + }, + // Statements don't produce values (`.return_stmt` is handled above + // as `.noreturn` — it diverges rather than yielding `void`). + .assignment, .var_decl, .const_decl, .fn_decl, + .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, + => .void, + else => .unresolved, + }; + } +}; diff --git a/src/ir/ir.zig b/src/ir/ir.zig index e808b5b..9f0b855 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -7,6 +7,7 @@ pub const lower = @import("lower.zig"); pub const program_index = @import("program_index.zig"); pub const type_resolver = @import("type_resolver.zig"); pub const packs = @import("packs.zig"); +pub const expr_typer = @import("expr_typer.zig"); pub const semantic_diagnostics = @import("semantic_diagnostics.zig"); pub const TypeId = types.TypeId; @@ -38,6 +39,7 @@ pub const ProgramIndex = program_index.ProgramIndex; pub const TypeResolver = type_resolver.TypeResolver; pub const ResolveEnv = type_resolver.ResolveEnv; pub const PackResolver = packs.PackResolver; +pub const ExprTyper = expr_typer.ExprTyper; pub const compiler_hooks = @import("compiler_hooks.zig"); pub const emit_llvm = @import("emit_llvm.zig"); @@ -59,6 +61,7 @@ pub const lower_tests = @import("lower.test.zig"); pub const program_index_tests = @import("program_index.test.zig"); pub const type_resolver_tests = @import("type_resolver.test.zig"); pub const packs_tests = @import("packs.test.zig"); +pub const expr_typer_tests = @import("expr_typer.test.zig"); pub const type_bridge_tests = @import("type_bridge.test.zig"); pub const emit_llvm_tests = @import("emit_llvm.test.zig"); pub const jni_descriptor_tests = @import("jni_descriptor.test.zig"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 9e93991..191d393 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -22,6 +22,7 @@ const ModuleConstInfo = program_index_mod.ModuleConstInfo; const TypeResolver = @import("type_resolver.zig").TypeResolver; const ResolveEnv = @import("type_resolver.zig").ResolveEnv; const PackResolver = @import("packs.zig").PackResolver; +const ExprTyper = @import("expr_typer.zig").ExprTyper; const semantic_diagnostics = @import("semantic_diagnostics.zig"); const TypeId = types.TypeId; @@ -75,7 +76,7 @@ const Scope = struct { self.map.put(name, binding) catch unreachable; } - fn lookup(self: *const Scope, name: []const u8) ?Binding { + pub fn lookup(self: *const Scope, name: []const u8) ?Binding { if (self.map.get(name)) |b| return b; if (self.parent) |p| return p.lookup(name); return null; @@ -4895,7 +4896,7 @@ pub const Lowering = struct { } /// Get the field list for a struct TypeId, or empty if not a struct. - fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field { + pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field { if (ty.isBuiltin()) return &.{}; var resolved = ty; const info = self.module.types.get(resolved); @@ -5823,7 +5824,7 @@ pub const Lowering = struct { /// `[]` with the pack name bound /// in the active `pack_arg_nodes` map and the index in range. /// Otherwise null — caller falls back to standard slice indexing. - fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node { + pub fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node { const pan = self.pack_arg_nodes orelse return null; if (ie.object.data != .identifier) return null; const arg_nodes = pan.get(ie.object.data.identifier.name) orelse return null; @@ -5837,7 +5838,7 @@ pub const Lowering = struct { /// Resolve an index expression to a comptime-known integer: a literal, /// or an identifier bound to an `int_val` in `comptime_constants` (e.g. /// the cursor of an `inline for 0..N (i)` unroll). Otherwise null. - fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 { + pub fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 { switch (index.data) { .int_literal => |lit| return lit.value, .identifier => |id| { @@ -12451,7 +12452,7 @@ pub const Lowering = struct { return declared_ty; } - fn resolveType(self: *Lowering, type_ann: *const Node) TypeId { + pub fn resolveType(self: *Lowering, type_ann: *const Node) TypeId { return self.resolveTypeWithBindings(type_ann); } @@ -14448,492 +14449,203 @@ pub const Lowering = struct { /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { - .string_literal => .string, - .int_literal => .s64, - .float_literal => .f64, - .bool_literal => .bool, - .null_literal => .void, - .binary_op => |bop| switch (bop.op) { - .or_op => blk: { - // A failable `or` (value-terminator or chain) yields the - // chain's success type (the error is absorbed/propagated); - // a non-failable `or` is boolean / optional-unwrap → bool. - // Detected structurally — a `try`-chain's operands type as - // non-failable `T`, so a type-only check would miss it. - if (self.orIsFailableChain(&bop)) break :blk self.orChainSuccessType(&bop); - break :blk .bool; - }, - .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool, - else => self.inferExprType(bop.lhs), - }, - .unary_op => |uop| switch (uop.op) { - .not => .bool, - .negate => self.inferExprType(uop.operand), - .xx => self.target_type orelse .unresolved, - .address_of => blk: { - const inner = self.inferExprType(uop.operand); - break :blk self.module.types.ptrTo(inner); - }, - else => .unresolved, - }, - // `try X` evaluates to X's success type (the value part). A - // pure-failable operand (`-> !` / `-> !Named`, whose type IS the - // error set) has no value → `void`; a value-carrying `-> (T..., !)` - // operand yields its value part (the lone value, or a value-tuple). - .try_expr => |te| blk: { - const op_ty = self.inferExprType(te.operand); - const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; - if (op_ty == channel) break :blk .void; - break :blk self.failableSuccessType(op_ty); - }, - // `expr catch ...` strips the error channel → the success type - // (void for a pure-failable LHS; the value part for value-carrying). - .catch_expr => |ce| blk: { - const op_ty = self.inferExprType(ce.operand); - const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; - if (op_ty == channel) break :blk .void; - break :blk self.failableSuccessType(op_ty); - }, - .caller_location => self.module.types.findByName(self.module.types.internString("Source_Location")) orelse .unresolved, - .if_expr => |ie| { - // If-else types as its branches' unified type. A `noreturn` - // branch (one that diverges — `return` / `raise` / `break` / - // `continue`) unifies away, so the expression takes the other - // branch's type; both diverging → `noreturn` (ERR E1.4c). - if (ie.else_branch) |eb| { - const then_ty = self.inferExprType(ie.then_branch); - if (then_ty == .noreturn) return self.inferExprType(eb); - return then_ty; + .call => |*c| self.inferCallType(c), + else => self.exprTyper().inferType(node), + }; + } + + fn exprTyper(self: *Lowering) ExprTyper { + return .{ .l = self }; + } + + /// Infer the result type of a call expression. Call typing stays in + /// `Lowering` for now (A3.1); A3.2 converges it into `CallResolver`. The + /// structural / non-call shapes live in `ExprTyper` (`expr_typer.zig`). + fn inferCallType(self: *Lowering, c: *const ast.Call) TypeId { + if (c.callee.data == .identifier) { + const bare_name = c.callee.data.identifier.name; + // Resolve local function name (bare → mangled) and UFCS aliases + const name = blk: { + const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; + if (self.program_index.ufcs_alias_map.get(bare_name)) |target| { + break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } - return .void; - }, - // Divergence shapes type as `noreturn` — they transfer control and - // produce no value at their site. A block whose last statement is - // one of these propagates `noreturn` (block arm below), which lets - // a `catch` body that ends in `return` / `raise` unify with the - // success type (ERR E1.4c / E1.5). - .return_stmt, .raise_stmt, .break_expr, .continue_expr => .noreturn, - .block => |blk| { - // A block's type is its last expression's type only when it - // produces a value (no trailing `;`); otherwise it is void. - if (blk.produces_value and blk.stmts.len > 0) { - return self.inferExprType(blk.stmts[blk.stmts.len - 1]); + break :blk scoped; + }; + if (resolveBuiltin(bare_name)) |bid| { + return switch (bid) { + .sqrt, .sin, .cos, .floor => blk: { + if (c.args.len > 0) { + const arg_ty = self.inferExprType(c.args[0]); + if (arg_ty == .f32) break :blk TypeId.f32; + } + break :blk TypeId.f64; + }, + .size_of, .align_of => .s64, + .cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .unresolved, + else => .unresolved, + }; + } + // 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, "error_tag_name")) return .string; + if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool; + if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void; + if (std.mem.eql(u8, bare_name, "__trace_resolve_frame")) + return self.module.types.findByName(self.module.types.internString("Frame")) orelse .unresolved; + 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.program_index.fn_ast_map.get(name)) |fd| { + if (fd.type_params.len > 0) { + return self.inferGenericReturnType(fd, c); } + } + // Check declared functions for return type + 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.program_index.fn_ast_map.get(name)) |fd| { + if (fd.return_type) |rt| return self.resolveType(rt); return .void; - }, - .call => |c| { - if (c.callee.data == .identifier) { - const bare_name = c.callee.data.identifier.name; - // Resolve local function name (bare → mangled) and UFCS aliases - const name = blk: { - const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; - if (self.program_index.ufcs_alias_map.get(bare_name)) |target| { - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - if (resolveBuiltin(bare_name)) |bid| { - return switch (bid) { - .sqrt, .sin, .cos, .floor => blk: { - if (c.args.len > 0) { - const arg_ty = self.inferExprType(c.args[0]); - if (arg_ty == .f32) break :blk TypeId.f32; - } - break :blk TypeId.f64; - }, - .size_of, .align_of => .s64, - .cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .unresolved, - else => .unresolved, - }; + } + // Check if callee is a local closure / function-type variable + // (e.g. a `cb: Closure(...) -> R` or bare `cb: (T) -> R` + // parameter) — extract its declared return type so `try` / + // `catch` on the call see the (possibly failable) result. + if (self.scope) |scope| { + if (scope.lookup(bare_name)) |binding| { + if (!binding.ty.isBuiltin()) { + const ti = self.module.types.get(binding.ty); + if (ti == .closure) return ti.closure.ret; + if (ti == .function) return ti.function.ret; } - // 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, "error_tag_name")) return .string; - if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool; - if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void; - if (std.mem.eql(u8, bare_name, "__trace_resolve_frame")) - return self.module.types.findByName(self.module.types.internString("Frame")) orelse .unresolved; - 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.program_index.fn_ast_map.get(name)) |fd| { - if (fd.type_params.len > 0) { - return self.inferGenericReturnType(fd, &c); - } + } + } + } else if (c.callee.data == .field_access) { + const cfa = c.callee.data.field_access; + // Check if receiver is a protocol type → return protocol method type + const recv_ty = self.inferExprType(cfa.object); + { + if (self.getProtocolInfo(recv_ty)) |proto_info| { + for (proto_info.methods) |m| { + if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; } - // Check declared functions for return type - 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.program_index.fn_ast_map.get(name)) |fd| { - if (fd.return_type) |rt| return self.resolveType(rt); - return .void; - } - // Check if callee is a local closure / function-type variable - // (e.g. a `cb: Closure(...) -> R` or bare `cb: (T) -> R` - // parameter) — extract its declared return type so `try` / - // `catch` on the call see the (possibly failable) result. - if (self.scope) |scope| { - if (scope.lookup(bare_name)) |binding| { - if (!binding.ty.isBuiltin()) { - const ti = self.module.types.get(binding.ty); - if (ti == .closure) return ti.closure.ret; - if (ti == .function) return ti.function.ret; - } - } - } - } else if (c.callee.data == .field_access) { - const cfa = c.callee.data.field_access; - // Check if receiver is a protocol type → return protocol method type - const recv_ty = self.inferExprType(cfa.object); - { - if (self.getProtocolInfo(recv_ty)) |proto_info| { - for (proto_info.methods) |m| { - if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; - } - } - } - // Foreign-class instance method: look up the method's - // declared return type so chained calls (e.g. - // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. - { - var recv_inner = recv_ty; - if (!recv_inner.isBuiltin()) { - const ri = self.module.types.get(recv_inner); - if (ri == .pointer) recv_inner = ri.pointer.pointee; - } - if (!recv_inner.isBuiltin()) { - const inner_info = self.module.types.get(recv_inner); - if (inner_info == .@"struct") { - const sn = self.module.types.getString(inner_info.@"struct".name); - if (self.program_index.foreign_class_map.get(sn)) |fcd| { - for (fcd.members) |m| switch (m) { - .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { - return self.resolveForeignMethodReturnType(fcd, md); - }, - else => {}, - }; - } - } - } - } - // Instance method call: obj.method(args) → look up StructName.method - { - var obj_ty = recv_ty; - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer) obj_ty = oi.pointer.pointee; - } - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .@"struct") { - const struct_name = self.module.types.getString(oi.@"struct".name); - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; - // Generic #compiler method dispatch — return type from declaration - if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { - if (method_fd.body.data == .compiler_expr) { - if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map); - return .void; - } - } - if (self.resolveFuncByName(qualified)) |fid| { - return self.module.functions.items[@intFromEnum(fid)].ret; - } - } - } - } - // Type.variant(args) — qualified enum construction - const type_name = switch (cfa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => null, - }; - if (type_name) |tn| { - // Foreign-class static method: `Alias.static_method(args)`. - if (self.program_index.foreign_class_map.get(tn)) |fcd| { + } + } + // Foreign-class instance method: look up the method's + // declared return type so chained calls (e.g. + // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. + { + var recv_inner = recv_ty; + if (!recv_inner.isBuiltin()) { + const ri = self.module.types.get(recv_inner); + if (ri == .pointer) recv_inner = ri.pointer.pointee; + } + if (!recv_inner.isBuiltin()) { + const inner_info = self.module.types.get(recv_inner); + if (inner_info == .@"struct") { + const sn = self.module.types.getString(inner_info.@"struct".name); + if (self.program_index.foreign_class_map.get(sn)) |fcd| { for (fcd.members) |m| switch (m) { - .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { return self.resolveForeignMethodReturnType(fcd, md); }, else => {}, }; } - const type_name_id = self.module.types.internString(tn); - if (self.module.types.findByName(type_name_id)) |ty| { - const ti = self.module.types.get(ty); - if (ti == .tagged_union or ti == .@"enum") return ty; + } + } + } + // Instance method call: obj.method(args) → look up StructName.method + { + var obj_ty = recv_ty; + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .pointer) obj_ty = oi.pointer.pointee; + } + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .@"struct") { + const struct_name = self.module.types.getString(oi.@"struct".name); + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; + // Generic #compiler method dispatch — return type from declaration + if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { + if (method_fd.body.data == .compiler_expr) { + if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map); + return .void; + } } - // Check for qualified function call. `resolveFuncByName` - // only finds ALREADY-LOWERED functions; namespace - // imports are typically lowered lazily on demand, so - // a fresh `pkg.hello()` call site may resolve through - // `fn_ast_map` first. Without this, the call's return - // type silently falls through to `.s64` and any - // pack-fn caller (e.g. `print("{}\n", pkg.hello())`) - // mangles the arg as s64, mis-tagging the actual - // string in the Any box. - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } - if (self.program_index.fn_ast_map.get(qualified)) |qfd| { - if (qfd.return_type) |rt| return self.resolveType(rt); - return .void; - } - // Namespace aliases sometimes register the function - // under its bare name (matches `lowerCall`'s effective- - // name resolution order). - if (self.program_index.fn_ast_map.get(cfa.field)) |bfd| { - if (bfd.return_type) |rt| return self.resolveType(rt); - return .void; - } - } - } else if (c.callee.data == .enum_literal) { - // .Variant(args) — dot-shorthand enum construction - return self.target_type orelse .unresolved; - } - return .unresolved; - }, - .field_access => |fa| { - // Pack-arity intercept: `.len` is s64. Mirrors - // the lowerFieldAccess intercept so AST-level type - // inference picks the same shape. - if (self.pack_param_count) |ppc| { - if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { - 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))) { - return self.module.types.ptrTo(.void); - } - } - // M2.2 — `obj.field` for an Obj-C `#property` field returns the field's type. - if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { - return self.resolveType(prop.field_type); - } - // M1.2 A.3 — sx-defined class state field returns the field's type. - if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { - return info.field_ty; - } - - var obj_ty = self.inferExprType(fa.object); - // Auto-deref: if object is a pointer, resolve through it (matches lowerFieldAccess behavior) - if (!obj_ty.isBuiltin()) { - const ptr_info = self.module.types.get(obj_ty); - if (ptr_info == .pointer) { - obj_ty = ptr_info.pointer.pointee; - } - } - // Optional chaining: ?T.field → ?FieldType (flattened if field is already optional) - const is_opt_chain = fa.is_optional; - if (is_opt_chain and !obj_ty.isBuiltin()) { - const opt_info = self.module.types.get(obj_ty); - if (opt_info == .optional) { - obj_ty = opt_info.optional.child; - } - } - if (std.mem.eql(u8, fa.field, "len")) return if (is_opt_chain) self.module.types.optionalOf(.s64) else .s64; - if (std.mem.eql(u8, fa.field, "ptr")) { - // .ptr on slice/string → [*]element_type - const elem_ty = self.getElementType(obj_ty); - const mp_ty = self.module.types.manyPtrTo(elem_ty); - return if (is_opt_chain) self.module.types.optionalOf(mp_ty) else mp_ty; - } - if (!obj_ty.isBuiltin()) { - const field_name_id = self.module.types.internString(fa.field); - // Check union fields (tagged enum payloads) + promoted struct fields - const info = self.module.types.get(obj_ty); - const u_fields2: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { - .@"union" => |u| u.fields, - .tagged_union => |u| u.fields, - else => null, + } + // Type.variant(args) — qualified enum construction + const type_name = switch (cfa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => null, + }; + if (type_name) |tn| { + // Foreign-class static method: `Alias.static_method(args)`. + if (self.program_index.foreign_class_map.get(tn)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + return self.resolveForeignMethodReturnType(fcd, md); + }, + else => {}, }; - if (u_fields2) |ufields| { - for (ufields) |f| { - if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty; - // Check promoted fields from anonymous struct variants - if (!f.ty.isBuiltin()) { - const fi = self.module.types.get(f.ty); - if (fi == .@"struct") { - for (fi.@"struct".fields) |sf| { - if (sf.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(sf.ty) else sf.ty; - } - } - } - } - } - // Check vector element access (.x/.y/.z/.w) - if (info == .vector) { - const elem = info.vector.element; - return if (is_opt_chain) self.optionalOfFlattened(elem) else elem; - } - // Tuple field access: numeric `t.0` or named `t.x`. - if (info == .tuple) { - const tup = info.tuple; - if (std.fmt.parseInt(usize, fa.field, 10)) |idx| { - if (idx < tup.fields.len) - return if (is_opt_chain) self.optionalOfFlattened(tup.fields[idx]) else tup.fields[idx]; - } else |_| {} - if (tup.names) |names| { - for (names, 0..) |nm, i| { - if (nm == field_name_id and i < tup.fields.len) - return if (is_opt_chain) self.optionalOfFlattened(tup.fields[i]) else tup.fields[i]; - } - } - } - // Check struct fields - const fields = self.getStructFields(obj_ty); - for (fields) |f| { - if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty; - } } - return .unresolved; - }, - .identifier => |id| { - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - return binding.ty; - } + const type_name_id = self.module.types.internString(tn); + if (self.module.types.findByName(type_name_id)) |ty| { + const ti = self.module.types.get(ty); + if (ti == .tagged_union or ti == .@"enum") return ty; } - // `context` is the implicit-ctx identifier; type is Context - // when the program has registered it (i.e. std.sx imported). - if (self.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) { - if (self.module.types.findByName(self.module.types.internString("Context"))) |ty| return ty; + // Check for qualified function call. `resolveFuncByName` + // only finds ALREADY-LOWERED functions; namespace + // imports are typically lowered lazily on demand, so + // a fresh `pkg.hello()` call site may resolve through + // `fn_ast_map` first. Without this, the call's return + // type silently falls through to `.s64` and any + // pack-fn caller (e.g. `print("{}\n", pkg.hello())`) + // mangles the arg as s64, mis-tagging the actual + // string in the Any box. + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; + if (self.resolveFuncByName(qualified)) |fid| { + return self.module.functions.items[@intFromEnum(fid)].ret; } - // Check global variables (e.g., `context : Context`) - if (self.program_index.global_names.get(id.name)) |gi| { - return gi.ty; + if (self.program_index.fn_ast_map.get(qualified)) |qfd| { + if (qfd.return_type) |rt| return self.resolveType(rt); + return .void; } - // Check module-level value constants (e.g., WIDTH :f32: 800) - if (self.program_index.module_const_map.get(id.name)) |ci| { - return ci.ty; + // Namespace aliases sometimes register the function + // under its bare name (matches `lowerCall`'s effective- + // name resolution order). + if (self.program_index.fn_ast_map.get(cfa.field)) |bfd| { + if (bfd.return_type) |rt| return self.resolveType(rt); + return .void; } - // A bare type name (alias like `Vec4`, struct name, or - // builtin primitive) referenced in expression position - // is a Type value — IR type `.any`. - if (self.isKnownTypeName(id.name)) return .any; - return .unresolved; - }, - .type_expr => |te| { - // type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type) - if (self.scope) |scope| { - if (scope.lookup(te.name)) |binding| { - return binding.ty; - } - } - // 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 .unresolved; - }, - .enum_literal => { - // Enum literals depend on context — use target_type if available - return self.target_type orelse .unresolved; - }, - .struct_literal => |sl| { - if (sl.struct_name) |name| { - const name_id = self.module.types.internString(name); - return self.module.types.findByName(name_id) orelse - self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); - } - return self.target_type orelse .unresolved; - }, - .tuple_literal => |tl| { - var field_types = std.ArrayList(TypeId).empty; - defer field_types.deinit(self.alloc); - for (tl.elements) |elem| { - field_types.append(self.alloc, self.inferExprType(elem.value)) catch unreachable; - } - return self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, field_types.items) catch unreachable, - .names = null, - } }); - }, - .index_expr => |ie| { - // Pack-arg type lookup: `[]`. - // Read directly from `pack_arg_types` — bypasses the - // synthesized-ident detour in `pack_arg_nodes` which - // would otherwise lose the type when the mono's - // scope isn't set up yet (generic-`$R` pre-inference). - if (self.pack_arg_types) |pat| { - if (ie.object.data == .identifier) { - if (pat.get(ie.object.data.identifier.name)) |arg_tys| { - if (self.comptimeIndexOf(ie.index)) |raw| { - if (raw >= 0) { - const i: usize = @intCast(raw); - if (i < arg_tys.len) return arg_tys[i]; - } - } - } - } - } - if (self.packArgNodeAt(&ie)) |arg_node| { - return self.inferExprType(arg_node); - } - const obj_ty = self.inferExprType(ie.object); - return self.getElementType(obj_ty); - }, - .slice_expr => |se| { - const obj_ty = self.inferExprType(se.object); - if (obj_ty == .string) return .string; - return self.module.types.sliceOf(self.getElementType(obj_ty)); - }, - .deref_expr => |de| { - const ptr_ty = self.inferExprType(de.operand); - if (!ptr_ty.isBuiltin()) { - const info = self.module.types.get(ptr_ty); - if (info == .pointer) return info.pointer.pointee; - } - return .unresolved; - }, - .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 (`.return_stmt` is handled above - // as `.noreturn` — it diverges rather than yielding `void`). - .assignment, .var_decl, .const_decl, .fn_decl, - .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, - => .void, - else => .unresolved, - }; + } + } else if (c.callee.data == .enum_literal) { + // .Variant(args) — dot-shorthand enum construction + return self.target_type orelse .unresolved; + } + return .unresolved; } /// Infer the return type of a generic function call by resolving type bindings. @@ -15572,7 +15284,7 @@ pub const Lowering = struct { } /// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U) - fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId { + pub fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId { if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) return ty; @@ -15631,7 +15343,7 @@ pub const Lowering = struct { /// Check if a name refers to a known type (primitive or registered struct/enum/union). /// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names. - fn isKnownTypeName(self: *Lowering, name: []const u8) bool { + pub fn isKnownTypeName(self: *Lowering, name: []const u8) bool { if (type_bridge.resolveTypePrimitive(name) != null) return true; if (self.type_bindings) |bindings| { if (bindings.get(name) != null) return true; @@ -15844,7 +15556,7 @@ pub const Lowering = struct { /// Get the element type for a slice/array/string type. A non-collection /// type has no element type — return `.unresolved` (asking for it is a bug) /// rather than a plausible `.s64`. - fn getElementType(self: *Lowering, ty: TypeId) TypeId { + pub fn getElementType(self: *Lowering, ty: TypeId) TypeId { if (ty == .string) return .u8; if (ty.isBuiltin()) return .unresolved; const info = self.module.types.get(ty); @@ -15987,7 +15699,7 @@ pub const Lowering = struct { /// If `ret_ty` belongs to a failable function, the TypeId of its error /// channel; else null. `-> !Named` / `-> !` resolve the error set directly; /// `-> (T..., !)` carries it as the last tuple field (the locked ABI). - fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId { + pub fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId { if (ret_ty.isBuiltin()) return null; switch (self.module.types.get(ret_ty)) { .error_set => return ret_ty, @@ -16164,7 +15876,7 @@ pub const Lowering = struct { /// failable, or a synthesized value-tuple `(T1, ..., Tn)` (error slot /// dropped) for a multi-value one. Callers must pass a value-carrying /// tuple — a pure `-> !`'s success type is `void`, handled separately. - fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { + pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { const fields = self.module.types.get(op_ty).tuple.fields; const n_vals = fields.len - 1; if (n_vals == 1) return fields[0]; @@ -16604,7 +16316,7 @@ pub const Lowering = struct { /// itself a nested failable `or` chain. Kept separate from `inferExprType`: /// a `try`-chain's *value* type is its success type `T` (non-failable), so /// the chain-ness is structural, not type-derived. - fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool { + pub fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool { return self.operandIsFailableLike(bop.lhs) or self.operandIsFailableLike(bop.rhs); } @@ -16619,7 +16331,7 @@ pub const Lowering = struct { /// The success (value) type of a failable `or` chain: descend to the /// leftmost operand, unwrap any `try`, and take its failable success type /// (`void` for a pure-`-> !` chain). All operands share this type. - fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId { + pub fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId { var lhs = bop.lhs; while (lhs.data == .binary_op and lhs.data.binary_op.op == .or_op and self.orIsFailableChain(&lhs.data.binary_op)) @@ -17323,7 +17035,7 @@ pub const Lowering = struct { /// in `foreign_class_map` under an Obj-C runtime. Used by the /// `obj.class` accessor (M1.3) to decide whether to lower the /// field access as a struct GEP or as `object_getClass(obj)`. - fn isObjcClassPointer(self: *Lowering, ty: TypeId) bool { + pub fn isObjcClassPointer(self: *Lowering, ty: TypeId) bool { if (ty.isBuiltin()) return false; const ptr_info = self.module.types.get(ty); if (ptr_info != .pointer) return false; @@ -17338,7 +17050,7 @@ pub const Lowering = struct { /// and that class (or any of its `#extends` ancestors) declares a /// `#property` field with the given name, return the /// `ForeignFieldDecl`. M2.2 + M2.3. - fn lookupObjcPropertyOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ast.ForeignFieldDecl { + pub fn lookupObjcPropertyOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ast.ForeignFieldDecl { const obj_ty = self.inferExprType(obj_expr); if (obj_ty.isBuiltin()) return null; const ptr_info = self.module.types.get(obj_ty); @@ -17409,7 +17121,7 @@ pub const Lowering = struct { /// and `field_name` is in the state struct (not a property), /// returns the field's TypeId, the state struct's TypeId, and /// the field's index. M1.2 A.3 supports. - fn lookupObjcDefinedStateFieldOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ObjcDefinedStateField { + pub fn lookupObjcDefinedStateFieldOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ObjcDefinedStateField { const obj_ty = self.inferExprType(obj_expr); if (obj_ty.isBuiltin()) return null; const ptr_info = self.module.types.get(obj_ty);