const std = @import("std"); 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 errors = @import("../../errors.zig"); const TypeResolver = @import("../type_resolver.zig").TypeResolver; const TypeId = types.TypeId; const StringId = types.StringId; const Ref = inst_mod.Ref; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const Module = mod_mod.Module; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; const Scope = lower.Scope; const binOpSymbol = Lowering.binOpSymbol; const arithResultType = Lowering.arithResultType; const exprIsFailable = Lowering.exprIsFailable; 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), 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 /// (locals, globals, and module-consts alike). A single helper used /// by both lowering and inference keeps the two resolvers in lockstep /// (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 //. 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. 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. 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; } // ── Core expression dispatch ─────────────────────────────────── pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Stamp this node's source span onto the instructions it emits (ERR // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ // restore so a parent's later emits keep the parent's span after a // child lowers. Skip the empty default so synthetic nodes don't reset // a meaningful enclosing span to offset 0. const saved_span = self.builder.current_span; defer self.builder.current_span = saved_span; if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end }; // A node carrying an explicit `source_file` is one spliced into a body // from another module — a substituted caller comptime-`$`-arg (stamped // at the `cpn` build site in lowerComptimeCall / monomorphizePackFn). // Resolve its bare names in THAT module's visibility context, overriding // the body's defining-module pin, then restore so sibling callee nodes // keep the enclosing context. Ordinary expression nodes never carry a // `source_file`, so this is a no-op on the hot path. const restore_source = node.source_file != null; const saved_source = self.current_source_file; if (node.source_file) |sf| self.setCurrentSourceFile(sf); defer if (restore_source) self.setCurrentSourceFile(saved_source); return switch (node.data) { // Bare `$` in expression position → an `[]Type` slice // value where each element is a `const_type(arg_types[i])`. // Per `Type → .any` mapping in type_bridge, the IR slice // type is `[]Any`; the interp stores raw `.type_tag` Values // (NOT Any-boxed) so `args[i]` reads back as a Type value // directly. Step 4 final slice — lets builder fns walk the // whole pack at interp time. .comptime_pack_ref => |cpr| blk: { // `$` is overloaded in expression position: // - Inside a pack-fn mono (or a `tryPackImplMatch` // impl mono), `name` is a pack binding → slice of // element types (`[]Type` lowered as `[]Any`). // - Inside an impl mono whose impl pattern bound a // single-type generic (`$R: Type` in // `Closure(..$args) -> $R`), `name` is in // `type_bindings` → single `const_type(R)` value. // Pack arg types are checked first (the slice form), // then pack_bindings (the impl-mono mirror), then // type_bindings (single-type binding); only if all // miss is it a real "outside an active binding" error. if (self.pack_arg_types) |pat| { if (pat.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.pack_bindings) |pb| { if (pb.get(cpr.pack_name)) |arg_tys| { break :blk self.buildPackSliceValue(arg_tys); } } if (self.type_bindings) |tb| { if (tb.get(cpr.pack_name)) |ty| { break :blk self.builder.constType(ty); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name}); } break :blk self.builder.constNull(self.module.types.sliceOf(.any)); }, // Pack-index in expression position: `$[]` → // `const_type(arg_types[index])`. Yields a comptime-only // Type value (`Value.type_tag(TypeId)` in the interp). // OOB / no-active-pack-binding → focused diagnostic; the // emitted Ref is a const_type(.void) placeholder so the // verifier downstream catches misuse rather than silently // succeeding with .void. .pack_index_type_expr => |pi| blk: { if (self.pack_arg_types) |pat| { if (pat.get(pi.pack_name)) |arg_tys| { if (pi.index < arg_tys.len) { break :blk self.builder.constType(arg_tys[pi.index]); } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ pi.pack_name, pi.index, pi.pack_name, arg_tys.len, if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), }); } break :blk self.builder.constType(.void); } } if (self.diagnostics) |diags| { diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ pi.pack_name, pi.index, }); } break :blk self.builder.constType(.void); }, .int_literal => |lit| { // If target is a float type, emit as float literal if (self.target_type) |tt| { if (tt == .f32 or tt == .f64) { return self.builder.constFloat(@floatFromInt(lit.value), tt); } } const ty = if (self.target_type) |tt| blk: { break :blk if (self.isIntEx(tt)) tt else .s64; } else .s64; return self.builder.constInt(lit.value, ty); }, .float_literal => |lit| { const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64; return self.builder.constFloat(lit.value, fty); }, .bool_literal => |lit| self.builder.constBool(lit.value), .string_literal => |lit| blk: { const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; const sid = self.module.types.internString(str); break :blk self.builder.constString(sid); }, // A bare `null` / `---` with no surrounding type expectation is a // legitimate typeless literal, not a failed lookup: `.void` is its // intentional default (emitConstNull/emitConstUndef handle void as // null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire. .null_literal => self.builder.constNull(self.target_type orelse .void), .undef_literal => self.builder.constUndef(self.target_type orelse .void), .identifier => |id| blk: { // A bare pack name in value position has no runtime // representation (Decision 1). Projections (`xs.len`, `xs[i]`, // `xs.value`) are field/index nodes handled elsewhere, so a bare // `xs` reaching here is always a pack-as-value misuse. if (self.isPackName(id.name)) { break :blk self.diagPackAsValue(id.name, node.span, .generic); } if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { break :blk self.builder.load(binding.ref, binding.ty); } break :blk binding.ref; } } // Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals if (self.comptime_constants.get(id.name)) |cv| { switch (cv) { .int_val => |iv| break :blk self.builder.constInt(iv, .s64), .enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty), } } // `context` resolves to a load through the lowering's // current `__sx_ctx` pointer. Every sx function (and // every `push Context.{...}` body) sets `current_ctx_ref` // to a `*Context` it owns, so this is one indirection. if (std.mem.eql(u8, id.name, "context")) { if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { break :blk self.diagnoseMissingContext("the `context` identifier"); } const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { break :blk self.diagnoseMissingContext("the `context` identifier"); }; break :blk self.builder.load(self.current_ctx_ref, ctx_ty); } // Check globals (#run constants) if (self.program_index.global_names.get(id.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Check module-level value constants (e.g. AF_INET :s32: 2) if (self.program_index.module_const_map.get(id.name)) |ci_global| { if (!self.isNameVisible(id.name)) { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name}); break :blk self.emitError(id.name, node.span); } // F2: emit the SOURCE-AWARE author's value (own-wins), not the // global last-wins `ci_global`. ≥2 flat-visible same-name const // authors → a loud ambiguity, never a silent // pick. `.none` after a visible name is the registration-only // author (no per-source partition) — emit its global value. switch (self.selectModuleConst(id.name)) { .resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source), .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); break :blk self.emitPlaceholder(id.name); }, .none => break :blk self.emitModuleConst(ci_global, null), } } // Check if it's a function name — produce function pointer reference // Resolve mangled name for block-local functions const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; if (self.program_index.fn_ast_map.contains(eff_fn_name)) { // Visibility check only for user-typed bare names (id.name // == eff_fn_name) without a UFCS alias. Mangled local- // scope names and UFCS rewrites are compiler indirections // and stay exempt. if (std.mem.eql(u8, eff_fn_name, id.name) and self.program_index.ufcs_alias_map.get(id.name) == null and !self.isNameVisible(eff_fn_name)) { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name}); break :blk self.emitError(eff_fn_name, node.span); } // Type-as-value: if target is Any (Type variable), produce a type name string if (self.target_type == .any) { const fd = self.program_index.fn_ast_map.get(eff_fn_name).?; const fn_type_str = self.formatFnTypeString(fd); const sid = self.module.types.internString(fn_type_str); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // taking a bare same-name fn as a VALUE // (func_ref, fn-ptr / closure coercion) must capture the // RESOLVED author's FuncId for a genuine flat collision, not // the first-wins winner's. Plain bare name only; `.ambiguous` // → loud diagnostic; `.none` → existing first-wins path. The // winner is lazily lowered ONLY on `.none` — a rerouted value // never uses the winner, so its body must not be lowered. const value_fid: ?FuncId = blk_fv: { if (std.mem.eql(u8, eff_fn_name, id.name) and self.program_index.ufcs_alias_map.get(id.name) == null and (if (self.scope) |scope| scope.lookup(id.name) == null else true)) { if (self.current_source_file) |caller_file| { switch (self.selectPlainCallableAuthor(id.name, caller_file)) { .func => |sf| { var selected = sf; break :blk_fv self.selectedFuncId(&selected, id.name); }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name}); break :blk self.emitError(id.name, node.span); }, .none => {}, } } } if (!self.lowered_functions.contains(eff_fn_name)) { self.lazyLowerFunction(eff_fn_name); } break :blk_fv self.resolveFuncByName(eff_fn_name); }; if (value_fid) |fid| { // Auto-promote bare function → closure when target_type is closure if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tt_info = self.module.types.get(tt); if (tt_info == .closure) { const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure); break :blk self.builder.closureCreate(tramp_id, Ref.none, tt); } // Coercing a bare fn name to a fn-pointer // type — the call_conv must match. A // default-conv sx fn assigned to a // callconv(.c) slot (e.g. passed to // pthread_create) would otherwise crash at // runtime when the C caller doesn't supply // the implicit __sx_ctx arg. if (tt_info == .function) { const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv; if (func_cc != tt_info.function.call_conv) { if (self.diagnostics) |d| { const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention"; const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention"; d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc }); } break :blk self.emitPlaceholder(eff_fn_name); } } // NOTE: `xx : *void` (e.g. // `class_addMethod(_, _, xx my_imp, _)`) // is intentionally NOT diagnosed here. // Manually-constructed Closure values // legitimately store default-conv sx fns // into a `*void` slot for sx-side dispatch // through the closure trampoline ABI. The // compiler can't distinguish C-side vs // sx-side use from the cast alone. // examples/50-smoke.sx has both shapes. } } break :blk self.builder.emit(.{ .func_ref = fid }, .s64); } } // Type-as-value: a name that resolves to a TypeId // (primitive, alias, registered struct/enum/union, // generic-struct instantiation) evaluates to a // `const_type` in expression position. Works for // direct assignment to a `Type`-typed slot // (`x: Type = Vec4`), comparison (`x == Vec4`), and // pack-arg / Any context (boxing happens at the // consumer). // E4 single-hop visibility + ambiguity gate: a bare type name used // as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over // 2+ flat hops is not bare-visible (consistent with annotations / // 0763); ≥2 direct flat same-name authors are ambiguous (loud // diagnostic, 0755/0767). A single source-keyed author — including // the querying source's OWN author over a same-name flat import // (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name // author a global `findByName` would pick. A value name / generic // param / undeclared name → `.proceed`, falling through below. const ty = blk_ty: { switch (self.headTypeGate(id.name, node.span)) { .ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name), .resolved => |tid| break :blk_ty tid, .proceed => {}, } if (self.type_bindings) |tb| { if (tb.get(id.name)) |t| break :blk_ty t; } if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t; if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t; const name_id = self.module.types.internString(id.name); if (self.module.types.findByName(name_id)) |t| break :blk_ty t; break :blk_ty TypeId.void; }; if (ty != .void) { break :blk self.builder.constType(ty); } // Unknown identifier break :blk self.emitError(id.name, node.span); }, .binary_op => |bop| self.lowerBinaryOp(&bop), .unary_op => |uop| blk: { // `xx ` with a slice target materializes the comptime // pack into a runtime `[]elem` (issue 0053). Must run before the // operand is lowered (a bare pack name otherwise hits the // pack-as-value error). if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) { const pname = uop.operand.data.identifier.name; if (self.target_type) |tt| { if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) { break :blk self.lowerPackToSlice(pname, tt); } } break :blk self.diagPackAsValue(pname, node.span, .generic); } // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of if (uop.op == .address_of and uop.operand.data == .index_expr) { const ie = &uop.operand.data.index_expr; const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); const elem_ty = self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For array targets, use the storage pointer (alloca for a // local, global_addr for a module global) so the resulting // pointer is into live storage, not a loaded copy. const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); } // address_of(field_access) → use lowerExprAsPtr for GEP chain // Handles all cases: pointer-based, index-based, nested field access if (uop.op == .address_of and uop.operand.data == .field_access) { const inner_ty = self.inferExprType(uop.operand); const ptr_ty = self.module.types.ptrTo(inner_ty); const ptr = self.lowerExprAsPtr(uop.operand); break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty); } // address_of(identifier) → return alloca directly (pointer to variable) if (uop.op == .address_of and uop.operand.data == .identifier) { const id_name = uop.operand.data.identifier.name; if (self.scope) |scope| { if (scope.lookup(id_name)) |binding| { if (binding.is_alloca) { const ptr_ty = self.module.types.ptrTo(binding.ty); break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); } } } // address_of(global) → emit global_addr (pointer to global, not load) if (self.program_index.global_names.get(id_name)) |gi| { const ptr_ty = self.module.types.ptrTo(gi.ty); break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); } } const operand = self.lowerExpr(uop.operand); break :blk switch (uop.op) { .negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)), .not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool), .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)), .xx => self.lowerXX(operand, uop.operand), .address_of => blk2: { const inner_ty = self.inferExprType(uop.operand); const ptr_ty = self.module.types.ptrTo(inner_ty); break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty); }, }; }, .if_expr => |ie| self.lowerIfExpr(&ie), .match_expr => |me| self.lowerMatch(&me), .while_expr => |we| self.lowerWhile(&we), .for_expr => |fe| self.lowerFor(&fe), .break_expr => self.lowerBreak(), .continue_expr => self.lowerContinue(), .call => |c| self.lowerCall(&c), .ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic), .field_access => |fa| self.lowerFieldAccess(&fa, node.span), .struct_literal => |sl| self.lowerStructLiteral(&sl, node.span), .array_literal => |al| self.lowerArrayLiteral(&al), .index_expr => |ie| self.lowerIndexExpr(&ie), .slice_expr => |se| self.lowerSliceExpr(&se), .lambda => |lam| self.lowerLambda(&lam), .force_unwrap => |fu| self.lowerForceUnwrap(&fu), .null_coalesce => |nc| self.lowerNullCoalesce(&nc), .deref_expr => |de| self.lowerDerefExpr(&de), .enum_literal => |el| self.lowerEnumLiteral(&el), .comptime_expr => |ct| self.lowerInlineComptime(ct.expr), .insert_expr => |ins| blk: { break :blk self.lowerInsertExprValue(ins.expr); }, .tuple_literal => |tl| self.lowerTupleLiteral(&tl), .spread_expr => self.emitError("spread_expr", node.span), .chained_comparison => |cc| self.lowerChainedComparison(&cc), // `#jni_env(env) { body }` in expression position — the block's // value becomes the env-scope's value. Save→set→body-value→restore. .jni_env_block => |eb| blk: { const env_ref = self.lowerExpr(eb.env); const fids = self.getJniEnvTlFids(); const ptr_ty = self.module.types.ptrTo(.void); const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void); self.jni_env_stack.append(self.alloc, env_ref) catch unreachable; const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void); _ = self.jni_env_stack.pop(); const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable; _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void); break :blk value; }, // Statements that can appear in expression position .block => |blk| blk: { // Create a child scope for block-level variable shadowing var block_scope = Scope.init(self.alloc, self.scope); const saved_scope = self.scope; self.scope = &block_scope; const saved_defer_len = self.defer_stack.items.len; defer { self.emitBlockDefers(saved_defer_len); self.scope = saved_scope; block_scope.deinit(); } // This block sits in value position (lowerExpr is reached only // for value contexts — statement blocks go through lowerBlock). // If its last expression's value is discarded by a `;`, the // surrounding expression has no value to use: report it. if (!blk.produces_value and blk.discarded_semi != null) { if (self.diagnostics) |diags| { diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{}); } } // A block in expression position yields its last statement's // value only when it produces one (no trailing `;`); otherwise // it runs as statements and evaluates to void. if (blk.produces_value and blk.stmts.len > 0) { for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { self.lowerStmt(stmt); } break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse self.builder.constInt(0, .void); } for (blk.stmts) |stmt| { self.lowerStmt(stmt); } break :blk self.builder.constInt(0, .void); }, // type_expr can appear as a variable reference when the name collides // with a builtin type name (e.g. s2, u8). Check scope first. .type_expr => |te| blk: { if (self.scope) |scope| { if (scope.lookup(te.name)) |binding| { if (binding.is_alloca) { break :blk self.builder.load(binding.ref, binding.ty); } break :blk binding.ref; } } if (self.program_index.global_names.get(te.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Type literal in expression position → first-class // `const_type` Value (i64 = TypeId.index()). Makes // `t : Type = f64;` store a real TypeId; lets // `t == f64` icmp at runtime against the same TypeId. if (self.isKnownTypeName(te.name)) { const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); break :blk self.builder.constType(ty); } break :blk self.emitError(te.name, node.span); }, .try_expr => |te| self.lowerTry(te.operand, node.span), .catch_expr => |ce| self.lowerCatch(&ce, node.span), .caller_location => self.lowerCallerLocation(node), else => self.emitError("unknown_expr", node.span), }; } /// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns /// the element (pointee) type so a value-position use can auto-deref it. pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId { if (node.data != .identifier) return null; const scope = self.scope orelse return null; const binding = scope.lookup(node.data.identifier.name) orelse return null; if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null; const info = self.module.types.get(binding.ty); return if (info == .pointer) info.pointer.pointee else null; } pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { // Short-circuit: `a and b` → if a then b else false if (bop.op == .and_op) { const lhs = self.lowerExpr(bop.lhs); const rhs_bb = self.freshBlock("and.rhs"); const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool}); const false_val = self.builder.constBool(false); self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val}); self.builder.switchToBlock(rhs_bb); const rhs = self.lowerExpr(bop.rhs); self.builder.br(merge_bb, &.{rhs}); self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, .bool); } // Short-circuit: `a or b` → if a then true else b if (bop.op == .or_op) { // A failable `or` (value-terminator or chain) routes to the error- // handling lowering, not the optional/boolean unwrap below. Detected // structurally (a `try`-chain's value type is non-failable `T`, so a // type-only `exprIsFailable(lhs)` would miss nested chains). if (self.orIsFailableChain(bop)) { return self.lowerFailableOr(bop); } const lhs = self.lowerExpr(bop.lhs); const rhs_bb = self.freshBlock("or.rhs"); const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool}); const true_val = self.builder.constBool(true); self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{}); self.builder.switchToBlock(rhs_bb); const rhs = self.lowerExpr(bop.rhs); self.builder.br(merge_bb, &.{rhs}); self.builder.switchToBlock(merge_bb); return self.builder.blockParam(merge_bb, 0, .bool); } // Type-literal comparison fold: when both sides are type-shaped // AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to // a static TypeId at lower time (`type_of(x)` for any // statically-typed `x`), resolve each and emit a `const_bool`. // Same semantic as `type_eq(A, B)` but using the standard `==` // operator — the user's intuition. Without the fold, both // sides lower as `const_type` undef-i64 and the runtime icmp // returns garbage. if (bop.op == .eq or bop.op == .neq) { if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) { const lhs_ty = self.resolveTypeArg(bop.lhs); const rhs_ty = self.resolveTypeArg(bop.rhs); const eq_result = lhs_ty == rhs_ty; return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result); } } // Any-shaped `==` (e.g. `t == s64` where `t: Type`): both // operands are 16-byte `{tag, value}` aggregates. LLVM // doesn't accept `icmp` on aggregates directly. Decompose // via `unbox_any` (which extracts the value field at // `.s64`) and compare the i64s. Tag fields are stable // across compilations of the same source so value-only // identity is enough. if (bop.op == .eq or bop.op == .neq) { const lhs_ty = self.inferExprType(bop.lhs); const rhs_ty = self.inferExprType(bop.rhs); if (lhs_ty == .any and rhs_ty == .any) { const lhs = self.lowerExpr(bop.lhs); const rhs = self.lowerExpr(bop.rhs); const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64); const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64); if (bop.op == .eq) { return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); } else { return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); } } } // Special case: optional == null / optional != null if (bop.op == .eq or bop.op == .neq) { const lhs_is_null = bop.lhs.data == .null_literal; const rhs_is_null = bop.rhs.data == .null_literal; if (lhs_is_null or rhs_is_null) { const opt_node = if (rhs_is_null) bop.lhs else bop.rhs; const opt_ty = self.inferExprType(opt_node); if (!opt_ty.isBuiltin()) { const info = self.module.types.get(opt_ty); if (info == .optional) { const opt_val = self.lowerExpr(opt_node); const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); // == null → !has_value, != null → has_value return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has; } } } } // Error-set equality: an error-set value compares only with an // `error.X` tag literal or another error-set value. Comparing to a raw // integer is a type error (coerce with `xx`). `e == error.X` resolves // X against e's set and validates membership. if (bop.op == .eq or bop.op == .neq) { if (self.tryLowerErrorSetEquality(bop)) |result| return result; } // Set target_type for null literals to match the other operand's type. // This ensures null gets the same LLVM type as the value being compared. if (bop.op == .eq or bop.op == .neq) { const null_on_rhs = bop.rhs.data == .null_literal; const null_on_lhs = bop.lhs.data == .null_literal; if (null_on_rhs or null_on_lhs) { var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); // Lower the non-null side first when its type isn't statically // inferable, and take the null's type from the lowered value — // never a guess. var pre_lowered: ?Ref = null; if (other_ty == .unresolved) { pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs); other_ty = self.builder.getRefType(pre_lowered.?); } if (other_ty != .void and other_ty != .unresolved) { const saved_tt = self.target_type; self.target_type = other_ty; const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?; const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?; self.target_type = saved_tt; const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }; return self.builder.emit(cmp_op, .bool); } } } var lhs = self.lowerExpr(bop.lhs); // A `for xs: (*x)` capture is a pointer; in a value position (here, an // operand) it auto-derefs to the element. const lhs_ref_pointee = self.refCapturePointee(bop.lhs); if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p); // Set target_type from LHS so enum literals on RHS resolve correctly. // When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use // the lowered operand's concrete type rather than a guess. const lhs_ty = blk: { if (lhs_ref_pointee) |p| break :blk p; const it = self.inferExprType(bop.lhs); break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it; }; const saved_tt = self.target_type; if (lhs_ty != .void) { if (!lhs_ty.isBuiltin()) { const lhs_info = self.module.types.get(lhs_ty); if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) { self.target_type = lhs_ty; } } else if (lhs_ty == .f32 or lhs_ty == .f64) { self.target_type = lhs_ty; } } var rhs = self.lowerExpr(bop.rhs); const rhs_ref_pointee = self.refCapturePointee(bop.rhs); if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p); self.target_type = saved_tt; // Result type follows the shared promotion rule: an int LHS with a // float RHS promotes to the float (`s64 * f32` → `f32`); vectors / // structs keep the LHS type. `inferExprType` reuses the same helper // so static typing agrees with the value produced here. const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs); var ty = arithResultType(lhs_ty, rhs_inferred); // Auto-unwrap optional operands for arithmetic/comparison if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) { ty = info.optional.child; lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty); } } const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs); if (!rhs_ty.isBuiltin()) { const rhs_info = self.module.types.get(rhs_ty); if (rhs_info == .optional) { rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child); } } // String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison if (ty == .string and (bop.op == .eq or bop.op == .neq)) { return if (bop.op == .eq) self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool) else self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool); } // Tuple operators if (!ty.isBuiltin()) { const lhs_info = self.module.types.get(ty); if (lhs_info == .tuple) { return self.lowerTupleOp(bop, lhs, rhs, ty); } } // Tuple membership: value in (tuple) if (bop.op == .in_op) { const rhs_ty_raw = self.inferExprType(bop.rhs); if (!rhs_ty_raw.isBuiltin()) { const rhs_info_raw = self.module.types.get(rhs_ty_raw); if (rhs_info_raw == .tuple) { return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple); } } } // Reject scalar ops on incompatible operand types (e.g. // `s64 + string`, `s64 < string`, `s64 & string`). The result type // `ty` is derived from the LHS, so without this the op lowers as // ` : ` and either reinterprets the RHS bytes (arithmetic // / bitwise → garbage) or feeds mismatched LLVM types to `icmp` // (ordering → verifier failure). { const group: enum { none, arith, ordering, bitwise } = switch (bop.op) { .add, .sub, .mul, .div, .mod => .arith, .lt, .lte, .gt, .gte => .ordering, .bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise, else => .none, }; if (group != .none) { const eff_rhs_ty = blk: { if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs); if (!rhs_ty.isBuiltin()) { const ri = self.module.types.get(rhs_ty); if (ri == .optional) break :blk ri.optional.child; } break :blk rhs_ty; }; const ok = switch (group) { .arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty), .ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty), .bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty), .none => true, }; if (!ok) { if (self.diagnostics) |diags| { diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{ binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty), }); } return self.emitPlaceholder("operand-type-mismatch"); } } } return switch (bop.op) { .add => self.builder.add(lhs, rhs, ty), .sub => self.builder.sub(lhs, rhs, ty), .mul => self.builder.mul(lhs, rhs, ty), .div => self.builder.div(lhs, rhs, ty), .mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), .eq => self.builder.cmpEq(lhs, rhs), .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), .lt => self.builder.cmpLt(lhs, rhs), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), .gt => self.builder.cmpGt(lhs, rhs), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), .and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool), .or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool), .bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), .bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), .bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), .in_op => self.emitError("in_op", bop.lhs.span), }; } /// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=) pub fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref { const lhs_info = self.module.types.get(lhs_ty); const lhs_fields = lhs_info.tuple.fields; switch (bop.op) { .add => { // Tuple concatenation: (a, b) + (c, d) → (a, b, c, d) const rhs_ty = self.inferExprType(bop.rhs); const rhs_fields = if (!rhs_ty.isBuiltin()) blk: { const ri = self.module.types.get(rhs_ty); break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{}; } else &[_]TypeId{}; var all_fields = std.ArrayList(TypeId).empty; defer all_fields.deinit(self.alloc); var all_vals = std.ArrayList(Ref).empty; defer all_vals.deinit(self.alloc); for (lhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; } for (rhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable; } const result_ty = self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, .names = null, } }); const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); }, .mul => { // Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b) const count: usize = switch (bop.rhs.data) { .int_literal => |il| @intCast(@as(u64, @bitCast(il.value))), else => 1, }; var all_fields = std.ArrayList(TypeId).empty; defer all_fields.deinit(self.alloc); var all_vals = std.ArrayList(Ref).empty; defer all_vals.deinit(self.alloc); for (0..count) |_| { for (lhs_fields, 0..) |f, i| { all_fields.append(self.alloc, f) catch unreachable; all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; } } const result_ty = self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, .names = null, } }); const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); }, .eq, .neq => { // Element-wise equality (or single-element tuple vs scalar) const rhs_is_tuple = blk: { const rt = self.inferExprType(bop.rhs); if (!rt.isBuiltin()) { break :blk self.module.types.get(rt) == .tuple; } break :blk false; }; if (!rhs_is_tuple and lhs_fields.len == 1) { // Single-element tuple vs scalar: unwrap and compare const lf = self.builder.structGet(lhs, 0, lhs_fields[0]); const eq = self.builder.cmpEq(lf, rhs); return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq; } var result = self.builder.constBool(true); for (lhs_fields, 0..) |f, i| { const lf = self.builder.structGet(lhs, @intCast(i), f); const rf = self.builder.structGet(rhs, @intCast(i), f); const eq = self.builder.cmpEq(lf, rf); result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool); } return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result; }, .lt, .lte, .gt, .gte => { // Lexicographic comparison return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields); }, else => return self.builder.constInt(0, .s64), } } pub fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref { // Lexicographic comparison using boolean logic. // (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1) // (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1) if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte); const n = fields.len; // Start with the last field using the actual op const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]); const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]); var result = switch (op) { .lt => self.builder.cmpLt(lf_last, rf_last), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), .gt => self.builder.cmpGt(lf_last, rf_last), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), else => unreachable, }; // Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result) if (n > 1) { var i: usize = n - 1; while (i > 0) { i -= 1; const lf = self.builder.structGet(lhs, @intCast(i), fields[i]); const rf = self.builder.structGet(rhs, @intCast(i), fields[i]); const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf); const eq = self.builder.cmpEq(lf, rf); const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool); result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool); } } return result; } pub fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref { // value in (a, b, c) → value == a || value == b || value == c var result = self.builder.constBool(false); for (tuple_info.fields, 0..) |f, i| { const elem = self.builder.structGet(tuple, @intCast(i), f); const eq = self.builder.cmpEq(value, elem); result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool); } return result; } // ── Chained comparison ────────────────────────────────────────── pub fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref { // a < b < c → (a < b) and (b < c) // Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice. if (cc.operands.len < 2 or cc.ops.len == 0) { return self.builder.constBool(true); } var refs = std.ArrayList(Ref).empty; defer refs.deinit(self.alloc); for (cc.operands) |op| { refs.append(self.alloc, self.lowerExpr(op)) catch unreachable; } var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]); var i: usize = 1; while (i < cc.ops.len) : (i += 1) { const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]); result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool); } return result; } pub fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref { return switch (op) { .eq => self.builder.cmpEq(lhs, rhs), .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), .lt => self.builder.cmpLt(lhs, rhs), .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), .gt => self.builder.cmpGt(lhs, rhs), .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), else => self.builder.constBool(false), }; }