refactor(ir): extract ExprTyper for non-call expression typing (A3.1)

Move the structural / non-call arms of Lowering.inferExprType into a new
src/ir/expr_typer.zig (ExprTyper): literals, unary/binary ops, try/catch, if,
block, field access, identifier/type-name, struct/tuple literals,
index/slice/deref, null-coalesce, caller_location, and the no-value statement
shapes. ExprTyper is a *Lowering facade (Principle 5, same as PackResolver) —
expression typing reads live lexical-scope / pack / target-type state and ~14
resolver helpers, so it borrows *Lowering rather than re-threading every field;
the plan's TypeResolver/ProgramIndex/ResolveEnv ideal is the later-phase target
as that state lifts into an explicit context (documented in the module doc).

Lowering.inferExprType is now a 2-arm dispatcher: `.call => inferCallType(c)`
(call result typing stays in Lowering until A3.2), else delegates to
ExprTyper.inferType. The call arm body moved verbatim into the new
Lowering.inferCallType (the by-value `|c|` capture became a `*const ast.Call`
param; the lone `&c` -> `c`).

14 Lowering helper methods consumed by the facade were widened to pub
(orIsFailableChain, orChainSuccessType, errorChannelOf, failableSuccessType,
isObjcClassPointer, lookupObjcPropertyOnPointer,
lookupObjcDefinedStateFieldOnPointer, getElementType, optionalOfFlattened,
getStructFields, isKnownTypeName, comptimeIndexOf, packArgNodeAt, resolveType)
plus Scope.lookup — the same pub-for-facade step PackResolver took. Fields need
no change (Zig fields are always cross-file accessible).

expr_typer.test.zig adds focused unit tests (literal shapes, comparison vs
arithmetic, unary not/negate, deref of non-pointer) for the scope-free
structural arms. Barrel-wired in ir.zig.

Behavior-preserving. Gate: zig build, zig build test (incl. new ExprTyper
tests), bash tests/run_examples.sh -> 356/0. lower.zig ~18774 -> 18598.
This commit is contained in:
agra
2026-06-02 18:14:34 +03:00
parent b72d49073e
commit 7d069107c8
4 changed files with 604 additions and 478 deletions

View File

@@ -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(&not_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));
}

336
src/ir/expr_typer.zig Normal file
View File

@@ -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: `<pack_name>.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: `<pack_name>[<int_literal>]`.
// 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,
};
}
};

View File

@@ -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");

View File

@@ -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 {
/// `<pack_name>[<comptime_int_literal>]` 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: `<pack_name>.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: `<pack_name>[<int_literal>]`.
// 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);