From 0592c9dc970dea1449b405ddec7fb7fb2af600c0 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 14:38:11 +0300 Subject: [PATCH] refactor(B8.1): move expression literals/field/index to lower/expr.zig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verbatim relocation of the 30-method expression cluster (struct/array/ tuple/enum/tagged-enum literals, init blocks, field access on values and types, optional chains, numeric limits, indexing, slicing, deref, force unwrap, null coalesce) into src/ir/lower/expr.zig — one contiguous 1,372-line cut. 30 aliases on Lowering keep all call sites unchanged. Nested StructConstInfo stays on Lowering (field type of struct_const_map), flipped pub and reached via an alias const, alongside headNameOfCallee. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 1408 +--------------------------------------- src/ir/lower/expr.zig | 1424 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1458 insertions(+), 1374 deletions(-) create mode 100644 src/ir/lower/expr.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2c6179a..503c5b8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -46,6 +46,7 @@ const lower_objc_class = @import("lower/objc_class.zig"); const lower_call = @import("lower/call.zig"); const lower_pack = @import("lower/pack.zig"); const lower_generic = @import("lower/generic.zig"); +const lower_expr = @import("lower/expr.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -370,7 +371,7 @@ pub const Lowering = struct { enum_tag: struct { ty: TypeId, tag: u32 }, }; - const StructConstInfo = struct { + pub const StructConstInfo = struct { value: *const Node, ty: ?TypeId, // null if no type annotation (inferred) }; @@ -1420,1379 +1421,6 @@ pub const Lowering = struct { // ── Control flow ──────────────────────────────────────────────── - fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { - // Check for tagged enum construction: .Variant.{ payload_fields } - // This happens when type_expr is an enum_literal and target_type is a union - if (sl.type_expr) |te| { - if (te.data == .enum_literal) { - const variant_name = te.data.enum_literal.name; - const union_ty = self.target_type orelse .unresolved; - if (!union_ty.isBuiltin()) { - const union_info = self.module.types.get(union_ty); - if (union_info == .tagged_union) { - return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); - } - } - } - } - - // `.{ name = ... }` against a tagged-union target_type. Reject: - // the only valid construction forms are `.variant(payload)` and - // `.variant.{ field, ... }`. Falling through would lower the - // user's values straight into the `(tag, payload_bytes)` slot - // pair and emit IR that LLVM later rejects. - if (sl.type_expr == null and sl.struct_name == null) { - const tu_ty = self.target_type orelse .unresolved; - if (!tu_ty.isBuiltin()) { - const tu_info = self.module.types.get(tu_ty); - if (tu_info == .tagged_union) { - if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { - const first_name = sl.field_inits[0].name.?; - if (self.diagnostics) |diags| { - const ty_name = self.formatTypeName(tu_ty); - if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { - diags.addFmt( - .err, - span, - "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", - .{ ty_name, first_name, first_name, first_name }, - ); - } else { - self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); - } - } - return self.builder.enumInit(0, Ref.none, tu_ty); - } - } - } - } - - const ty: TypeId = if (sl.struct_name) |name| - // Source-aware (E2): a bare struct-literal type name resolves to the - // querying source's OWN same-name author, not the global `findByName` - // first-match — so `Box.{...}` in module B builds B's `Box`, never a - // flat-imported A's. `.undeclared`/`.pending` keep the empty-struct - // stub (byte-identical to the legacy `findByName orelse intern`); - // `.ambiguous`/`.not_visible` surface their loud diagnostic + poison. - self.resolveNominalLeaf(name, false, span) - else if (sl.type_expr) |te| - // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr - self.resolveTypeWithBindings(te) - else self.target_type orelse .unresolved; - - // Get struct field types for coercion and ordering - const struct_fields = self.getStructFields(ty); - - // Look up field defaults from AST - const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: { - const ti = self.module.types.get(ty); - break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null); - } else @as(?[]const u8, null); - const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn| - (self.struct_defaults_map.get(sn) orelse &.{}) - else - &.{}; - - // Check if any field_init has a name (named literal) - const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; - - if (has_names and struct_fields.len > 0) { - // Named literal: reorder fields to match struct declaration order - // First, lower all field values in source order (to preserve evaluation order) - var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty; - defer lowered.deinit(self.alloc); - for (sl.field_inits) |fi| { - const saved_tt = self.target_type; - // Set target_type to the field's declared type so array literals - // know if the target is a vector, etc. - if (fi.name) |fname| { - for (struct_fields) |sf| { - if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { - self.target_type = sf.ty; - break; - } - } - } - const val = self.lowerExpr(fi.value); - self.target_type = saved_tt; - lowered.append(self.alloc, .{ - .val = val, - .name = fi.name orelse "", - .node = fi.value, - }) catch unreachable; - } - - // Build fields in declaration order - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - for (struct_fields, 0..) |sf, fi| { - const sf_name = self.module.types.getString(sf.name); - // Find the matching lowered value - var found = false; - for (lowered.items) |l| { - if (std.mem.eql(u8, l.name, sf_name)) { - var val = l.val; - const src_ty = self.builder.getRefType(val); - val = self.coerceToType(val, src_ty, sf.ty); - fields.append(self.alloc, val) catch unreachable; - found = true; - break; - } - } - if (!found) { - // Field not specified — use default if available, else zero - if (fi < field_defaults.len) { - if (field_defaults[fi]) |default_expr| { - // Coerce the default to the field type at the IR - // level (the implicit narrowing rule) so a float - // default folds/errors here instead of being - // silently bit-coerced by the backend. - fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; - } else { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } else { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - } - - const result = self.builder.structInit(fields.items, ty); - if (sl.init_block) |ib| { - return self.lowerInitBlock(result, ty, ib); - } - return result; - } - - // Positional literal: use source order - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - - for (sl.field_inits, 0..) |fi, i| { - var val = self.lowerExpr(fi.value); - // Coerce field value to match struct field type - if (i < struct_fields.len) { - const src_ty = self.inferExprType(fi.value); - val = self.coerceToType(val, src_ty, struct_fields[i].ty); - } - fields.append(self.alloc, val) catch unreachable; - } - - // Pad missing fields with defaults or zeroes - if (fields.items.len < struct_fields.len) { - for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { - if (fi < field_defaults.len) { - if (field_defaults[fi]) |default_expr| { - fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; - continue; - } - } - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - - const result = self.builder.structInit(fields.items, ty); - - // Lower init block if present - if (sl.init_block) |ib| { - return self.lowerInitBlock(result, ty, ib); - } - - return result; - } - - /// Lower an init block: store struct value to alloca, bind `self`, execute block, reload. - fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref { - // Store struct value to a temporary alloca - const ptr_ty = self.module.types.ptrTo(ty); - const slot = self.builder.alloca(ty); - self.builder.store(slot, struct_val); - - // Create a nested scope with `self` bound to the alloca pointer - var init_scope = Scope.init(self.alloc, self.scope); - defer init_scope.deinit(); - const saved_scope = self.scope; - self.scope = &init_scope; - - // `self` is the pointer to the struct (not an alloca itself — it IS the pointer value) - init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false }); - - // Lower the init block body - self.lowerBlock(ib); - - // Restore scope - self.scope = saved_scope; - - // Load and return the (possibly modified) struct value - return self.builder.load(slot, ty); - } - - /// Get the field list for a struct TypeId, or empty if not a struct. - 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); - // Dereference pointer types to get to the underlying struct - if (info == .pointer) { - resolved = info.pointer.pointee; - if (resolved.isBuiltin()) return &.{}; - const inner = self.module.types.get(resolved); - return switch (inner) { - .@"struct" => |s| s.fields, - else => &.{}, - }; - } - return switch (info) { - .@"struct" => |s| s.fields, - else => &.{}, - }; - } - - /// If a method's first param expects a pointer (*T) but we're passing T by value, - /// swap the first arg with the alloca address (implicit address-of). - pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { - // Skip the implicit __sx_ctx param when inspecting the receiver slot. - const skip: usize = if (func.has_implicit_ctx) 1 else 0; - if (func.params.len <= skip) return; - const first_param_ty = func.params[skip].ty; - // Check if first param expects a pointer - if (!first_param_ty.isBuiltin()) { - const pi = self.module.types.get(first_param_ty); - if (pi == .pointer) { - // If obj is already a pointer type, it's already correct (no addr_of needed) - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer) return; // already a pointer - } - // Method expects *T — pass the address of the receiver (value type in alloca) - if (obj_node.data == .identifier) { - if (self.scope) |scope| { - if (scope.lookup(obj_node.data.identifier.name)) |binding| { - if (binding.is_alloca) { - const ptr_ty = self.module.types.ptrTo(binding.ty); - method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); - return; - } - } - } - } - // Field access: obj.field.method() → GEP to field, pass pointer directly. - // This avoids copying the struct value (mutations through *T must be visible). - if (obj_node.data == .field_access) { - const gep_ref = self.lowerExprAsPtr(obj_node); - // GEP returns a pointer in LLVM but its IR type is the field value type. - // Wrap with addr_of (no-op in LLVM) to set the IR type to *T, - // preventing coerceCallArgs from doing a spurious alloca+store. - const ptr_ty = self.module.types.ptrTo(obj_ty); - method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty); - return; - } - // General case: alloca+store the value and pass the alloca pointer - { - const slot = self.builder.alloca(obj_ty); - self.builder.store(slot, method_args.items[0]); - method_args.items[0] = slot; - } - } else { - // Method expects a value `T` but the receiver is a `*T` (e.g. a - // `for xs: (*x)` by-ref capture) — deref to pass the value. - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer and oi.pointer.pointee == first_param_ty) { - method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty); - } - } - } - } - } - - /// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. - pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { - if (ty.isBuiltin()) { - // Map builtin types to their names for method resolution (e.g., s64.eq) - return builtinTypeName(ty); - } - var resolved = ty; - const info = self.module.types.get(resolved); - if (info == .pointer) { - resolved = info.pointer.pointee; - if (resolved.isBuiltin()) return builtinTypeName(resolved); - } - const ri = self.module.types.get(resolved); - return switch (ri) { - .@"struct" => |s| self.module.types.getString(s.name), - else => null, - }; - } - - fn builtinTypeName(ty: TypeId) ?[]const u8 { - return switch (ty) { - .s8 => "s8", - .s16 => "s16", - .s32 => "s32", - .s64 => "s64", - .u8 => "u8", - .u16 => "u16", - .u32 => "u32", - .u64 => "u64", - .f32 => "f32", - .f64 => "f64", - .bool => "bool", - .string => "string", - else => null, - }; - } - - /// Resolve the type of a named field on a given type. - fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { - if (std.mem.eql(u8, field, "len")) return .s64; - if (std.mem.eql(u8, field, "ptr")) { - const elem_ty = self.getElementType(ty); - return self.module.types.manyPtrTo(elem_ty); - } - const field_name_id = self.module.types.internString(field); - // Check union fields + promoted fields - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { - .@"union" => |u| u.fields, - .tagged_union => |u| u.fields, - else => null, - }; - if (u_fields) |ufields| { - for (ufields) |f| { - if (f.name == field_name_id) return 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 sf.ty; - } - } - } - } - } - } - // Check tuple fields - if (!ty.isBuiltin()) { - const ti = self.module.types.get(ty); - if (ti == .tuple) { - const tuple = ti.tuple; - // Try named fields - if (tuple.names) |names| { - for (names, 0..) |name_id, i| { - if (name_id == field_name_id) return tuple.fields[i]; - } - } - // Try numeric index - const idx = std.fmt.parseInt(usize, field, 10) catch { - return .unresolved; - }; - if (idx < tuple.fields.len) return tuple.fields[idx]; - return .unresolved; - } - } - const struct_fields = self.getStructFields(ty); - for (struct_fields) |f| { - if (f.name == field_name_id) return f.ty; - } - return .unresolved; - } - - fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { - // `error.X` — an error-tag literal. The `error` keyword in expression - // position parses as identifier "error" (E0.2), so `error.X` is a - // field access we intercept here. `error` is reserved, so this is - // unambiguous (no struct/pack can be named `error`). - if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) { - return self.lowerErrorTagLiteral(fa.field, span); - } - - // Pack-arity intercept: `.len` in a pack-fn mono's - // body resolves to the comptime-known N. The mono doesn't - // materialise the `[]Any` slice that the inline path used, so - // `args` isn't in scope as a value. - if (self.pack_param_count) |ppc| { - if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { - if (ppc.get(fa.object.data.identifier.name)) |n| { - return self.builder.constInt(@as(i64, @intCast(n)), .s64); - } - } - } - - // Pack value projection: `xs.` where `` is a (zero-arg) method of - // the pack's constraint protocol projects it over every element → - // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) - if (self.pack_constraint) |pcon| { - if (fa.object.data == .identifier) { - if (pcon.get(fa.object.data.identifier.name)) |proto| { - if (self.lookupProtocolField(proto, fa.field) != null) { - return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); - } - } - } - } - - // Interface-only enforcement (Decision): a member access on a - // constrained pack element `xs[i].` may only name a method of the - // constraint protocol — not an arbitrary concrete field. Checked here, - // on the `xs[i]` (index_expr) base, BEFORE substitution erases the - // "constrained to P" context. Protocol method CALLS go through the call - // path; a method name passes this check (it's in the protocol). - if (self.pack_constraint) |pcon| { - if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { - const base_name = fa.object.data.index_expr.object.data.identifier.name; - if (pcon.get(base_name)) |proto| { - if (self.lookupProtocolField(proto, fa.field) == null) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); - } - return self.builder.constInt(0, .void); - } - } - } - } - - // Check for struct constant access: Struct.CONST - if (fa.object.data == .identifier) { - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; - if (self.struct_const_map.get(qualified)) |info| { - return self.lowerStructConstant(info); - } - } - - // Numeric-limit accessor: `.min` / `.max` folds to a comptime - // const of the queried type (sibling of the identifier-receiver - // intercepts above). Placed AFTER `Struct.CONST` so a user const named - // `min`/`max` wins on its own struct; a builtin type name can never - // name a user struct (reserved — issue 0076), so they never collide. - if (self.lowerNumericLimit(fa, span)) |ref| return ref; - - // M1.3 — `obj.class` on any Obj-C-class pointer lowers to - // `object_getClass(obj)`. Sugar; the receiver is opaque so - // we don't auto-deref. Returns `Class` (alias for *void; - // typed Class(T) parameterization is M1.1.b). - if (std.mem.eql(u8, fa.field, "class")) { - const expr_ty = self.inferExprType(fa.object); - if (self.objc().isObjcClassPointer(expr_ty)) { - const obj_ref = self.lowerExpr(fa.object); - const ptr_void = self.module.types.ptrTo(.void); - const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); - const args = self.alloc.alloc(Ref, 1) catch unreachable; - args[0] = obj_ref; - return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); - } - } - - // M2.2 — `obj.field` where `field` is declared with `#property` - // on a foreign Obj-C class lowers as `[obj field]` (the synthesized - // getter). Receiver stays opaque — no auto-deref. - if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { - return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span); - } - - // M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class - // pointer for a plain instance field (NOT a #property) lowers as - // `object_getIvar(obj, load(___state_ivar))` + struct_gep on - // the state struct + load. The receiver is the opaque Obj-C id - // (matching Apple's `self` semantics); the state lives in the - // hidden `__sx_state` ivar. - if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { - return self.lowerObjcDefinedStateFieldRead(fa.object, info); - } - - var obj = self.lowerExpr(fa.object); - var obj_ty = self.inferExprType(fa.object); - - // Auto-deref: if the object is a pointer to a struct, load through it - if (!obj_ty.isBuiltin()) { - const ptr_info = self.module.types.get(obj_ty); - if (ptr_info == .pointer) { - const pointee = ptr_info.pointer.pointee; - obj = self.builder.load(obj, pointee); - obj_ty = pointee; - } - } - - // Special fields on slices/strings (NOT structs with .len/.ptr fields) - if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) { - // Only use length/data_ptr for slice, string, array, vector types - const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { - const info = self.module.types.get(obj_ty); - break :blk info == .slice or info == .array or info == .vector; - } else false); - - if (is_special) { - if (std.mem.eql(u8, fa.field, "len")) { - return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); - } - { - const elem_ty = self.getElementType(obj_ty); - const mp_ty = self.module.types.manyPtrTo(elem_ty); - return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty); - } - } - } - - // Optional chaining: p?.field - if (fa.is_optional) { - return self.lowerOptionalChain(obj, fa, span); - } - - return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); - } - - /// True when an `.identifier` receiver text resolves to an in-scope VALUE - /// binding rather than a builtin type. A backtick raw identifier (F0.6) can - /// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``); - /// such a value is reachable through the same three sources the ordinary - /// identifier field-access path consults (see `expr_typer` `.identifier` - /// arm): lexical `scope`, program `global_names`, and module value - /// constants `module_const_map`. The numeric-limit intercept must defer to - /// ordinary field access whenever ANY of the three binds the name, so a - /// raw value field read is never hijacked into a numeric-limit fold - /// (issues 0092 local / 0093 global + module-const). A single helper used - /// by both lowering and inference keeps the two resolvers in lockstep - /// (issue-0083 two-resolver defect class). - pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool { - if (self.scope) |scope| { - if (scope.lookup(name) != null) return true; - } - if (self.program_index.global_names.get(name) != null) return true; - if (self.program_index.module_const_map.get(name) != null) return true; - return false; - } - - /// Numeric-limit accessor intercept (`.min`/`.max`/`.epsilon`/ - /// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` / - /// `Struct.CONST` / pack-arity identifier-receiver intercepts in - /// `lowerFieldAccess`. Folds the limit to a comptime const of the queried - /// type via the shared `TypeResolver` logic (no second computor) + the - /// existing `constInt` / `constFloat` const paths: - /// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`); - /// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/ - /// `.nan` → `constFloat` (via `floatLimitFor`). - /// Returns null when the field is not a limit accessor, or the receiver is not - /// a builtin type (a user struct → ordinary field lowering reports - /// field-not-found). Two clean diagnostics (then a placeholder, so lowering - /// finishes and `hasErrors()` aborts the build): - /// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`); - /// - any accessor on a builtin NON-numeric receiver - /// (`bool`/`string`/`void`/`Any`/`noreturn`). - fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref { - const name = switch (fa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => return null, - }; - if (!TypeResolver.isLimitField(fa.field)) return null; - const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null; - - // A backtick raw identifier (F0.6) can bind a value whose spelling - // shadows a builtin type name (`` `f64 := … ``). Field access on that - // value is an ordinary field read, not a numeric-limit fold — defer to - // the normal field-access path when the receiver identifier resolves to - // a value binding through any of scope / globals / module consts - // (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type - // and can never be value-shadowed. - if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null; - - if (TypeResolver.integerLimitFor(name, fa.field)) |value| { - return self.builder.constInt(value, ty); - } - if (TypeResolver.floatLimitFor(name, fa.field)) |value| { - return self.builder.constFloat(value, ty); - } - // The field is a limit accessor, but it does not apply to this type. - if (self.diagnostics) |d| { - if (TypeResolver.integerWidthSign(name) != null) { - // Integer receiver + a float-only accessor. - d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field }); - } else { - // Non-numeric builtin receiver (bool/string/void/Any/noreturn). - d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field }); - } - } - return self.emitPlaceholder(fa.field); - } - - /// Lower a struct-level constant value (e.g., Phys.GRAVITY). - fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { - const val_node = info.value; - return switch (val_node.data) { - .int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64), - .float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64), - .bool_literal => |lit| self.builder.constBool(lit.value), - .string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)), - else => self.lowerExpr(val_node), - }; - } - - /// Lower optional chaining: `p?.field` where p is ?T - /// Produces ?FieldType: some(unwrap(p).field) if p has value, else null - /// If FieldType is already optional (?U), flattens to ?U (no double wrapping) - fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { - const obj_ty = self.inferExprType(fa.object); - // Get the inner (non-optional) type - const inner_ty = if (!obj_ty.isBuiltin()) blk: { - const info = self.module.types.get(obj_ty); - break :blk if (info == .optional) info.optional.child else obj_ty; - } else obj_ty; - - // Get the field type on the inner type - const field_ty = self.resolveFieldType(inner_ty, fa.field); - // If field is already optional, flatten (don't double-wrap) - const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false; - const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty); - - // Check if optional has value - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool); - - // Create blocks - const some_bb = self.freshBlock("chain.some"); - const none_bb = self.freshBlock("chain.none"); - const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty}); - - self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); - - // Some: unwrap, access field (already ?FieldType if flattened, else wrap) - self.builder.switchToBlock(some_bb); - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); - const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); - const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); - self.builder.br(merge_bb, &.{some_result}); - - // None: produce null optional - self.builder.switchToBlock(none_bb); - const none_result = self.builder.constNull(result_ty); - self.builder.br(merge_bb, &.{none_result}); - - // Merge - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, result_ty); - } - - /// Field access on a known type (shared by regular field access and optional chaining) - /// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour - /// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any - /// other field name so the read path (`lowerFieldAccessOnType`) and the - /// write path (`lowerAssignment`) share one resolver and reject a - /// non-lane field identically (issue 0086). - pub fn vectorLaneIndex(field: []const u8) ?u32 { - if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0; - if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1; - if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2; - if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3; - return null; - } - - fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { - const field_name_id = self.module.types.internString(field); - - // Check if it's a union type - if (!obj_ty.isBuiltin()) { - const info = self.module.types.get(obj_ty); - switch (info) { - .tagged_union => |u| { - // .tag → extract the enum tag value with the correct tag type - if (std.mem.eql(u8, field, "tag")) { - return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); - } - // Tagged union — use enum_payload - for (u.fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); - } - } - // Check promoted fields from anonymous struct variants - for (u.fields) |f| { - if (!f.ty.isBuiltin()) { - const field_info = self.module.types.get(f.ty); - if (field_info == .@"struct") { - for (field_info.@"struct".fields, 0..) |sf, si| { - if (sf.name == field_name_id) { - const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); - return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); - } - } - } - } - } - }, - .@"union" => |u| { - // Untagged union — use union_get to reinterpret bytes - for (u.fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); - } - } - // Check promoted fields from anonymous struct variants - for (u.fields) |f| { - if (!f.ty.isBuiltin()) { - const field_info = self.module.types.get(f.ty); - if (field_info == .@"struct") { - for (field_info.@"struct".fields, 0..) |sf, si| { - if (sf.name == field_name_id) { - const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); - return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); - } - } - } - } - } - }, - else => {}, - } - } - - // Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) → - // lane 0/1/2/3. Shares lane-index resolution with the write path - // (lowerAssignment) via vectorLaneIndex; a non-lane field falls - // through to the field-not-found error below. - if (!obj_ty.isBuiltin()) { - const vinfo = self.module.types.get(obj_ty); - if (vinfo == .vector) { - if (Lowering.vectorLaneIndex(field)) |vidx| { - return self.builder.structGet(obj, vidx, vinfo.vector.element); - } - } - } - - // Closure field access: .fn_ptr → field 0, .env → field 1 - if (!obj_ty.isBuiltin()) { - const cinfo = self.module.types.get(obj_ty); - if (cinfo == .closure) { - if (std.mem.eql(u8, field, "fn_ptr")) { - const fn_ptr_ty = self.module.types.ptrTo(.void); - return self.builder.structGet(obj, 0, fn_ptr_ty); - } else if (std.mem.eql(u8, field, "env")) { - const env_ty = self.module.types.ptrTo(.void); - return self.builder.structGet(obj, 1, env_ty); - } - } - } - - // Tuple field access: .0, .1, etc. or named fields - if (!obj_ty.isBuiltin()) { - const tinfo = self.module.types.get(obj_ty); - if (tinfo == .tuple) { - const tuple = tinfo.tuple; - // Try named fields first - if (tuple.names) |names| { - for (names, 0..) |name_id, i| { - if (name_id == field_name_id) { - return self.builder.structGet(obj, @intCast(i), tuple.fields[i]); - } - } - } - // Try numeric index (e.g., "0", "1") - const idx = std.fmt.parseInt(u32, field, 10) catch { - return self.emitFieldError(obj_ty, field, span); - }; - if (idx < tuple.fields.len) { - return self.builder.structGet(obj, idx, tuple.fields[idx]); - } - return self.emitFieldError(obj_ty, field, span); - } - } - - // Resolve struct field index and type - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.structGet(obj, @intCast(i), f.ty); - } - } - - return self.emitFieldError(obj_ty, field, span); - } - - fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { - const target = self.target_type orelse .unresolved; - const tag = self.resolveVariantValue(target, el.name); - return self.builder.enumInit(tag, Ref.none, target); - } - - /// Lower an `error.X` tag literal to its global tag id (a `u32`). When the - /// destination context (`target_type`) is a named error set, the value is - /// typed as that set and `X`'s membership is validated; otherwise the value - /// is the raw `u32` global tag id (per the spec's context rule). - fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref { - const tag_id = self.module.types.internTag(tag_name); - if (self.target_type) |t| { - if (!t.isBuiltin()) { - const info = self.module.types.get(t); - if (info == .error_set) { - // The bare-`!` inferred placeholder (reserved name "!") accepts - // any tag — its members aren't known until the whole-program SCC - // pass (E1.4) folds in every raised tag. Skip membership for it. - if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) { - var in_set = false; - for (info.error_set.tags) |member| { - if (member == tag_id) { - in_set = true; - break; - } - } - if (!in_set) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) }); - } - } - } - return self.builder.constInt(@as(i64, @intCast(tag_id)), t); - } - } - } - return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32); - } - - /// Lower a tagged enum construction: .Variant.{ field_inits } - /// The struct literal provides the payload fields; we wrap them in an enum_init. - fn lowerTaggedEnumLiteral( - self: *Lowering, - sl: *const ast.StructLiteral, - variant_name: []const u8, - union_ty: TypeId, - union_info: types.TypeInfo.TaggedUnionInfo, - span: ast.Span, - ) Ref { - if (self.findTaggedVariant(union_info, variant_name) == null) { - self.emitBadVariant(union_ty, union_info, variant_name, span); - return self.builder.enumInit(0, Ref.none, union_ty); - } - - const tag = self.resolveVariantValue(union_ty, variant_name); - const name_id = self.module.types.internString(variant_name); - - // Find the payload type for this variant - var payload_ty: TypeId = .void; - for (union_info.fields) |f| { - if (f.name == name_id) { - payload_ty = f.ty; - break; - } - } - - if (payload_ty == .void or sl.field_inits.len == 0) { - // No payload or no fields — just tag - return self.builder.enumInit(tag, Ref.none, union_ty); - } - - // Lower the payload as a struct init of the payload type - const saved_tt = self.target_type; - self.target_type = payload_ty; - const payload_fields = self.getStructFields(payload_ty); - - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - - for (sl.field_inits, 0..) |fi, i| { - if (i < payload_fields.len) { - const saved_inner = self.target_type; - self.target_type = payload_fields[i].ty; - var val = self.lowerExpr(fi.value); - self.target_type = saved_inner; - const src_ty = self.inferExprType(fi.value); - val = self.coerceToType(val, src_ty, payload_fields[i].ty); - fields.append(self.alloc, val) catch unreachable; - } else { - fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable; - } - } - - // Pad missing payload fields with zeroes - if (fields.items.len < payload_fields.len) { - for (payload_fields[fields.items.len..]) |sf| { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - - const payload = self.builder.structInit(fields.items, payload_ty); - self.target_type = saved_tt; - - return self.builder.enumInit(tag, payload, union_ty); - } - - fn findTaggedVariant( - self: *Lowering, - union_info: types.TypeInfo.TaggedUnionInfo, - variant_name: []const u8, - ) ?usize { - const name_id = self.module.types.internString(variant_name); - for (union_info.fields, 0..) |f, i| { - if (f.name == name_id) return i; - } - return null; - } - - fn emitBadVariant( - self: *Lowering, - union_ty: TypeId, - union_info: types.TypeInfo.TaggedUnionInfo, - variant_name: []const u8, - span: ast.Span, - ) void { - const diags = self.diagnostics orelse return; - const ty_name = self.formatTypeName(union_ty); - var list: std.ArrayList(u8) = .empty; - for (union_info.fields, 0..) |f, i| { - if (i > 0) list.appendSlice(self.alloc, ", ") catch return; - list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; - } - diags.addFmt( - .err, - span, - "'{s}' is not a variant of '{s}' (variants are: {s})", - .{ variant_name, ty_name, list.items }, - ); - } - - /// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). - fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { - if (ty.isBuiltin()) return 0; - const info = self.module.types.get(ty); - const name_id = self.module.types.internString(variant_name); - switch (info) { - .@"enum" => |e| { - for (e.variants, 0..) |v, i| { - if (v == name_id) { - if (e.explicit_values) |vals| { - if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); - } - return @intCast(i); - } - } - }, - .tagged_union => |u| { - for (u.fields, 0..) |f, i| { - if (f.name == name_id) { - if (u.explicit_tag_values) |vals| { - if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); - } - return @intCast(i); - } - } - }, - else => {}, - } - return 0; - } - - /// Resolve a variant name to its tag index within an enum or union type. - pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { - if (ty.isBuiltin()) return 0; - const info = self.module.types.get(ty); - const name_id = self.module.types.internString(variant_name); - switch (info) { - .tagged_union => |u| { - for (u.fields, 0..) |f, i| { - if (f.name == name_id) return @intCast(i); - } - }, - .@"enum" => |e| { - for (e.variants, 0..) |v, i| { - if (v == name_id) return @intCast(i); - } - }, - else => {}, - } - return 0; - } - - fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref { - var elems = std.ArrayList(Ref).empty; - defer elems.deinit(self.alloc); - - // Determine element type: explicit type_expr > target_type > inference - var elem_ty: TypeId = .unresolved; - var from_target = false; - var is_vector = false; - - // First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3]) - if (al.type_expr) |te| { - const resolved = self.resolveArrayLiteralType(te); - if (resolved != .unresolved) { - if (!resolved.isBuiltin()) { - const info = self.module.types.get(resolved); - switch (info) { - .array => |a| { - elem_ty = a.element; - from_target = true; - }, - .vector => |v| { - elem_ty = v.element; - from_target = true; - is_vector = true; - }, - .slice => |s| { - elem_ty = s.element; - from_target = true; - }, - else => {}, - } - } - } - } - - if (!from_target) { - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const info = self.module.types.get(tt); - switch (info) { - .array => |a| { - elem_ty = a.element; - from_target = true; - }, - .slice => |s| { - elem_ty = s.element; - from_target = true; - }, - .vector => |v| { - elem_ty = v.element; - from_target = true; - is_vector = true; - }, - else => {}, - } - } - } - } - if (!from_target and al.elements.len > 0) { - const inferred = self.inferExprType(al.elements[0]); - if (inferred != .void) elem_ty = inferred; - } - - for (al.elements) |elem| { - const old_tt = self.target_type; - self.target_type = elem_ty; - var val = self.lowerExpr(elem); - self.target_type = old_tt; - // A nested `.[...]` element at a slice element type lowers to an - // aggregate array `[N]U` (lowerArrayLiteral always yields an array - // value); materialize it into a `[]U` slice so the element is a real - // {ptr,len} header rather than a raw array the callee would read its - // header off of (issue 0085). This per-element coercion recurses with - // the literal nesting, so `[][]T` and deeper coerce at every level. - if (!elem_ty.isBuiltin()) { - const ei = self.module.types.get(elem_ty); - if (ei == .slice) { - const val_ty = self.builder.getRefType(val); - if (!val_ty.isBuiltin()) { - const vi = self.module.types.get(val_ty); - if (vi == .array and vi.array.element == ei.slice.element) { - val = self.coerceToType(val, val_ty, elem_ty); - } - } - } - } - elems.append(self.alloc, val) catch unreachable; - } - - const result_ty = if (is_vector) - self.module.types.vectorOf(elem_ty, @intCast(al.elements.len)) - else - self.module.types.arrayOf(elem_ty, @intCast(al.elements.len)); - return self.builder.structInit(elems.items, result_ty); - } - - /// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]). - /// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr. - fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { - switch (te.data) { - .call => |cl| { - // Vector(3, f32) or Module.Vector(3, f32) - const callee_name = switch (cl.callee.data) { - .identifier => |id| id.name, - .field_access => |fa| fa.field, - else => return .unresolved, - }; - if (std.mem.eql(u8, callee_name, "Vector")) { - if (cl.args.len == 2) { - const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; - const elem = self.resolveTypeWithBindings(cl.args[1]); - return self.module.types.vectorOf(elem, length); - } - } - // Generic-struct typed-literal head (`Box(s64).[...]`): route - // through the single layout choke-point (CP-1). A qualified head - // `a.Box(s64).[...]` selects a's OWN template via the namespace edge - // (Counter-1: was the global last-wins map); a bare head selects the - // single bare-VISIBLE author. - if (headNameOfCallee(cl.callee)) |hn| { - switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { - .template => |t| return self.instantiateGenericStruct(&t, cl.args), - .poisoned => return .unresolved, - .not_generic => {}, - } - } - return .unresolved; - }, - .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), - .identifier => |id| { - // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type - // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is - // not bare-visible (consistent with annotations / 0763); ≥2 direct - // flat same-name authors are ambiguous (loud diagnostic, consistent - // with the leaf / 0755); a single source-keyed author resolves to - // ITS TypeId instead of a global `findByName` first-/last-wins pick. - switch (self.headTypeGate(id.name, te.span)) { - .ambiguous, .not_visible => return .unresolved, - .resolved => |tid| return tid, - .proceed => {}, - } - const name_id = self.module.types.internString(id.name); - return self.module.types.findByName(name_id) orelse .unresolved; - }, - .type_expr => |inner| { - if (self.headTypeLeak(inner.name, te.span)) return .unresolved; - return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - }, - .field_access => |fa| { - // Module.Type — try to resolve the field as a type name - const name_id = self.module.types.internString(fa.field); - return self.module.types.findByName(name_id) orelse .unresolved; - }, - else => return .unresolved, - } - } - - fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { - // Pack-arg substitution: `args[]` inside a body - // whose enclosing comptime call bound `args` as a pack name. - // Lowering the i-th call-site arg directly gives the concrete - // call-arg type — bypasses the `[]Any` slice boxing that would - // otherwise lose the type. Non-literal indices fall through to - // the standard slice indexing path. - if (self.packArgNodeAt(ie)) |arg_node| { - return self.lowerExpr(arg_node); - } - // Out-of-bounds pack indexing: object IS a pack name + index - // IS a comptime int literal but exceeds the pack arity. Emit - // a focused diagnostic so the user gets "pack index 2 out of - // bounds" instead of the generic "unresolved 'args'" that the - // fall-through scope-lookup would produce. - if (self.diagPackIndexOOB(ie)) { - return self.builder.constInt(0, .s64); - } - // Runtime index into a comptime-only pack (Decision 1): a pack has no - // runtime representation, so the index must be a compile-time constant. - // A runtime index is a hard error — clearer than the "unresolved - // ''" the slice-index fall-through would otherwise produce. - if (self.pack_param_count) |ppc| { - if (ie.object.data == .identifier) { - const pname = ie.object.data.identifier.name; - if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname}); - } - return self.builder.constInt(0, .s64); - } - } - } - const obj = self.lowerExpr(ie.object); - const idx = self.lowerExpr(ie.index); - // Infer element type from the object's slice/array type - const obj_ty = self.inferExprType(ie.object); - const elem_ty = self.getElementType(obj_ty); - return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); - } - - fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { - const obj = self.lowerExpr(se.object); - const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); - const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); - // Infer result slice type from the object - const obj_ty = self.inferExprType(se.object); - // Subslice of string stays string (same {ptr, i64} layout, correct type category) - if (obj_ty == .string) { - return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); - } - const elem_ty = self.getElementType(obj_ty); - const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); - return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); - } - - fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { - var elems = std.ArrayList(Ref).empty; - defer elems.deinit(self.alloc); - var field_type_ids = std.ArrayList(TypeId).empty; - defer field_type_ids.deinit(self.alloc); - var name_ids = std.ArrayList(types.StringId).empty; - defer name_ids.deinit(self.alloc); - var has_names = false; - - // A tuple_init's element values must match its field types exactly - // (LLVM `insertvalue` does no implicit conversion). When a contextual - // target tuple of matching arity is in scope (annotation, assignment - // LHS, call/return slot), its field types drive element lowering so an - // ambient scalar `target_type` (e.g. the enclosing fn's int return - // type) can't narrow an element below its field width. Otherwise each - // element's type is inferred independently. - // A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields, - // so element-count ≠ field-count and a contextual target tuple can't be - // aligned by index — infer field types from the expanded refs instead. - var has_spread = false; - for (tl.elements) |elem| { - if (elem.value.data == .spread_expr) has_spread = true; - } - - // Contextual target tuple field types. Without a spread we require - // exact arity (existing behavior); with a spread we index positionally - // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces - // / erases each spliced element to its slot's type). - var target_fields: ?[]const TypeId = null; - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const tinfo = self.module.types.get(tt); - if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) { - target_fields = tinfo.tuple.fields; - } - } - } - - const saved_target = self.target_type; - var out_idx: usize = 0; - for (tl.elements) |elem| { - // Pack-spread element → splice its per-element values as fields. - if (elem.value.data == .spread_expr) { - const sp_operand = elem.value.data.spread_expr.operand; - if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| { - defer self.alloc.free(refs); - // Element AST nodes (for protocol-erasure lvalue/name fallback) - // when the spread is a bare pack name. - const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null) - self.pack_arg_nodes.?.get(sp_operand.data.identifier.name) - else - null; - for (refs, 0..) |r, ri| { - var val = r; - var vty = self.builder.getRefType(r); - if (target_fields) |tf| { - if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) { - const want = tf[out_idx]; - const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value; - val = self.coerceOrErase(r, vty, want, node); - vty = want; - } - } - elems.append(self.alloc, val) catch unreachable; - field_type_ids.append(self.alloc, vty) catch unreachable; - name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; - out_idx += 1; - } - continue; - } - // Not a pack spread (e.g. tuple-value spread) — not yet handled. - _ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic - continue; - } - const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value); - self.target_type = field_ty; - var val = self.lowerExpr(elem.value); - self.target_type = saved_target; - const val_ty = self.builder.getRefType(val); - if (val_ty != field_ty and val_ty != .void) { - val = self.coerceToType(val, val_ty, field_ty); - } - elems.append(self.alloc, val) catch unreachable; - field_type_ids.append(self.alloc, field_ty) catch unreachable; - if (elem.name) |name| { - name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable; - has_names = true; - } else { - name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; - } - out_idx += 1; - } - - // Reuse the contextual target tuple type when it drove lowering so the - // value's type identity (incl. field names) matches the destination - // slot; otherwise build the tuple type from the inferred fields. - const tuple_ty = if (target_fields != null and self.target_type != null) - self.target_type.? - else - self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable, - .names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null, - } }); - - const owned = self.alloc.dupe(Ref, elems.items) catch unreachable; - return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); - } - - fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref { - const ptr = self.lowerExpr(de.operand); - // Resolve pointee type from the pointer type. - const ptr_ty = self.inferExprType(de.operand); - if (!ptr_ty.isBuiltin()) { - const info = self.module.types.get(ptr_ty); - if (info == .pointer) { - return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee); - } - } - // Operand isn't a pointer — `.*` is invalid. Diagnose here instead of - // emitting a `.deref` with an `.unresolved` result type, which would - // otherwise slip through to emit_llvm's "unresolved type reached LLVM - // emission" panic with no source location. - if (self.diagnostics) |d| { - d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)}); - } - return ptr; - } - - fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref { - const val = self.lowerExpr(fu.operand); - const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand)); - return self.builder.optionalUnwrap(val, inner_ty); - } - - fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { - const lhs = self.lowerExpr(nc.lhs); - const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); - - // Short-circuit: only evaluate RHS if LHS is null. - // IMPORTANT: optional_unwrap must be in the "has value" branch, - // not before the condBr — the interpreter errors on unwrapping null. - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); - - const then_bb = self.freshBlock("nc.has"); - const rhs_bb = self.freshBlock("nc.rhs"); - const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); - - // If has value, go to then_bb to unwrap; else go to rhs_bb - self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); - - // Then block: unwrap LHS and branch to merge - self.builder.switchToBlock(then_bb); - const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); - self.builder.br(merge_bb, &.{unwrapped}); - - // RHS block: evaluate fallback and branch to merge - self.builder.switchToBlock(rhs_bb); - var rhs = self.lowerExpr(nc.rhs); - const rhs_ty = self.builder.getRefType(rhs); - if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { - rhs = self.coerceToType(rhs, rhs_ty, inner_ty); - } - self.builder.br(merge_bb, &.{rhs}); - - // Continue at merge - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, inner_ty); - } - - fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .optional) return info.optional.child; - } - return .unresolved; - } - - // ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ - fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { // Lower the lambda body as a new anonymous function var buf: [64]u8 = undefined; @@ -4620,4 +3248,36 @@ pub const Lowering = struct { pub const canonicalIntConstraintName = lower_generic.canonicalIntConstraintName; pub const diagValueParamNotConst = lower_generic.diagValueParamNotConst; pub const diagValueParamRange = lower_generic.diagValueParamRange; + + // --- moved to lower/expr.zig (lower_expr) --- + pub const lowerStructLiteral = lower_expr.lowerStructLiteral; + pub const lowerInitBlock = lower_expr.lowerInitBlock; + pub const getStructFields = lower_expr.getStructFields; + pub const fixupMethodReceiver = lower_expr.fixupMethodReceiver; + pub const getStructTypeName = lower_expr.getStructTypeName; + pub const builtinTypeName = lower_expr.builtinTypeName; + pub const resolveFieldType = lower_expr.resolveFieldType; + pub const lowerFieldAccess = lower_expr.lowerFieldAccess; + pub const identifierBindsValue = lower_expr.identifierBindsValue; + pub const lowerNumericLimit = lower_expr.lowerNumericLimit; + pub const lowerStructConstant = lower_expr.lowerStructConstant; + pub const lowerOptionalChain = lower_expr.lowerOptionalChain; + pub const vectorLaneIndex = lower_expr.vectorLaneIndex; + pub const lowerFieldAccessOnType = lower_expr.lowerFieldAccessOnType; + pub const lowerEnumLiteral = lower_expr.lowerEnumLiteral; + pub const lowerErrorTagLiteral = lower_expr.lowerErrorTagLiteral; + pub const lowerTaggedEnumLiteral = lower_expr.lowerTaggedEnumLiteral; + pub const findTaggedVariant = lower_expr.findTaggedVariant; + pub const emitBadVariant = lower_expr.emitBadVariant; + pub const resolveVariantValue = lower_expr.resolveVariantValue; + pub const resolveVariantIndex = lower_expr.resolveVariantIndex; + pub const lowerArrayLiteral = lower_expr.lowerArrayLiteral; + pub const resolveArrayLiteralType = lower_expr.resolveArrayLiteralType; + pub const lowerIndexExpr = lower_expr.lowerIndexExpr; + pub const lowerSliceExpr = lower_expr.lowerSliceExpr; + pub const lowerTupleLiteral = lower_expr.lowerTupleLiteral; + pub const lowerDerefExpr = lower_expr.lowerDerefExpr; + pub const lowerForceUnwrap = lower_expr.lowerForceUnwrap; + pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce; + pub const resolveOptionalInner = lower_expr.resolveOptionalInner; }; diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig new file mode 100644 index 0000000..76663a9 --- /dev/null +++ b/src/ir/lower/expr.zig @@ -0,0 +1,1424 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +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 CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; +const headNameOfCallee = Lowering.headNameOfCallee; +const StructConstInfo = Lowering.StructConstInfo; + +pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { + // Check for tagged enum construction: .Variant.{ payload_fields } + // This happens when type_expr is an enum_literal and target_type is a union + if (sl.type_expr) |te| { + if (te.data == .enum_literal) { + const variant_name = te.data.enum_literal.name; + const union_ty = self.target_type orelse .unresolved; + if (!union_ty.isBuiltin()) { + const union_info = self.module.types.get(union_ty); + if (union_info == .tagged_union) { + return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); + } + } + } + } + + // `.{ name = ... }` against a tagged-union target_type. Reject: + // the only valid construction forms are `.variant(payload)` and + // `.variant.{ field, ... }`. Falling through would lower the + // user's values straight into the `(tag, payload_bytes)` slot + // pair and emit IR that LLVM later rejects. + if (sl.type_expr == null and sl.struct_name == null) { + const tu_ty = self.target_type orelse .unresolved; + if (!tu_ty.isBuiltin()) { + const tu_info = self.module.types.get(tu_ty); + if (tu_info == .tagged_union) { + if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { + const first_name = sl.field_inits[0].name.?; + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(tu_ty); + if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { + diags.addFmt( + .err, + span, + "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", + .{ ty_name, first_name, first_name, first_name }, + ); + } else { + self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); + } + } + return self.builder.enumInit(0, Ref.none, tu_ty); + } + } + } + } + + const ty: TypeId = if (sl.struct_name) |name| + // Source-aware (E2): a bare struct-literal type name resolves to the + // querying source's OWN same-name author, not the global `findByName` + // first-match — so `Box.{...}` in module B builds B's `Box`, never a + // flat-imported A's. `.undeclared`/`.pending` keep the empty-struct + // stub (byte-identical to the legacy `findByName orelse intern`); + // `.ambiguous`/`.not_visible` surface their loud diagnostic + poison. + self.resolveNominalLeaf(name, false, span) + else if (sl.type_expr) |te| + // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr + self.resolveTypeWithBindings(te) + else self.target_type orelse .unresolved; + + // Get struct field types for coercion and ordering + const struct_fields = self.getStructFields(ty); + + // Look up field defaults from AST + const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: { + const ti = self.module.types.get(ty); + break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null); + } else @as(?[]const u8, null); + const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn| + (self.struct_defaults_map.get(sn) orelse &.{}) + else + &.{}; + + // Check if any field_init has a name (named literal) + const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + + if (has_names and struct_fields.len > 0) { + // Named literal: reorder fields to match struct declaration order + // First, lower all field values in source order (to preserve evaluation order) + var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty; + defer lowered.deinit(self.alloc); + for (sl.field_inits) |fi| { + const saved_tt = self.target_type; + // Set target_type to the field's declared type so array literals + // know if the target is a vector, etc. + if (fi.name) |fname| { + for (struct_fields) |sf| { + if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { + self.target_type = sf.ty; + break; + } + } + } + const val = self.lowerExpr(fi.value); + self.target_type = saved_tt; + lowered.append(self.alloc, .{ + .val = val, + .name = fi.name orelse "", + .node = fi.value, + }) catch unreachable; + } + + // Build fields in declaration order + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + for (struct_fields, 0..) |sf, fi| { + const sf_name = self.module.types.getString(sf.name); + // Find the matching lowered value + var found = false; + for (lowered.items) |l| { + if (std.mem.eql(u8, l.name, sf_name)) { + var val = l.val; + const src_ty = self.builder.getRefType(val); + val = self.coerceToType(val, src_ty, sf.ty); + fields.append(self.alloc, val) catch unreachable; + found = true; + break; + } + } + if (!found) { + // Field not specified — use default if available, else zero + if (fi < field_defaults.len) { + if (field_defaults[fi]) |default_expr| { + // Coerce the default to the field type at the IR + // level (the implicit narrowing rule) so a float + // default folds/errors here instead of being + // silently bit-coerced by the backend. + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; + } else { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } else { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + } + + const result = self.builder.structInit(fields.items, ty); + if (sl.init_block) |ib| { + return self.lowerInitBlock(result, ty, ib); + } + return result; + } + + // Positional literal: use source order + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + + for (sl.field_inits, 0..) |fi, i| { + var val = self.lowerExpr(fi.value); + // Coerce field value to match struct field type + if (i < struct_fields.len) { + const src_ty = self.inferExprType(fi.value); + val = self.coerceToType(val, src_ty, struct_fields[i].ty); + } + fields.append(self.alloc, val) catch unreachable; + } + + // Pad missing fields with defaults or zeroes + if (fields.items.len < struct_fields.len) { + for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { + if (fi < field_defaults.len) { + if (field_defaults[fi]) |default_expr| { + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; + continue; + } + } + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + + const result = self.builder.structInit(fields.items, ty); + + // Lower init block if present + if (sl.init_block) |ib| { + return self.lowerInitBlock(result, ty, ib); + } + + return result; +} + +/// Lower an init block: store struct value to alloca, bind `self`, execute block, reload. +pub fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref { + // Store struct value to a temporary alloca + const ptr_ty = self.module.types.ptrTo(ty); + const slot = self.builder.alloca(ty); + self.builder.store(slot, struct_val); + + // Create a nested scope with `self` bound to the alloca pointer + var init_scope = Scope.init(self.alloc, self.scope); + defer init_scope.deinit(); + const saved_scope = self.scope; + self.scope = &init_scope; + + // `self` is the pointer to the struct (not an alloca itself — it IS the pointer value) + init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false }); + + // Lower the init block body + self.lowerBlock(ib); + + // Restore scope + self.scope = saved_scope; + + // Load and return the (possibly modified) struct value + return self.builder.load(slot, ty); +} + +/// Get the field list for a struct TypeId, or empty if not a struct. +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); + // Dereference pointer types to get to the underlying struct + if (info == .pointer) { + resolved = info.pointer.pointee; + if (resolved.isBuiltin()) return &.{}; + const inner = self.module.types.get(resolved); + return switch (inner) { + .@"struct" => |s| s.fields, + else => &.{}, + }; + } + return switch (info) { + .@"struct" => |s| s.fields, + else => &.{}, + }; +} + +/// If a method's first param expects a pointer (*T) but we're passing T by value, +/// swap the first arg with the alloca address (implicit address-of). +pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { + // Skip the implicit __sx_ctx param when inspecting the receiver slot. + const skip: usize = if (func.has_implicit_ctx) 1 else 0; + if (func.params.len <= skip) return; + const first_param_ty = func.params[skip].ty; + // Check if first param expects a pointer + if (!first_param_ty.isBuiltin()) { + const pi = self.module.types.get(first_param_ty); + if (pi == .pointer) { + // If obj is already a pointer type, it's already correct (no addr_of needed) + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .pointer) return; // already a pointer + } + // Method expects *T — pass the address of the receiver (value type in alloca) + if (obj_node.data == .identifier) { + if (self.scope) |scope| { + if (scope.lookup(obj_node.data.identifier.name)) |binding| { + if (binding.is_alloca) { + const ptr_ty = self.module.types.ptrTo(binding.ty); + method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); + return; + } + } + } + } + // Field access: obj.field.method() → GEP to field, pass pointer directly. + // This avoids copying the struct value (mutations through *T must be visible). + if (obj_node.data == .field_access) { + const gep_ref = self.lowerExprAsPtr(obj_node); + // GEP returns a pointer in LLVM but its IR type is the field value type. + // Wrap with addr_of (no-op in LLVM) to set the IR type to *T, + // preventing coerceCallArgs from doing a spurious alloca+store. + const ptr_ty = self.module.types.ptrTo(obj_ty); + method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty); + return; + } + // General case: alloca+store the value and pass the alloca pointer + { + const slot = self.builder.alloca(obj_ty); + self.builder.store(slot, method_args.items[0]); + method_args.items[0] = slot; + } + } else { + // Method expects a value `T` but the receiver is a `*T` (e.g. a + // `for xs: (*x)` by-ref capture) — deref to pass the value. + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .pointer and oi.pointer.pointee == first_param_ty) { + method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty); + } + } + } + } +} + +/// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. +pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { + if (ty.isBuiltin()) { + // Map builtin types to their names for method resolution (e.g., s64.eq) + return builtinTypeName(ty); + } + var resolved = ty; + const info = self.module.types.get(resolved); + if (info == .pointer) { + resolved = info.pointer.pointee; + if (resolved.isBuiltin()) return builtinTypeName(resolved); + } + const ri = self.module.types.get(resolved); + return switch (ri) { + .@"struct" => |s| self.module.types.getString(s.name), + else => null, + }; +} + +pub fn builtinTypeName(ty: TypeId) ?[]const u8 { + return switch (ty) { + .s8 => "s8", + .s16 => "s16", + .s32 => "s32", + .s64 => "s64", + .u8 => "u8", + .u16 => "u16", + .u32 => "u32", + .u64 => "u64", + .f32 => "f32", + .f64 => "f64", + .bool => "bool", + .string => "string", + else => null, + }; +} + +/// Resolve the type of a named field on a given type. +pub fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { + if (std.mem.eql(u8, field, "len")) return .s64; + if (std.mem.eql(u8, field, "ptr")) { + const elem_ty = self.getElementType(ty); + return self.module.types.manyPtrTo(elem_ty); + } + const field_name_id = self.module.types.internString(field); + // Check union fields + promoted fields + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { + .@"union" => |u| u.fields, + .tagged_union => |u| u.fields, + else => null, + }; + if (u_fields) |ufields| { + for (ufields) |f| { + if (f.name == field_name_id) return 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 sf.ty; + } + } + } + } + } + } + // Check tuple fields + if (!ty.isBuiltin()) { + const ti = self.module.types.get(ty); + if (ti == .tuple) { + const tuple = ti.tuple; + // Try named fields + if (tuple.names) |names| { + for (names, 0..) |name_id, i| { + if (name_id == field_name_id) return tuple.fields[i]; + } + } + // Try numeric index + const idx = std.fmt.parseInt(usize, field, 10) catch { + return .unresolved; + }; + if (idx < tuple.fields.len) return tuple.fields[idx]; + return .unresolved; + } + } + const struct_fields = self.getStructFields(ty); + for (struct_fields) |f| { + if (f.name == field_name_id) return f.ty; + } + return .unresolved; +} + +pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { + // `error.X` — an error-tag literal. The `error` keyword in expression + // position parses as identifier "error" (E0.2), so `error.X` is a + // field access we intercept here. `error` is reserved, so this is + // unambiguous (no struct/pack can be named `error`). + if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) { + return self.lowerErrorTagLiteral(fa.field, span); + } + + // Pack-arity intercept: `.len` in a pack-fn mono's + // body resolves to the comptime-known N. The mono doesn't + // materialise the `[]Any` slice that the inline path used, so + // `args` isn't in scope as a value. + if (self.pack_param_count) |ppc| { + if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { + if (ppc.get(fa.object.data.identifier.name)) |n| { + return self.builder.constInt(@as(i64, @intCast(n)), .s64); + } + } + } + + // Pack value projection: `xs.` where `` is a (zero-arg) method of + // the pack's constraint protocol projects it over every element → + // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) + if (self.pack_constraint) |pcon| { + if (fa.object.data == .identifier) { + if (pcon.get(fa.object.data.identifier.name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) != null) { + return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); + } + } + } + } + + // Interface-only enforcement (Decision): a member access on a + // constrained pack element `xs[i].` may only name a method of the + // constraint protocol — not an arbitrary concrete field. Checked here, + // on the `xs[i]` (index_expr) base, BEFORE substitution erases the + // "constrained to P" context. Protocol method CALLS go through the call + // path; a method name passes this check (it's in the protocol). + if (self.pack_constraint) |pcon| { + if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { + const base_name = fa.object.data.index_expr.object.data.identifier.name; + if (pcon.get(base_name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) == null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); + } + return self.builder.constInt(0, .void); + } + } + } + } + + // Check for struct constant access: Struct.CONST + if (fa.object.data == .identifier) { + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; + if (self.struct_const_map.get(qualified)) |info| { + return self.lowerStructConstant(info); + } + } + + // Numeric-limit accessor: `.min` / `.max` folds to a comptime + // const of the queried type (sibling of the identifier-receiver + // intercepts above). Placed AFTER `Struct.CONST` so a user const named + // `min`/`max` wins on its own struct; a builtin type name can never + // name a user struct (reserved — issue 0076), so they never collide. + if (self.lowerNumericLimit(fa, span)) |ref| return ref; + + // M1.3 — `obj.class` on any Obj-C-class pointer lowers to + // `object_getClass(obj)`. Sugar; the receiver is opaque so + // we don't auto-deref. Returns `Class` (alias for *void; + // typed Class(T) parameterization is M1.1.b). + if (std.mem.eql(u8, fa.field, "class")) { + const expr_ty = self.inferExprType(fa.object); + if (self.objc().isObjcClassPointer(expr_ty)) { + const obj_ref = self.lowerExpr(fa.object); + const ptr_void = self.module.types.ptrTo(.void); + const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); + const args = self.alloc.alloc(Ref, 1) catch unreachable; + args[0] = obj_ref; + return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); + } + } + + // M2.2 — `obj.field` where `field` is declared with `#property` + // on a foreign Obj-C class lowers as `[obj field]` (the synthesized + // getter). Receiver stays opaque — no auto-deref. + if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { + return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span); + } + + // M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class + // pointer for a plain instance field (NOT a #property) lowers as + // `object_getIvar(obj, load(___state_ivar))` + struct_gep on + // the state struct + load. The receiver is the opaque Obj-C id + // (matching Apple's `self` semantics); the state lives in the + // hidden `__sx_state` ivar. + if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { + return self.lowerObjcDefinedStateFieldRead(fa.object, info); + } + + var obj = self.lowerExpr(fa.object); + var obj_ty = self.inferExprType(fa.object); + + // Auto-deref: if the object is a pointer to a struct, load through it + if (!obj_ty.isBuiltin()) { + const ptr_info = self.module.types.get(obj_ty); + if (ptr_info == .pointer) { + const pointee = ptr_info.pointer.pointee; + obj = self.builder.load(obj, pointee); + obj_ty = pointee; + } + } + + // Special fields on slices/strings (NOT structs with .len/.ptr fields) + if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) { + // Only use length/data_ptr for slice, string, array, vector types + const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { + const info = self.module.types.get(obj_ty); + break :blk info == .slice or info == .array or info == .vector; + } else false); + + if (is_special) { + if (std.mem.eql(u8, fa.field, "len")) { + return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); + } + { + const elem_ty = self.getElementType(obj_ty); + const mp_ty = self.module.types.manyPtrTo(elem_ty); + return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty); + } + } + } + + // Optional chaining: p?.field + if (fa.is_optional) { + return self.lowerOptionalChain(obj, fa, span); + } + + return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); +} + +/// True when an `.identifier` receiver text resolves to an in-scope VALUE +/// binding rather than a builtin type. A backtick raw identifier (F0.6) can +/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``); +/// such a value is reachable through the same three sources the ordinary +/// identifier field-access path consults (see `expr_typer` `.identifier` +/// arm): lexical `scope`, program `global_names`, and module value +/// constants `module_const_map`. The numeric-limit intercept must defer to +/// ordinary field access whenever ANY of the three binds the name, so a +/// raw value field read is never hijacked into a numeric-limit fold +/// (issues 0092 local / 0093 global + module-const). A single helper used +/// by both lowering and inference keeps the two resolvers in lockstep +/// (issue-0083 two-resolver defect class). +pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool { + if (self.scope) |scope| { + if (scope.lookup(name) != null) return true; + } + if (self.program_index.global_names.get(name) != null) return true; + if (self.program_index.module_const_map.get(name) != null) return true; + return false; +} + +/// Numeric-limit accessor intercept (`.min`/`.max`/`.epsilon`/ +/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` / +/// `Struct.CONST` / pack-arity identifier-receiver intercepts in +/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried +/// type via the shared `TypeResolver` logic (no second computor) + the +/// existing `constInt` / `constFloat` const paths: +/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`); +/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/ +/// `.nan` → `constFloat` (via `floatLimitFor`). +/// Returns null when the field is not a limit accessor, or the receiver is not +/// a builtin type (a user struct → ordinary field lowering reports +/// field-not-found). Two clean diagnostics (then a placeholder, so lowering +/// finishes and `hasErrors()` aborts the build): +/// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`); +/// - any accessor on a builtin NON-numeric receiver +/// (`bool`/`string`/`void`/`Any`/`noreturn`). +pub fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref { + const name = switch (fa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => return null, + }; + if (!TypeResolver.isLimitField(fa.field)) return null; + const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null; + + // A backtick raw identifier (F0.6) can bind a value whose spelling + // shadows a builtin type name (`` `f64 := … ``). Field access on that + // value is an ordinary field read, not a numeric-limit fold — defer to + // the normal field-access path when the receiver identifier resolves to + // a value binding through any of scope / globals / module consts + // (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type + // and can never be value-shadowed. + if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null; + + if (TypeResolver.integerLimitFor(name, fa.field)) |value| { + return self.builder.constInt(value, ty); + } + if (TypeResolver.floatLimitFor(name, fa.field)) |value| { + return self.builder.constFloat(value, ty); + } + // The field is a limit accessor, but it does not apply to this type. + if (self.diagnostics) |d| { + if (TypeResolver.integerWidthSign(name) != null) { + // Integer receiver + a float-only accessor. + d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field }); + } else { + // Non-numeric builtin receiver (bool/string/void/Any/noreturn). + d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field }); + } + } + return self.emitPlaceholder(fa.field); +} + +/// Lower a struct-level constant value (e.g., Phys.GRAVITY). +pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { + const val_node = info.value; + return switch (val_node.data) { + .int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64), + .float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64), + .bool_literal => |lit| self.builder.constBool(lit.value), + .string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)), + else => self.lowerExpr(val_node), + }; +} + +/// Lower optional chaining: `p?.field` where p is ?T +/// Produces ?FieldType: some(unwrap(p).field) if p has value, else null +/// If FieldType is already optional (?U), flattens to ?U (no double wrapping) +pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { + const obj_ty = self.inferExprType(fa.object); + // Get the inner (non-optional) type + const inner_ty = if (!obj_ty.isBuiltin()) blk: { + const info = self.module.types.get(obj_ty); + break :blk if (info == .optional) info.optional.child else obj_ty; + } else obj_ty; + + // Get the field type on the inner type + const field_ty = self.resolveFieldType(inner_ty, fa.field); + // If field is already optional, flatten (don't double-wrap) + const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false; + const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty); + + // Check if optional has value + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool); + + // Create blocks + const some_bb = self.freshBlock("chain.some"); + const none_bb = self.freshBlock("chain.none"); + const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty}); + + self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); + + // Some: unwrap, access field (already ?FieldType if flattened, else wrap) + self.builder.switchToBlock(some_bb); + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); + const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); + const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); + self.builder.br(merge_bb, &.{some_result}); + + // None: produce null optional + self.builder.switchToBlock(none_bb); + const none_result = self.builder.constNull(result_ty); + self.builder.br(merge_bb, &.{none_result}); + + // Merge + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, result_ty); +} + +/// Field access on a known type (shared by regular field access and optional chaining) +/// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour +/// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any +/// other field name so the read path (`lowerFieldAccessOnType`) and the +/// write path (`lowerAssignment`) share one resolver and reject a +/// non-lane field identically (issue 0086). +pub fn vectorLaneIndex(field: []const u8) ?u32 { + if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0; + if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1; + if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2; + if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3; + return null; +} + +pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { + const field_name_id = self.module.types.internString(field); + + // Check if it's a union type + if (!obj_ty.isBuiltin()) { + const info = self.module.types.get(obj_ty); + switch (info) { + .tagged_union => |u| { + // .tag → extract the enum tag value with the correct tag type + if (std.mem.eql(u8, field, "tag")) { + return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); + } + // Tagged union — use enum_payload + for (u.fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); + } + } + // Check promoted fields from anonymous struct variants + for (u.fields) |f| { + if (!f.ty.isBuiltin()) { + const field_info = self.module.types.get(f.ty); + if (field_info == .@"struct") { + for (field_info.@"struct".fields, 0..) |sf, si| { + if (sf.name == field_name_id) { + const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); + return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); + } + } + } + } + } + }, + .@"union" => |u| { + // Untagged union — use union_get to reinterpret bytes + for (u.fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); + } + } + // Check promoted fields from anonymous struct variants + for (u.fields) |f| { + if (!f.ty.isBuiltin()) { + const field_info = self.module.types.get(f.ty); + if (field_info == .@"struct") { + for (field_info.@"struct".fields, 0..) |sf, si| { + if (sf.name == field_name_id) { + const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); + return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); + } + } + } + } + } + }, + else => {}, + } + } + + // Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) → + // lane 0/1/2/3. Shares lane-index resolution with the write path + // (lowerAssignment) via vectorLaneIndex; a non-lane field falls + // through to the field-not-found error below. + if (!obj_ty.isBuiltin()) { + const vinfo = self.module.types.get(obj_ty); + if (vinfo == .vector) { + if (Lowering.vectorLaneIndex(field)) |vidx| { + return self.builder.structGet(obj, vidx, vinfo.vector.element); + } + } + } + + // Closure field access: .fn_ptr → field 0, .env → field 1 + if (!obj_ty.isBuiltin()) { + const cinfo = self.module.types.get(obj_ty); + if (cinfo == .closure) { + if (std.mem.eql(u8, field, "fn_ptr")) { + const fn_ptr_ty = self.module.types.ptrTo(.void); + return self.builder.structGet(obj, 0, fn_ptr_ty); + } else if (std.mem.eql(u8, field, "env")) { + const env_ty = self.module.types.ptrTo(.void); + return self.builder.structGet(obj, 1, env_ty); + } + } + } + + // Tuple field access: .0, .1, etc. or named fields + if (!obj_ty.isBuiltin()) { + const tinfo = self.module.types.get(obj_ty); + if (tinfo == .tuple) { + const tuple = tinfo.tuple; + // Try named fields first + if (tuple.names) |names| { + for (names, 0..) |name_id, i| { + if (name_id == field_name_id) { + return self.builder.structGet(obj, @intCast(i), tuple.fields[i]); + } + } + } + // Try numeric index (e.g., "0", "1") + const idx = std.fmt.parseInt(u32, field, 10) catch { + return self.emitFieldError(obj_ty, field, span); + }; + if (idx < tuple.fields.len) { + return self.builder.structGet(obj, idx, tuple.fields[idx]); + } + return self.emitFieldError(obj_ty, field, span); + } + } + + // Resolve struct field index and type + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.structGet(obj, @intCast(i), f.ty); + } + } + + return self.emitFieldError(obj_ty, field, span); +} + +pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { + const target = self.target_type orelse .unresolved; + const tag = self.resolveVariantValue(target, el.name); + return self.builder.enumInit(tag, Ref.none, target); +} + +/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the +/// destination context (`target_type`) is a named error set, the value is +/// typed as that set and `X`'s membership is validated; otherwise the value +/// is the raw `u32` global tag id (per the spec's context rule). +pub fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref { + const tag_id = self.module.types.internTag(tag_name); + if (self.target_type) |t| { + if (!t.isBuiltin()) { + const info = self.module.types.get(t); + if (info == .error_set) { + // The bare-`!` inferred placeholder (reserved name "!") accepts + // any tag — its members aren't known until the whole-program SCC + // pass (E1.4) folds in every raised tag. Skip membership for it. + if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) { + var in_set = false; + for (info.error_set.tags) |member| { + if (member == tag_id) { + in_set = true; + break; + } + } + if (!in_set) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) }); + } + } + } + return self.builder.constInt(@as(i64, @intCast(tag_id)), t); + } + } + } + return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32); +} + +/// Lower a tagged enum construction: .Variant.{ field_inits } +/// The struct literal provides the payload fields; we wrap them in an enum_init. +pub fn lowerTaggedEnumLiteral( + self: *Lowering, + sl: *const ast.StructLiteral, + variant_name: []const u8, + union_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + span: ast.Span, +) Ref { + if (self.findTaggedVariant(union_info, variant_name) == null) { + self.emitBadVariant(union_ty, union_info, variant_name, span); + return self.builder.enumInit(0, Ref.none, union_ty); + } + + const tag = self.resolveVariantValue(union_ty, variant_name); + const name_id = self.module.types.internString(variant_name); + + // Find the payload type for this variant + var payload_ty: TypeId = .void; + for (union_info.fields) |f| { + if (f.name == name_id) { + payload_ty = f.ty; + break; + } + } + + if (payload_ty == .void or sl.field_inits.len == 0) { + // No payload or no fields — just tag + return self.builder.enumInit(tag, Ref.none, union_ty); + } + + // Lower the payload as a struct init of the payload type + const saved_tt = self.target_type; + self.target_type = payload_ty; + const payload_fields = self.getStructFields(payload_ty); + + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + + for (sl.field_inits, 0..) |fi, i| { + if (i < payload_fields.len) { + const saved_inner = self.target_type; + self.target_type = payload_fields[i].ty; + var val = self.lowerExpr(fi.value); + self.target_type = saved_inner; + const src_ty = self.inferExprType(fi.value); + val = self.coerceToType(val, src_ty, payload_fields[i].ty); + fields.append(self.alloc, val) catch unreachable; + } else { + fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable; + } + } + + // Pad missing payload fields with zeroes + if (fields.items.len < payload_fields.len) { + for (payload_fields[fields.items.len..]) |sf| { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + + const payload = self.builder.structInit(fields.items, payload_ty); + self.target_type = saved_tt; + + return self.builder.enumInit(tag, payload, union_ty); +} + +pub fn findTaggedVariant( + self: *Lowering, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, +) ?usize { + const name_id = self.module.types.internString(variant_name); + for (union_info.fields, 0..) |f, i| { + if (f.name == name_id) return i; + } + return null; +} + +pub fn emitBadVariant( + self: *Lowering, + union_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, + span: ast.Span, +) void { + const diags = self.diagnostics orelse return; + const ty_name = self.formatTypeName(union_ty); + var list: std.ArrayList(u8) = .empty; + for (union_info.fields, 0..) |f, i| { + if (i > 0) list.appendSlice(self.alloc, ", ") catch return; + list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; + } + diags.addFmt( + .err, + span, + "'{s}' is not a variant of '{s}' (variants are: {s})", + .{ variant_name, ty_name, list.items }, + ); +} + +/// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). +pub fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { + if (ty.isBuiltin()) return 0; + const info = self.module.types.get(ty); + const name_id = self.module.types.internString(variant_name); + switch (info) { + .@"enum" => |e| { + for (e.variants, 0..) |v, i| { + if (v == name_id) { + if (e.explicit_values) |vals| { + if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); + } + return @intCast(i); + } + } + }, + .tagged_union => |u| { + for (u.fields, 0..) |f, i| { + if (f.name == name_id) { + if (u.explicit_tag_values) |vals| { + if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); + } + return @intCast(i); + } + } + }, + else => {}, + } + return 0; +} + +/// Resolve a variant name to its tag index within an enum or union type. +pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { + if (ty.isBuiltin()) return 0; + const info = self.module.types.get(ty); + const name_id = self.module.types.internString(variant_name); + switch (info) { + .tagged_union => |u| { + for (u.fields, 0..) |f, i| { + if (f.name == name_id) return @intCast(i); + } + }, + .@"enum" => |e| { + for (e.variants, 0..) |v, i| { + if (v == name_id) return @intCast(i); + } + }, + else => {}, + } + return 0; +} + +pub fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref { + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + + // Determine element type: explicit type_expr > target_type > inference + var elem_ty: TypeId = .unresolved; + var from_target = false; + var is_vector = false; + + // First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3]) + if (al.type_expr) |te| { + const resolved = self.resolveArrayLiteralType(te); + if (resolved != .unresolved) { + if (!resolved.isBuiltin()) { + const info = self.module.types.get(resolved); + switch (info) { + .array => |a| { + elem_ty = a.element; + from_target = true; + }, + .vector => |v| { + elem_ty = v.element; + from_target = true; + is_vector = true; + }, + .slice => |s| { + elem_ty = s.element; + from_target = true; + }, + else => {}, + } + } + } + } + + if (!from_target) { + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const info = self.module.types.get(tt); + switch (info) { + .array => |a| { + elem_ty = a.element; + from_target = true; + }, + .slice => |s| { + elem_ty = s.element; + from_target = true; + }, + .vector => |v| { + elem_ty = v.element; + from_target = true; + is_vector = true; + }, + else => {}, + } + } + } + } + if (!from_target and al.elements.len > 0) { + const inferred = self.inferExprType(al.elements[0]); + if (inferred != .void) elem_ty = inferred; + } + + for (al.elements) |elem| { + const old_tt = self.target_type; + self.target_type = elem_ty; + var val = self.lowerExpr(elem); + self.target_type = old_tt; + // A nested `.[...]` element at a slice element type lowers to an + // aggregate array `[N]U` (lowerArrayLiteral always yields an array + // value); materialize it into a `[]U` slice so the element is a real + // {ptr,len} header rather than a raw array the callee would read its + // header off of (issue 0085). This per-element coercion recurses with + // the literal nesting, so `[][]T` and deeper coerce at every level. + if (!elem_ty.isBuiltin()) { + const ei = self.module.types.get(elem_ty); + if (ei == .slice) { + const val_ty = self.builder.getRefType(val); + if (!val_ty.isBuiltin()) { + const vi = self.module.types.get(val_ty); + if (vi == .array and vi.array.element == ei.slice.element) { + val = self.coerceToType(val, val_ty, elem_ty); + } + } + } + } + elems.append(self.alloc, val) catch unreachable; + } + + const result_ty = if (is_vector) + self.module.types.vectorOf(elem_ty, @intCast(al.elements.len)) + else + self.module.types.arrayOf(elem_ty, @intCast(al.elements.len)); + return self.builder.structInit(elems.items, result_ty); +} + +/// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]). +/// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr. +pub fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { + switch (te.data) { + .call => |cl| { + // Vector(3, f32) or Module.Vector(3, f32) + const callee_name = switch (cl.callee.data) { + .identifier => |id| id.name, + .field_access => |fa| fa.field, + else => return .unresolved, + }; + if (std.mem.eql(u8, callee_name, "Vector")) { + if (cl.args.len == 2) { + const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; + const elem = self.resolveTypeWithBindings(cl.args[1]); + return self.module.types.vectorOf(elem, length); + } + } + // Generic-struct typed-literal head (`Box(s64).[...]`): route + // through the single layout choke-point (CP-1). A qualified head + // `a.Box(s64).[...]` selects a's OWN template via the namespace edge + // (Counter-1: was the global last-wins map); a bare head selects the + // single bare-VISIBLE author. + if (headNameOfCallee(cl.callee)) |hn| { + switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { + .template => |t| return self.instantiateGenericStruct(&t, cl.args), + .poisoned => return .unresolved, + .not_generic => {}, + } + } + return .unresolved; + }, + .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), + .identifier => |id| { + // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type + // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is + // not bare-visible (consistent with annotations / 0763); ≥2 direct + // flat same-name authors are ambiguous (loud diagnostic, consistent + // with the leaf / 0755); a single source-keyed author resolves to + // ITS TypeId instead of a global `findByName` first-/last-wins pick. + switch (self.headTypeGate(id.name, te.span)) { + .ambiguous, .not_visible => return .unresolved, + .resolved => |tid| return tid, + .proceed => {}, + } + const name_id = self.module.types.internString(id.name); + return self.module.types.findByName(name_id) orelse .unresolved; + }, + .type_expr => |inner| { + if (self.headTypeLeak(inner.name, te.span)) return .unresolved; + return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + }, + .field_access => |fa| { + // Module.Type — try to resolve the field as a type name + const name_id = self.module.types.internString(fa.field); + return self.module.types.findByName(name_id) orelse .unresolved; + }, + else => return .unresolved, + } +} + +pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { + // Pack-arg substitution: `args[]` inside a body + // whose enclosing comptime call bound `args` as a pack name. + // Lowering the i-th call-site arg directly gives the concrete + // call-arg type — bypasses the `[]Any` slice boxing that would + // otherwise lose the type. Non-literal indices fall through to + // the standard slice indexing path. + if (self.packArgNodeAt(ie)) |arg_node| { + return self.lowerExpr(arg_node); + } + // Out-of-bounds pack indexing: object IS a pack name + index + // IS a comptime int literal but exceeds the pack arity. Emit + // a focused diagnostic so the user gets "pack index 2 out of + // bounds" instead of the generic "unresolved 'args'" that the + // fall-through scope-lookup would produce. + if (self.diagPackIndexOOB(ie)) { + return self.builder.constInt(0, .s64); + } + // Runtime index into a comptime-only pack (Decision 1): a pack has no + // runtime representation, so the index must be a compile-time constant. + // A runtime index is a hard error — clearer than the "unresolved + // ''" the slice-index fall-through would otherwise produce. + if (self.pack_param_count) |ppc| { + if (ie.object.data == .identifier) { + const pname = ie.object.data.identifier.name; + if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname}); + } + return self.builder.constInt(0, .s64); + } + } + } + const obj = self.lowerExpr(ie.object); + const idx = self.lowerExpr(ie.index); + // Infer element type from the object's slice/array type + const obj_ty = self.inferExprType(ie.object); + const elem_ty = self.getElementType(obj_ty); + return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); +} + +pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { + const obj = self.lowerExpr(se.object); + const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); + const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); + // Infer result slice type from the object + const obj_ty = self.inferExprType(se.object); + // Subslice of string stays string (same {ptr, i64} layout, correct type category) + if (obj_ty == .string) { + return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); + } + const elem_ty = self.getElementType(obj_ty); + const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); + return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); +} + +pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + var field_type_ids = std.ArrayList(TypeId).empty; + defer field_type_ids.deinit(self.alloc); + var name_ids = std.ArrayList(types.StringId).empty; + defer name_ids.deinit(self.alloc); + var has_names = false; + + // A tuple_init's element values must match its field types exactly + // (LLVM `insertvalue` does no implicit conversion). When a contextual + // target tuple of matching arity is in scope (annotation, assignment + // LHS, call/return slot), its field types drive element lowering so an + // ambient scalar `target_type` (e.g. the enclosing fn's int return + // type) can't narrow an element below its field width. Otherwise each + // element's type is inferred independently. + // A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields, + // so element-count ≠ field-count and a contextual target tuple can't be + // aligned by index — infer field types from the expanded refs instead. + var has_spread = false; + for (tl.elements) |elem| { + if (elem.value.data == .spread_expr) has_spread = true; + } + + // Contextual target tuple field types. Without a spread we require + // exact arity (existing behavior); with a spread we index positionally + // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces + // / erases each spliced element to its slot's type). + var target_fields: ?[]const TypeId = null; + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const tinfo = self.module.types.get(tt); + if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) { + target_fields = tinfo.tuple.fields; + } + } + } + + const saved_target = self.target_type; + var out_idx: usize = 0; + for (tl.elements) |elem| { + // Pack-spread element → splice its per-element values as fields. + if (elem.value.data == .spread_expr) { + const sp_operand = elem.value.data.spread_expr.operand; + if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| { + defer self.alloc.free(refs); + // Element AST nodes (for protocol-erasure lvalue/name fallback) + // when the spread is a bare pack name. + const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null) + self.pack_arg_nodes.?.get(sp_operand.data.identifier.name) + else + null; + for (refs, 0..) |r, ri| { + var val = r; + var vty = self.builder.getRefType(r); + if (target_fields) |tf| { + if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) { + const want = tf[out_idx]; + const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value; + val = self.coerceOrErase(r, vty, want, node); + vty = want; + } + } + elems.append(self.alloc, val) catch unreachable; + field_type_ids.append(self.alloc, vty) catch unreachable; + name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; + out_idx += 1; + } + continue; + } + // Not a pack spread (e.g. tuple-value spread) — not yet handled. + _ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic + continue; + } + const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value); + self.target_type = field_ty; + var val = self.lowerExpr(elem.value); + self.target_type = saved_target; + const val_ty = self.builder.getRefType(val); + if (val_ty != field_ty and val_ty != .void) { + val = self.coerceToType(val, val_ty, field_ty); + } + elems.append(self.alloc, val) catch unreachable; + field_type_ids.append(self.alloc, field_ty) catch unreachable; + if (elem.name) |name| { + name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable; + has_names = true; + } else { + name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; + } + out_idx += 1; + } + + // Reuse the contextual target tuple type when it drove lowering so the + // value's type identity (incl. field names) matches the destination + // slot; otherwise build the tuple type from the inferred fields. + const tuple_ty = if (target_fields != null and self.target_type != null) + self.target_type.? + else + self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable, + .names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null, + } }); + + const owned = self.alloc.dupe(Ref, elems.items) catch unreachable; + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); +} + +pub fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref { + const ptr = self.lowerExpr(de.operand); + // Resolve pointee type from the pointer type. + const ptr_ty = self.inferExprType(de.operand); + if (!ptr_ty.isBuiltin()) { + const info = self.module.types.get(ptr_ty); + if (info == .pointer) { + return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee); + } + } + // Operand isn't a pointer — `.*` is invalid. Diagnose here instead of + // emitting a `.deref` with an `.unresolved` result type, which would + // otherwise slip through to emit_llvm's "unresolved type reached LLVM + // emission" panic with no source location. + if (self.diagnostics) |d| { + d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)}); + } + return ptr; +} + +pub fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref { + const val = self.lowerExpr(fu.operand); + const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand)); + return self.builder.optionalUnwrap(val, inner_ty); +} + +pub fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { + const lhs = self.lowerExpr(nc.lhs); + const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); + + // Short-circuit: only evaluate RHS if LHS is null. + // IMPORTANT: optional_unwrap must be in the "has value" branch, + // not before the condBr — the interpreter errors on unwrapping null. + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); + + const then_bb = self.freshBlock("nc.has"); + const rhs_bb = self.freshBlock("nc.rhs"); + const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); + + // If has value, go to then_bb to unwrap; else go to rhs_bb + self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); + + // Then block: unwrap LHS and branch to merge + self.builder.switchToBlock(then_bb); + const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); + self.builder.br(merge_bb, &.{unwrapped}); + + // RHS block: evaluate fallback and branch to merge + self.builder.switchToBlock(rhs_bb); + var rhs = self.lowerExpr(nc.rhs); + const rhs_ty = self.builder.getRefType(rhs); + if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { + rhs = self.coerceToType(rhs, rhs_ty, inner_ty); + } + self.builder.br(merge_bb, &.{rhs}); + + // Continue at merge + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, inner_ty); +} + +pub fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .optional) return info.optional.child; + } + return .unresolved; +} + +// ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─