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 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; /// Names that must keep external LLVM linkage because the OS loader (not /// sx code) is the caller. Without this they'd default to internal and /// either DCE away or stay hidden from the dynamic symbol table. fn isExportedEntryName(name: []const u8) bool { return std.mem.eql(u8, name, "main") or std.mem.eql(u8, name, "android_main") or std.mem.eql(u8, name, "ANativeActivity_onCreate") or std.mem.eql(u8, name, "JNI_OnLoad"); } // ── Scope ─────────────────────────────────────────────────────────────── const Binding = struct { ref: Ref, ty: TypeId, is_alloca: bool, // true if ref is a pointer that needs load }; const Scope = struct { map: std.StringHashMap(Binding), fn_names: std.StringHashMap([]const u8), // bare name → mangled name for local functions parent: ?*Scope, fn init(alloc: Allocator, parent: ?*Scope) Scope { return .{ .map = std.StringHashMap(Binding).init(alloc), .fn_names = std.StringHashMap([]const u8).init(alloc), .parent = parent, }; } fn deinit(self: *Scope) void { self.map.deinit(); self.fn_names.deinit(); } fn put(self: *Scope, name: []const u8, binding: Binding) void { self.map.put(name, binding) catch unreachable; } fn lookup(self: *const Scope, name: []const u8) ?Binding { if (self.map.get(name)) |b| return b; if (self.parent) |p| return p.lookup(name); return null; } fn lookupFn(self: *const Scope, name: []const u8) ?[]const u8 { if (self.fn_names.get(name)) |mangled| return mangled; if (self.parent) |p| return p.lookupFn(name); return null; } }; // ── Lowering ──────────────────────────────────────────────────────────── pub const Lowering = struct { module: *Module, builder: Builder, alloc: Allocator, scope: ?*Scope = null, break_target: ?BlockId = null, continue_target: ?BlockId = null, block_counter: u32 = 0, comptime_counter: u32 = 0, main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern resolved_root: ?*const Node = null, // full AST root (for building comptime modules) comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions fn_ast_map: std.StringHashMap(*const ast.FnDecl), target_type: ?TypeId = null, // target type for struct/enum literals without explicit names lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered local_fn_counter: u32 = 0, // unique counter for mangling local function names import_flags: std.StringHashMap(bool), // tracks whether each function is imported module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution) import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter) current_source_file: ?[]const u8 = null, // source file of function currently being lowered sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback) type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId) current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch) force_block_value: bool = false, // set by lowerBlockValue to extract if-else values block_terminated: bool = false, // set when constant-folded if emits a return/br into current block defer_stack: std.ArrayList(*const Node) = std.ArrayList(*const Node).empty, // block-scoped defer stack func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this) global_names: std.StringHashMap(GlobalInfo) = std.StringHashMap(GlobalInfo).init(std.heap.page_allocator), // #run global name → GlobalId deferred_type_fns: std.ArrayList([]const u8) = std.ArrayList([]const u8).empty, // functions deferred until all types registered processing_deferred: bool = false, // true when processing deferred functions (prevents re-deferral) struct_template_map: std.StringHashMap(StructTemplate) = std.StringHashMap(StructTemplate).init(std.heap.page_allocator), // generic struct name → template struct_defaults_map: std.StringHashMap([]const ?*const Node) = std.StringHashMap([]const ?*const Node).init(std.heap.page_allocator), // struct name → field defaults struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)) = std.StringHashMap(std.StringHashMap(TypeId)).init(std.heap.page_allocator), // mangled struct name → type param bindings struct_instance_template: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // mangled struct name → template name comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value) protocol_decl_map: std.StringHashMap(ProtocolDeclInfo) = std.StringHashMap(ProtocolDeclInfo).init(std.heap.page_allocator), // protocol name → protocol info protocol_ast_map: std.StringHashMap(*const ast.ProtocolDecl) = std.StringHashMap(*const ast.ProtocolDecl).init(std.heap.page_allocator), // protocol name → AST node protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00\x00" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap) struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2) foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames type_alias_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // type alias name → target TypeId ufcs_alias_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // UFCS alias name → target function name target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if) comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH) diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx pub const ComptimeValue = union(enum) { int_val: i64, enum_tag: struct { ty: TypeId, tag: u32 }, }; const StructConstInfo = struct { value: *const Node, ty: ?TypeId, // null if no type annotation (inferred) }; const ModuleConstInfo = struct { value: *const Node, ty: TypeId, }; const ProtocolDeclInfo = struct { name: []const u8, is_inline: bool, methods: []const ProtocolMethodInfo, }; const ProtocolMethodInfo = struct { name: []const u8, param_types: []const TypeId, // excluding self ret_type: TypeId, }; /// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`). /// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled). /// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics. const ParamImplEntry = struct { methods: []const *const ast.FnDecl, source_ty: TypeId, target_args: []const TypeId, defining_module: []const u8, span: ast.Span, }; /// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports) const StructTemplate = struct { name: []const u8, type_params: []const TemplateParam, field_names: []const []const u8, field_type_nodes: []const *const Node, // raw AST pointers — must be copied from heap nodes }; const TemplateParam = struct { name: []const u8, is_type_param: bool, // true for $T: Type, false for $N: u32 }; const GlobalInfo = struct { id: inst_mod.GlobalId, ty: TypeId }; pub fn init(module: *Module) Lowering { return .{ .module = module, .builder = Builder.init(module), .alloc = module.alloc, .fn_ast_map = std.StringHashMap(*const ast.FnDecl).init(module.alloc), .lowered_functions = std.StringHashMap(void).init(module.alloc), .import_flags = std.StringHashMap(bool).init(module.alloc), .global_names = std.StringHashMap(GlobalInfo).init(module.alloc), }; } // ── Public entry point ────────────────────────────────────────── /// Lower all top-level declarations from a root node. /// Pass 1: Scan all declarations (register ASTs, types, extern stubs). /// Pass 2: Lower only `main` (everything else is lowered lazily on demand). pub fn lowerRoot(self: *Lowering, root: *const Node) void { const decls = switch (root.data) { .root => |r| r.decls, else => return, }; // Pass 1: scan — register all function ASTs, struct types, extern stubs self.scanDecls(decls); // Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config self.injectComptimeConstants(); // Pass 2: lower main (and comptime side-effects) self.lowerMainAndComptime(decls); // Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered self.lowerDeferredTypeFns(); // Pass 4: target-specific entry-point sanity checks self.checkRequiredEntryPoints(); } /// On Android, the OS loader calls `android_main(app: *void)` — there's /// no `main()` invocation from the system. If the user hasn't defined /// `android_main`, native_app_glue can't find it at runtime and the /// activity dies with an unhelpful "library doesn't export /// `android_main`" error after the .so is loaded. Catch this at /// compile time with a clear hint pointing at the platform module /// that provides the helper. fn checkRequiredEntryPoints(self: *Lowering) void { const tc = self.target_config orelse return; if (!tc.isAndroid()) return; const wanted = self.module.types.internString("android_main"); var has_defn = false; for (self.module.functions.items) |func| { if (func.name != wanted) continue; if (func.is_extern) continue; if (func.blocks.items.len == 0) continue; has_defn = true; break; } if (has_defn) return; if (self.diagnostics) |diags| { diags.addFmt(.err, null, "target is Android but no `android_main` function defined. " ++ "The OS calls `android_main(app: *void)` as the entry point — " ++ "add it to your main.sx (it can be a 3-line trampoline that " ++ "calls `sx_android_bootstrap(app)` then `main()` — see " ++ "`examples/99-android-egl-clear.sx`).", .{}); } } /// Inject compile-time constants from target_config into comptime_constants. /// Called after scanDecls so that enum types (OperatingSystem, Architecture) are registered. fn injectComptimeConstants(self: *Lowering) void { const tc = self.target_config orelse return; // OS: OperatingSystem enum { macos; linux; windows; wasm; unknown; } const os_name_id = self.module.types.internString("OperatingSystem"); if (self.module.types.findByName(os_name_id)) |os_ty| { const os_info = self.module.types.get(os_ty); if (os_info == .@"enum") { const tag: u32 = if (tc.isWasm()) self.findVariantIndex(os_info.@"enum".variants, "wasm") else if (tc.isWindows()) self.findVariantIndex(os_info.@"enum".variants, "windows") else if (tc.isAndroid()) self.findVariantIndex(os_info.@"enum".variants, "android") else if (tc.isLinux()) self.findVariantIndex(os_info.@"enum".variants, "linux") else if (tc.isIOS()) self.findVariantIndex(os_info.@"enum".variants, "ios") else if (tc.isMacOS()) self.findVariantIndex(os_info.@"enum".variants, "macos") else self.findVariantIndex(os_info.@"enum".variants, "unknown"); self.comptime_constants.put("OS", .{ .enum_tag = .{ .ty = os_ty, .tag = tag } }) catch {}; } } // ARCH: Architecture enum { aarch64; x86_64; wasm32; wasm64; unknown; } const arch_name_id = self.module.types.internString("Architecture"); if (self.module.types.findByName(arch_name_id)) |arch_ty| { const arch_info = self.module.types.get(arch_ty); if (arch_info == .@"enum") { const tag: u32 = if (tc.isWasm32()) self.findVariantIndex(arch_info.@"enum".variants, "wasm32") else if (tc.isWasm64()) self.findVariantIndex(arch_info.@"enum".variants, "wasm64") else if (tc.isAarch64()) self.findVariantIndex(arch_info.@"enum".variants, "aarch64") else if (tc.isX86_64()) self.findVariantIndex(arch_info.@"enum".variants, "x86_64") else self.findVariantIndex(arch_info.@"enum".variants, "unknown"); self.comptime_constants.put("ARCH", .{ .enum_tag = .{ .ty = arch_ty, .tag = tag } }) catch {}; } } // POINTER_SIZE: s64 (4 for wasm32, 8 for wasm64 and other 64-bit targets) const ptr_size: i64 = if (tc.isWasm32()) 4 else 8; self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {}; } fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 { const name_id = self.module.types.internString(name); for (variants, 0..) |v, i| { if (v == name_id) return @intCast(i); } return 0; // fallback to first variant } /// Lower functions that were deferred because they use type-category matching. /// At this point, main is fully lowered and all types are in the TypeTable. fn lowerDeferredTypeFns(self: *Lowering) void { if (self.deferred_type_fns.items.len == 0) return; self.processing_deferred = true; for (self.deferred_type_fns.items) |name| { self.lazyLowerFunction(name); } self.processing_deferred = false; } /// Lower a list of top-level declarations (used by irComptimeEval — non-lazy path). /// This preserves the old behavior for comptime evaluation contexts. pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { self.current_source_file = decl.source_file; const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else false; switch (decl.data) { .fn_decl => |fd| { self.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; self.lowerFunction(&fd, fd.name, is_imported); }, .const_decl => |cd| { if (cd.value.data == .fn_decl) { self.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {}; self.lowerFunction(&cd.value.data.fn_decl, cd.name, is_imported); } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .union_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } }, .comptime_expr => |ct| { self.lowerComptimeSideEffect(ct.expr); }, .struct_decl => |sd| { self.registerStructDecl(&sd); }, .enum_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types); }, .union_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types); }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { self.registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .namespace_decl => |ns| { if (self.main_file != null) { self.lowerDecls(ns.decls); } }, else => {}, } } } /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. fn scanDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { self.current_source_file = decl.source_file; const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else false; switch (decl.data) { .fn_decl => |fd| { self.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; self.import_flags.put(fd.name, is_imported) catch {}; // Declare extern stub for all functions (bodies lowered lazily) self.declareFunction(&fd, fd.name); }, .const_decl => |cd| { if (cd.value.data == .fn_decl) { self.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {}; self.import_flags.put(cd.name, is_imported) catch {}; self.declareFunction(&cd.value.data.fn_decl, cd.name); } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { // Register enum/tagged-union types in the type table _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .union_decl) { // Register plain union types in the type table _ = type_bridge.resolveAstType(cd.value, &self.module.types); } else if (cd.value.data == .type_expr) { // Type alias: MyFloat :: f64; → register MyFloat as alias for f64 const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types); self.type_alias_map.put(cd.name, target_ty) catch {}; } // Handle generic struct instantiation: Vec3 :: Vec(3, f32) // Parser produces a .call node for these (not parameterized_type_expr) if (cd.value.data == .call) { const call_data = &cd.value.data.call; const callee_name = switch (call_data.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => "", }; if (callee_name.len > 0) { if (self.struct_template_map.getPtr(callee_name)) |tmpl| { const inst_id = self.instantiateGenericStruct(tmpl, call_data.args); // Register under the alias name const alias_name_id = self.module.types.internString(cd.name); const inst_info = self.module.types.get(inst_id); if (inst_info == .@"struct") { const alias_info: types.TypeInfo = .{ .@"struct" = .{ .name = alias_name_id, .fields = inst_info.@"struct".fields, } }; const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); self.module.types.update(alias_id, alias_info); } } else if (self.fn_ast_map.get(callee_name)) |fd| { // Type-returning function: Foo :: Complex(u32) if (fd.type_params.len > 0) { if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| { self.type_alias_map.put(cd.name, result_ty) catch {}; } } } } } else if (cd.value.data == .parameterized_type_expr) { // Type alias for generic struct (from type_bridge path) const pt = &cd.value.data.parameterized_type_expr; const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; if (self.struct_template_map.getPtr(base_name)) |tmpl| { const inst_id = self.instantiateGenericStruct(tmpl, pt.args); const alias_name_id = self.module.types.internString(cd.name); const inst_info = self.module.types.get(inst_id); if (inst_info == .@"struct") { const alias_info: types.TypeInfo = .{ .@"struct" = .{ .name = alias_name_id, .fields = inst_info.@"struct".fields, } }; const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); self.module.types.update(alias_id, alias_info); } } } // comptime_expr handled in Pass 2 // Simple value constants with type annotation (e.g. AF_INET :s32: 2) if (cd.type_annotation != null) { switch (cd.value.data) { .int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal => { const ty = self.resolveType(cd.type_annotation); self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; }, else => {}, } } else { // Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;) switch (cd.value.data) { .string_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .string }) catch {}, .int_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {}, .float_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {}, .bool_literal => self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .bool }) catch {}, // Complex constant expressions (e.g. COLOR_WHITE :: Color.{ r = 255, ... }) .struct_literal => { const inferred_ty = self.inferExprType(cd.value); self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = inferred_ty }) catch {}; }, else => {}, } } }, .struct_decl => |sd| { self.registerStructDecl(&sd); }, .enum_decl => { // Register enum/tagged-union types in the type table _ = type_bridge.resolveAstType(decl, &self.module.types); }, .union_decl => { // Register plain union types in the type table _ = type_bridge.resolveAstType(decl, &self.module.types); }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { self.registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .namespace_decl => |ns| { if (self.main_file != null) { self.scanDecls(ns.decls); } }, .ufcs_alias => |ua| { self.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, .var_decl => |vd| { // Top-level mutable global (e.g., `context : Context = ---;`) // Use self.resolveType so type aliases like `Handle :: u32;` resolve // to their target type (not a synthetic empty struct). const var_ty = self.resolveType(vd.type_annotation); // Foreign globals reference a symbol defined in libSystem etc. // (`_NSConcreteStackBlock : *void #foreign;`). The C symbol // name is the optional override or the sx name itself. const sym_name = vd.foreign_name orelse vd.name; const name_id = self.module.types.internString(sym_name); const init_val: ?inst_mod.ConstantValue = if (vd.is_foreign) null else if (vd.value) |v| switch (v.data) { .undef_literal => .zeroinit, .int_literal => |il| .{ .int = il.value }, .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .array_literal => |al| self.constArrayLiteral(al.elements), .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), else => null, } else null; const gid = self.module.addGlobal(.{ .name = name_id, .ty = var_ty, .init_val = init_val, .is_const = false, .is_extern = vd.is_foreign, }); self.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {}; }, else => {}, } } } /// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate. /// Returns null if any element is not a compile-time constant. fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue { const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; for (elements, 0..) |elem, i| { vals[i] = self.constExprValue(elem) orelse return null; } return .{ .aggregate = vals }; } /// Try to convert a single AST expression into a compile-time ConstantValue. /// Returns null if the expression is not constant-foldable here. fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue { return switch (expr.data) { .int_literal => |il| .{ .int = il.value }, .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .undef_literal => .zeroinit, .unary_op => |uo| switch (uo.op) { .negate => switch (uo.operand.data) { .int_literal => |il| .{ .int = -il.value }, .float_literal => |fl| .{ .float = -fl.value }, else => null, }, else => null, }, .array_literal => |al| self.constArrayLiteral(al.elements), else => null, }; } /// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the /// struct's fields in declaration order, filling missing fields from the struct's /// field defaults. Returns null if any value is not constant-foldable. fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue { if (ty.isBuiltin()) return null; const ti = self.module.types.get(ty); if (ti != .@"struct") return null; const struct_fields = ti.@"struct".fields; const struct_name = self.module.types.getString(ti.@"struct".name); const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{}; const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null; for (struct_fields, 0..) |sf, fi| { const sf_name = self.module.types.getString(sf.name); const init_expr: ?*const Node = blk: { if (has_names) { for (sl.field_inits) |init_pair| { if (init_pair.name) |n| { if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value; } } } else if (fi < sl.field_inits.len) { break :blk sl.field_inits[fi].value; } if (fi < field_defaults.len) break :blk field_defaults[fi]; break :blk null; }; if (init_expr) |e| { vals[fi] = self.constExprValue(e) orelse return null; } else { vals[fi] = .zeroinit; } } return .{ .aggregate = vals }; } /// Pass 2: Lower main function body and comptime side-effects. fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { switch (decl.data) { .const_decl => |cd| { if (cd.value.data == .fn_decl) { if (isExportedEntryName(cd.name)) { self.lazyLowerFunction(cd.name); } } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } }, .fn_decl => |fd| { if (isExportedEntryName(fd.name)) { self.lazyLowerFunction(fd.name); } }, .comptime_expr => |ct| { self.lowerComptimeSideEffect(ct.expr); }, .namespace_decl => |ns| { if (self.main_file != null) { self.lowerMainAndComptime(ns.decls); } }, else => {}, } } } /// Declare a function as an extern stub (signature only, no body). fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { // Skip generic templates — they're monomorphized on demand, not declared as extern if (fd.type_params.len > 0) return; const ret_ty = self.resolveReturnType(fd); var params = std.ArrayList(Function.Param).empty; for (fd.params) |p| { const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default; // For #foreign with C name override, declare under C name and map sx name → C name if (fd.body.data == .foreign_expr) { const fe = fd.body.data.foreign_expr; if (fe.c_name) |c_name| { const c_name_id = self.module.types.internString(c_name); const fid = self.builder.declareExtern(c_name_id, params.items, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = cc; func.source_file = self.current_source_file; self.foreign_name_map.put(name, c_name) catch {}; return; } } const name_id = self.module.types.internString(name); const fid = self.builder.declareExtern(name_id, params.items, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = cc; func.source_file = self.current_source_file; } /// Check if a C-imported function is visible from the current source file. /// Returns true for non-C functions (always visible) or if no scoping info available. fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool { const fd = self.fn_ast_map.get(fn_name) orelse return true; // Only restrict C import fn_decls: foreign_expr with no library_ref if (fd.body.data != .foreign_expr) return true; if (fd.body.data.foreign_expr.library_ref != null) return true; // It's a C import fn_decl — check module scope const scopes = self.module_scopes orelse return true; const source = self.current_source_file orelse return true; const scope = scopes.get(source) orelse return true; return scope.contains(fn_name); } /// Lazily lower a function body on demand. Called when lowerCall can't find /// the function and it exists in fn_ast_map. fn lazyLowerFunction(self: *Lowering, name: []const u8) void { // Already lowered? if (self.lowered_functions.contains(name)) return; // No AST? (builtins, foreign functions, or imported functions not in this file) const fd = self.fn_ast_map.get(name) orelse return; // Check builtin/foreign/generic — these stay as extern stubs if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) return; if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13) // Defer functions with type-category matches until all types are registered. // any_to_string uses `if type == { case slice: ... }` which compiles a switch // with type tags from resolveTypeCategoryTags. This must happen AFTER main is // fully lowered so all types ([]s32, List__s32, etc.) are in the TypeTable. if (!self.processing_deferred and std.mem.eql(u8, name, "any_to_string")) { self.deferred_type_fns.append(self.alloc, name) catch {}; return; } // Mark as lowered before lowering (prevents infinite recursion) self.lowered_functions.put(name, {}) catch {}; // Save builder state (same pattern as lambda lowering) const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_defer_base = self.func_defer_base; const saved_block_terminated = self.block_terminated; const saved_force_block_value = self.force_block_value; const saved_source_file = self.current_source_file; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; self.force_block_value = false; // Find the existing extern stub and replace it with a full body const name_id = self.module.types.internString(name); const ret_ty = self.resolveReturnType(fd); // Look up the existing function declaration (from scanDecls) var func_id: ?FuncId = null; for (self.module.functions.items, 0..) |func, i| { if (func.name == name_id) { func_id = FuncId.fromIndex(@intCast(i)); break; } } if (func_id == null) { // Function not yet declared — create it fresh via lowerFunction self.lowerFunction(fd, name, false); // Restore builder state self.current_source_file = saved_source_file; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return; } if (func_id) |fid| { // Re-use the existing function slot — switch builder to it self.builder.func = fid; const func = &self.module.functions.items[@intFromEnum(fid)]; self.current_source_file = func.source_file; if (!func.is_extern) { // Already promoted (e.g., via lowerComptimeDeps) — skip self.current_source_file = saved_source_file; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return; } func.is_extern = false; // promote from extern stub to real function func.linkage = if (isExportedEntryName(name)) .external else .internal; if (fd.call_conv == .c) func.call_conv = .c; // Set inst_counter to param count (params occupy refs 0..N-1) std.debug.assert(func.params.len == fd.params.len); // AST and IR param counts must match self.builder.inst_counter = @intCast(func.params.len); // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope and bind params var scope = Scope.init(self.alloc, null); defer scope.deinit(); self.scope = &scope; for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(@intCast(i)); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Auto-initialize context with default GPA at the start of main() if (std.mem.eql(u8, name, "main")) { self.emitDefaultContextInit(); } // Lower the function body (set target_type to return type for implicit returns) const saved_target = self.target_type; self.target_type = if (ret_ty != .void) ret_ty else null; if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { // Check if the body value is void (e.g., last stmt is a void call) const val_ty = self.builder.getRefType(val); if (val_ty == .void) { self.ensureTerminator(ret_ty); } else { const coerced = self.coerceToType(val, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(fd.body); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } // Restore builder state self.current_source_file = saved_source_file; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.force_block_value = saved_force_block_value; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } /// Lower a single function declaration. pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, is_imported: bool) void { const name_id = self.module.types.internString(name); const ret_ty = self.resolveReturnType(fd); // Build param list var params = std.ArrayList(Function.Param).empty; for (fd.params) |p| { const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } // Check if the function body is a builtin or foreign declaration (no body needed) if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) { // Already declared by scanDecls/declareFunction (which handles #foreign renames) return; } // Skip generic functions (they have type parameters and are templates, not concrete) if (fd.type_params.len > 0) { _ = self.builder.declareExtern(name_id, params.items, ret_ty); return; } // Imported functions: declare as extern (don't lower bodies from other files) if (is_imported) { _ = self.builder.declareExtern(name_id, params.items, ret_ty); return; } const func_id = self.builder.beginFunction( name_id, params.items, ret_ty, ); _ = func_id; // Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly, // matches C `static`). isExportedEntryName lists the names the OS // loader calls — `main`, Android NativeActivity hooks — which must // stay externally visible. if (isExportedEntryName(name)) { self.builder.currentFunc().linkage = .external; } // Set calling convention if (fd.call_conv == .c) { self.builder.currentFunc().call_conv = .c; } // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope and bind params var scope = Scope.init(self.alloc, self.scope); defer scope.deinit(); self.scope = &scope; defer self.scope = scope.parent; for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); // Allocate stack slot for param, store initial value. // Refs 0..N-1 are reserved for function parameters by beginFunction. const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(@intCast(i)); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Lower the function body, capturing the last expression's value for implicit return const saved_target = self.target_type; self.target_type = if (ret_ty != .void) ret_ty else null; if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { // Check if body value is void (e.g., last stmt is a void call) const val_ty = self.builder.getRefType(val); if (val_ty == .void) { self.ensureTerminator(ret_ty); } else { const coerced = self.coerceToType(val, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(fd.body); self.ensureTerminator(ret_ty); } self.target_type = saved_target; self.builder.finalize(); } // ── Statement lowering ────────────────────────────────────────── fn lowerBlock(self: *Lowering, node: *const Node) void { switch (node.data) { .block => |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(); } for (blk.stmts) |stmt| { if (self.block_terminated) break; self.lowerStmt(stmt); } }, else => { // Single expression as body (arrow functions) self.lowerStmt(node); }, } } /// Lower an `inline if` branch — block body emits statements, expression returns value. fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref { if (node.data == .block) { self.lowerBlock(node); // A `return` inside the branch terminates the current LLVM block; propagate // that up so the enclosing block lowering stops emitting fall-through. if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } return self.builder.constInt(0, .void); } return self.lowerExpr(node); } /// Lower a block and return the last expression's value (for implicit returns). fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { // Set force_block_value so nested if-else expressions produce values const saved = self.force_block_value; self.force_block_value = true; defer self.force_block_value = saved; switch (node.data) { .block => |blk| { if (blk.stmts.len == 0) return null; // 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(); } // Lower all statements except the last normally self.force_block_value = false; // don't force for non-last statements for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { if (self.block_terminated) return null; self.lowerStmt(stmt); } if (self.block_terminated) return null; // Last statement: if it's an expression, return its value self.force_block_value = true; const last = blk.stmts[blk.stmts.len - 1]; return self.tryLowerAsExpr(last); }, else => { // Single expression as body (arrow functions) return self.tryLowerAsExpr(node); }, } } /// Try to lower a node as an expression, returning its value. /// Statement nodes are lowered as statements (returning null). fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { return switch (node.data) { .var_decl, .const_decl, .fn_decl, .return_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => { self.lowerStmt(node); return null; }, else => self.lowerExpr(node), }; } fn lowerStmt(self: *Lowering, node: *const Node) void { switch (node.data) { .var_decl => |vd| self.lowerVarDecl(&vd), .const_decl => |cd| self.lowerConstDecl(&cd), .fn_decl => |fd| self.lowerLocalFnDecl(&fd), .return_stmt => |rs| self.lowerReturn(&rs), .assignment => |asgn| self.lowerAssignment(&asgn), .defer_stmt => |ds| self.lowerDefer(&ds), .push_stmt => |ps| self.lowerPush(&ps), .multi_assign => |ma| self.lowerMultiAssign(&ma), .destructure_decl => |dd| self.lowerDestructureDecl(&dd), .insert_expr => |ins| self.lowerInsertExpr(ins.expr), .block => self.lowerBlock(node), // Block-local type declarations .struct_decl => |sd| self.registerStructDecl(&sd), .enum_decl, .union_decl => { _ = type_bridge.resolveAstType(node, &self.module.types); }, .ufcs_alias => |ua| { self.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, // Expression statement else => { _ = self.lowerExpr(node); }, } } fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void { if (vd.type_annotation != null) { // Explicit type annotation — resolve type first, then lower value const ty = self.resolveType(vd.type_annotation); const slot = self.builder.alloca(ty); if (vd.value) |val| { // = --- (undef_literal) on tuple types: zero-initialize if (val.data == .undef_literal and !ty.isBuiltin()) { const ti = self.module.types.get(ty); if (ti == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (ti.tuple.fields) |f| { field_vals.append(self.alloc, self.builder.constInt(0, f)) catch unreachable; } const zero = self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); self.builder.store(slot, zero); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } return; } } const saved_target = self.target_type; const saved_fbv = self.force_block_value; self.target_type = ty; self.force_block_value = true; var ref = self.lowerExpr(val); self.target_type = saved_target; self.force_block_value = saved_fbv; // If target is optional and value isn't null, wrap with optional_wrap if (!ty.isBuiltin()) { const ty_info = self.module.types.get(ty); if (ty_info == .optional and val.data != .null_literal) { ref = self.builder.optionalWrap(ref, ty); } else if (ty_info == .slice) { // Array → slice promotion: if value is an array, convert to slice const ref_ty = self.builder.getRefType(ref); if (!ref_ty.isBuiltin()) { const ref_info = self.module.types.get(ref_ty); if (ref_info == .array) { ref = self.builder.emit(.{ .array_to_slice = .{ .operand = ref } }, ty); } } } else if (self.getProtocolInfo(ty) != null) { // Auto type erasure: concrete → protocol const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty) { ref = self.buildProtocolErasure(ref, val, ref_ty, ty); } } } // Coerce value to match target type (e.g. u8 → s64 widening) { const ref_ty = self.builder.getRefType(ref); if (ref_ty != ty and ref_ty != .void and ty != .void) { ref = self.coerceToType(ref, ref_ty, ty); } } self.builder.store(slot, ref); } else { // No value: zero-initialize or apply struct defaults const zero = self.buildDefaultValue(ty); self.builder.store(slot, zero); } if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else if (vd.value) |val| { // No type annotation — lower expr first, then get type from result. // This is critical for generic calls where the return type is only // known after monomorphization. const saved_fbv = self.force_block_value; self.force_block_value = true; const ref = self.lowerExpr(val); self.force_block_value = saved_fbv; const ty = self.builder.getRefType(ref); const slot = self.builder.alloca(ty); self.builder.store(slot, ref); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } else { const ty = TypeId.s64; const slot = self.builder.alloca(ty); self.builder.store(slot, self.zeroValue(ty)); if (self.scope) |scope| { scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true }); } } } /// Handle a bare fn_decl node as a local function declaration. /// The parser produces `fn_decl` (not `const_decl`) for `name :: (params) -> T { body }`. fn lowerLocalFnDecl(self: *Lowering, fd: *const ast.FnDecl) void { // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope) |scope| blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ fd.name, self.local_fn_counter }) catch fd.name; self.local_fn_counter += 1; scope.fn_names.put(fd.name, mangled) catch {}; break :blk mangled; } else fd.name; self.fn_ast_map.put(name, fd) catch {}; self.lazyLowerFunction(name); } fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void { // Handle local function declarations: fx :: (s:s3) -> s3 { ... } if (cd.value.data == .fn_decl) { const fd = &cd.value.data.fn_decl; // Use mangled name for local functions to support block-scoped shadowing const name = if (self.scope != null) blk: { const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ cd.name, self.local_fn_counter }) catch cd.name; self.local_fn_counter += 1; // Register the bare→mangled mapping in the current scope if (self.scope) |scope| { scope.fn_names.put(cd.name, mangled) catch {}; } break :blk mangled; } else cd.name; // Register in fn_ast_map so it can be resolved by lowerCall self.fn_ast_map.put(name, fd) catch {}; // Lower the function body (saves/restores builder state) self.lazyLowerFunction(name); return; } // Handle local type declarations: MyType :: struct/union/enum { ... } if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); return; } if (cd.value.data == .enum_decl or cd.value.data == .union_decl) { _ = type_bridge.resolveAstType(cd.value, &self.module.types); return; } const ref = self.lowerExpr(cd.value); // If there's an explicit type annotation, use it. Otherwise, infer from the expression. const ty = if (cd.type_annotation != null) self.resolveType(cd.type_annotation) else self.builder.getRefType(ref); if (self.scope) |scope| { scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false }); } } fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void { // Set target_type to function return type so null_literal etc. get the right type const old_target = self.target_type; const ret_ty_for_target = if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.s64; if (ret_ty_for_target != .void) self.target_type = ret_ty_for_target; // Evaluate return value first (before defers) const ret_val = if (rs.value) |val| self.lowerExpr(val) else null; self.target_type = old_target; // Emit ALL pending defers for THIS function in LIFO order before the return self.emitBlockDefers(self.func_defer_base); if (ret_val) |ref| { const ret_ty = if (self.builder.func) |fid| self.module.functions.items[@intFromEnum(fid)].ret else TypeId.s64; if (ret_ty == .void) { // Void function — just return void (the value expression was evaluated for side effects) self.builder.retVoid(); } else { // Coerce return value to match function return type (e.g., ?s32 → s32) const val_ty = self.builder.getRefType(ref); const coerced = self.coerceToType(ref, val_ty, ret_ty); self.builder.ret(coerced, ret_ty); } } else { self.builder.retVoid(); } } fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { // Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.) const old_target = self.target_type; if (asgn.target.data == .identifier) { var found_local = false; if (self.scope) |scope| { if (scope.lookup(asgn.target.data.identifier.name)) |binding| { self.target_type = binding.ty; found_local = true; } } if (!found_local) { if (self.global_names.get(asgn.target.data.identifier.name)) |gi| { self.target_type = gi.ty; } } } else if (asgn.target.data == .index_expr) { // For array[i] = val, set target_type to the element type const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object)); if (elem_ty != .void) self.target_type = elem_ty; } else if (asgn.target.data == .field_access) { // For obj.field = val, set target_type to the field's type so RHS // sub-expressions (enum/struct literals, branch arms, xx casts) can // resolve against it. Skipped for forms that would forward the type // unchanged into method-call arg slots (`resolveCallParamTypes` can't // override target_type per-arg). const needs_target = switch (asgn.value.data) { .enum_literal, .struct_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true, .call => |vc| vc.callee.data == .enum_literal, else => false, }; if (needs_target) { const fa = asgn.target.data.field_access; const obj_ty_raw = self.inferExprType(fa.object); const obj_ty = if (!obj_ty_raw.isBuiltin()) blk: { const pinfo = self.module.types.get(obj_ty_raw); break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw; } else obj_ty_raw; if (!obj_ty.isBuiltin()) { const field_name_id = self.module.types.internString(fa.field); const struct_fields = self.getStructFields(obj_ty); for (struct_fields) |f| { if (f.name == field_name_id) { self.target_type = f.ty; break; } } } } } const val = self.lowerExpr(asgn.value); self.target_type = old_target; switch (asgn.target.data) { .identifier => |id| { var handled = false; if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { handled = true; if (asgn.op == .assign) { // Coerce value to match binding type (e.g., f32 → ?f32, concrete → protocol) var store_val = val; const val_ty = self.builder.getRefType(val); if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) { store_val = self.coerceToType(val, val_ty, binding.ty); } self.builder.store(binding.ref, store_val); } else { // Compound assignment: load, op, store const loaded = self.builder.load(binding.ref, binding.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, binding.ty); self.builder.store(binding.ref, result); } } } } // Fallback: global variable assignment if (!handled) { if (self.global_names.get(id.name)) |gi| { if (asgn.op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) self.coerceToType(val, val_ty, gi.ty) else val; self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); } else { // Compound assignment: load current value, apply op, store back const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty); const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty); self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void); } } } }, .field_access => |fa| { var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref: if the object is a pointer field from a non-identifier // (i.e., result of structGep on a pointer slot), load the pointer value. if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const pinfo = self.module.types.get(obj_ty); if (pinfo == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = pinfo.pointer.pointee; } } // Special .len/.ptr handling only for slices, strings, arrays — NOT structs const is_special_container = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { const obj_info = self.module.types.get(obj_ty); break :blk obj_info == .slice or obj_info == .array or obj_info == .vector; } else false); if (is_special_container and std.mem.eql(u8, fa.field, "len")) { const gep = self.builder.structGepTyped(obj_ptr, 1, .s64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .s64); } else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) { const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); self.storeOrCompound(gep, val, asgn.op, .s64); } else { const field_name_id = self.module.types.internString(fa.field); // Check if this is a union field assignment if (!obj_ty.isBuiltin()) { const type_info = self.module.types.get(obj_ty); const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (union_fields) |fields| { for (fields, 0..) |f, i| { if (f.name == field_name_id) { const gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, f.ty); self.storeOrCompound(gep, coerced, asgn.op, f.ty); return; } // 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, 0..) |sf, si| { if (sf.name == field_name_id) { // GEP into union payload area, then into the struct field const union_gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty)); const field_gep = self.builder.structGepTyped(union_gep, @intCast(si), sf.ty, f.ty); const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, sf.ty); self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty); return; } } } } } } } const struct_fields = self.getStructFields(obj_ty); var field_idx: u32 = 0; var field_ty: TypeId = .s64; for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { field_idx = @intCast(i); field_ty = f.ty; break; } } // Wrap in ptrTo so the store handler sees *field_ty (consistent // with index_gep which uses ptrTo(elem_ty)). Without this, a // [*]BigNode field makes the store handler extract BigNode as the // target type, storing element-sized bytes instead of a pointer. const gep_ty = self.module.types.ptrTo(field_ty); const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty); // Coerce value to field type — use the lowered value's actual type // (not inferExprType, which can re-read target_type after restore). const src_ty = self.builder.getRefType(val); const coerced = self.coerceToType(val, src_ty, field_ty); self.storeOrCompound(gep, coerced, asgn.op, field_ty); } }, .index_expr => |ie| { 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 fixed-size array assignment targets, use the alloca pointer directly // so that the store modifies the original variable (not a loaded copy). const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { // Array alloca: single-index GEP with element stride const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else if (is_array) { // Array in a struct field or other composite: get pointer to array in-place const obj_ptr = self.lowerExprAsPtr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj_ptr, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } else { // Pointer/slice: load the pointer value and GEP const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.storeOrCompound(gep, val, asgn.op, elem_ty); } }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); if (asgn.op == .assign) { const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); } else { const pointee_ty = self.inferExprType(de.operand); const elem_ty = blk: { if (!pointee_ty.isBuiltin()) { const info = self.module.types.get(pointee_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk pointee_ty; }; self.storeOrCompound(ptr, val, asgn.op, elem_ty); } }, else => { _ = self.emitError("assignment_target", asgn.target.span); }, } } /// Get the pointer (alloca ref) for an lvalue expression, without loading. fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { switch (node.data) { .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { // If the variable IS a pointer (e.g., p: *Vec2), load it // to get the actual pointer value for GEP/store operations if (!binding.ty.isBuiltin()) { const info = self.module.types.get(binding.ty); if (info == .pointer) { return self.builder.load(binding.ref, binding.ty); } } return binding.ref; } } } }, .field_access => |fa| { var obj_ptr = self.lowerExprAsPtr(fa.object); var obj_ty = self.inferExprType(fa.object); // Auto-deref for chained pointer field access: // When fa.object is a field_access or index_expr, lowerExprAsPtr returns // a structGep/pointer to the slot. If the slot holds a pointer type, // we need to load the pointer value before GEPing into the pointee struct. // (Identifiers are already loaded by the identifier handler in lowerExprAsPtr.) if (fa.object.data != .identifier and !obj_ty.isBuiltin()) { const info = self.module.types.get(obj_ty); if (info == .pointer) { obj_ptr = self.builder.load(obj_ptr, obj_ty); obj_ty = info.pointer.pointee; } } const struct_fields = self.getStructFields(obj_ty); const field_name_id = self.module.types.internString(fa.field); for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { return self.builder.structGepTyped(obj_ptr, @intCast(i), f.ty, obj_ty); } } return self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty); }, .index_expr => |ie| { 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 fixed-size arrays, use the alloca so GEP addresses the original memory 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); return self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); }, .deref_expr => |de| { return self.lowerExpr(de.operand); }, else => {}, } // Fallback: lower as expression (may produce a value, not pointer) return self.lowerExpr(node); } /// Store a value to a GEP, handling both plain and compound assignment. fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void { if (op == .assign) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != ty and val_ty != .void and ty != .void) self.coerceToType(val, val_ty, ty) else val; self.builder.store(gep, store_val); } else { const loaded = self.builder.load(gep, ty); const result = self.emitCompoundOp(loaded, val, op, ty); self.builder.store(gep, result); } } fn emitCompoundOp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.Assignment.Op, ty: TypeId) Ref { return switch (op) { .add_assign => self.builder.add(lhs, rhs, ty), .sub_assign => self.builder.sub(lhs, rhs, ty), .mul_assign => self.builder.mul(lhs, rhs, ty), .div_assign => self.builder.div(lhs, rhs, ty), .mod_assign => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), .and_assign => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), .or_assign => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), .xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), else => self.emitError("compound_assign", null), }; } // ── Expression lowering ───────────────────────────────────────── fn lowerExpr(self: *Lowering, node: *const Node) Ref { return switch (node.data) { .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); }, .null_literal => self.builder.constNull(self.target_type orelse .void), .undef_literal => self.builder.constUndef(self.target_type orelse .void), .identifier => |id| blk: { 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), } } // Check globals (#run constants) if (self.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.module_const_map.get(id.name)) |ci| { break :blk self.emitModuleConst(ci); } // 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.fn_ast_map.contains(eff_fn_name)) { // Type-as-value: if target is Any (Type variable), produce a type name string if (self.target_type == .any) { const fd = self.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); } if (!self.lowered_functions.contains(eff_fn_name)) { self.lazyLowerFunction(eff_fn_name); } if (self.resolveFuncByName(eff_fn_name)) |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); } } } break :blk self.builder.emit(.{ .func_ref = fid }, .s64); } } // Type-as-value: if target is Any (Type context), produce a type name string if (self.target_type == .any) { const sid = self.module.types.internString(id.name); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // Type-as-value: known type name used where a value is expected (e.g. cast(s64, val)) // The type arg is lowered but unused — the caller resolves from AST. if (self.isKnownTypeName(id.name)) { break :blk self.emitPlaceholder(id.name); } // Unknown identifier break :blk self.emitError(id.name, node.span); }, .binary_op => |bop| self.lowerBinaryOp(&bop), .unary_op => |uop| blk: { // 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 alloca directly so the pointer is persistent 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.lowerExpr(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.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), .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), // 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(); } if (self.force_block_value and blk.stmts.len > 0) { // Extract last expression value (for if-else branch blocks) 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.global_names.get(te.name)) |gi| { break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); } // Type-as-value: if target is Any (Type variable), produce a boxed string if (self.target_type == .any) { const sid = self.module.types.internString(te.name); const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } // Type expressions are always type names from the parser — they appear // in value position when used as args to cast() etc. Silent placeholder. if (self.isKnownTypeName(te.name)) { break :blk self.emitPlaceholder(te.name); } break :blk self.emitError(te.name, node.span); }, else => self.emitError("unknown_expr", node.span), }; } 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) { 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); } // 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; } } } } // 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) { const other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); if (other_ty != .void) { const saved_tt = self.target_type; self.target_type = other_ty; const lv = self.lowerExpr(bop.lhs); const rv = self.lowerExpr(bop.rhs); 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); // Set target_type from LHS so enum literals on RHS resolve correctly const lhs_ty = self.inferExprType(bop.lhs); 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); self.target_type = saved_tt; // Infer result type from LHS operand (covers float, bool, etc.) var ty = lhs_ty; // Promote int×float → float (e.g., s64 * f32 → f32) // Only for scalar int LHS — don't affect vectors or structs. { const rhs_inferred = self.inferExprType(bop.rhs); const l_int = isInt(ty); const r_float = (rhs_inferred == .f32 or rhs_inferred == .f64); if (l_int and r_float) { 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 = 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); } } } 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 (==, !=, <, <=, >, >=) 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), } } 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; } 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; } // ── Control flow ──────────────────────────────────────────────── fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { // inline if: evaluate condition at compile time, only lower taken branch if (ie.is_comptime) { if (self.evalComptimeCondition(ie.condition)) |is_true| { if (is_true) { return self.lowerInlineBranch(ie.then_branch); } else if (ie.else_branch) |eb| { return self.lowerInlineBranch(eb); } return self.builder.constInt(0, .void); } // Condition couldn't be evaluated — fall through to runtime } // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors if (self.tryConstBoolCondition(ie.condition)) |is_true| { if (is_true) { // Condition always true: only lower then-branch if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) { return self.lowerExpr(ie.then_branch); } self.lowerBlock(ie.then_branch); // If then-branch terminated (return/break), mark block as dead if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } return self.builder.constInt(0, .void); } else { // Condition always false: only lower else-branch (if any) if (ie.else_branch) |eb| { if (ie.is_inline or self.force_block_value) { return self.lowerExpr(eb); } self.lowerBlock(eb); if (self.currentBlockHasTerminator()) { self.block_terminated = true; return .none; } } return self.builder.constInt(0, .void); } } // Optional binding: `if val := expr { ... }` // Clear target_type so the ternary's result type doesn't leak into the condition // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32) const saved_cond_target = self.target_type; self.target_type = null; const opt_val = self.lowerExpr(ie.condition); self.target_type = saved_cond_target; const cond = if (ie.binding_name != null) blk: { // The condition is an optional — emit has_value check break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); } else opt_val; const has_else = ie.else_branch != null; // If-else produces a value when inline OR when then-branch has a non-void type const is_value = (ie.is_inline or self.force_block_value) and has_else; // Infer result type from then branch for value if-exprs // If then_branch is null/void, try else_branch (e.g., `if cond then null else val`) const result_type: TypeId = if (is_value) blk: { const then_ty = self.inferExprType(ie.then_branch); if (then_ty == .void and ie.else_branch != null) { break :blk self.inferExprType(ie.else_branch.?); } break :blk then_ty; } else .void; const then_bb = self.freshBlock("if.then"); const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null; const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{}; const merge_bb = self.freshBlockWithParams("if.merge", merge_params); // Conditional branch self.builder.condBr( cond, then_bb, &.{}, if (else_bb) |eb| eb else merge_bb, &.{}, ); // Then branch self.builder.switchToBlock(then_bb); // If binding: unwrap the optional and bind to the name if (ie.binding_name) |bind_name| { const opt_ty = self.inferExprType(ie.condition); const inner_ty = if (!opt_ty.isBuiltin()) blk: { const info = self.module.types.get(opt_ty); break :blk if (info == .optional) info.optional.child else opt_ty; } else opt_ty; const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty); const slot = self.builder.alloca(inner_ty); self.builder.store(slot, unwrapped); if (self.scope) |scope| { scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true }); } } // Set target_type so null/undef in branches get the right type const saved_target = self.target_type; if (is_value and result_type != .void) self.target_type = result_type; if (is_value) { var v = self.lowerExpr(ie.then_branch); if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(v); if (v_ty != result_type and v_ty != .void and result_type != .void) { v = self.coerceToType(v, v_ty, result_type); } self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(ie.then_branch); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } // Else branch if (has_else) { self.builder.switchToBlock(else_bb.?); if (is_value) { var v = self.lowerExpr(ie.else_branch.?); if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(v); if (v_ty != result_type and v_ty != .void and result_type != .void) { v = self.coerceToType(v, v_ty, result_type); } self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(ie.else_branch.?); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } } self.target_type = saved_target; // Continue at merge self.builder.switchToBlock(merge_bb); if (is_value) { return self.builder.blockParam(merge_bb, 0, result_type); } return self.builder.constInt(0, .void); } /// Try to evaluate an AST condition as a compile-time constant bool. /// Returns true/false if the condition is known at compile time, null otherwise. fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool { switch (node.data) { .bool_literal => |bl| return bl.value, .call => |c| { if (c.callee.data == .identifier) { const cname = c.callee.data.identifier.name; if (std.mem.eql(u8, cname, "is_flags")) { // Resolve the type arg to check if it's actually a flags enum if (c.args.len > 0) { const ty = self.resolveTypeArg(c.args[0]); if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .@"enum") return info.@"enum".is_flags; } } return false; } } }, else => {}, } return null; } /// Evaluate a compile-time condition for `inline if`. /// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { if (node.data != .binary_op) return null; const bo = &node.data.binary_op; if (bo.op != .eq and bo.op != .neq) return null; // LHS must be an identifier that's in comptime_constants const name = switch (bo.lhs.data) { .identifier => |id| id.name, else => return null, }; const cv = self.comptime_constants.get(name) orelse return null; switch (cv) { .enum_tag => |et| { // RHS must be an enum literal (.variant) const variant_name = switch (bo.rhs.data) { .enum_literal => |el| el.name, else => return null, }; // Look up variant index in the enum type const enum_info = self.module.types.get(et.ty); if (enum_info != .@"enum") return null; const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); const result = et.tag == variant_idx; return if (bo.op == .eq) result else !result; }, .int_val => |iv| { // RHS must be an integer literal const rhs_val: i64 = switch (bo.rhs.data) { .int_literal => |il| il.value, else => return null, }; const result = iv == rhs_val; return if (bo.op == .eq) result else !result; }, } } /// Evaluate a compile-time match expression for `inline if ... == { case ... }`. /// Returns the body of the matching arm, or null if the match can't be resolved. fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node { // Subject must be a comptime constant identifier const name = switch (me.subject.data) { .identifier => |id| id.name, else => return null, }; const cv = self.comptime_constants.get(name) orelse return null; switch (cv) { .enum_tag => |et| { const enum_info = self.module.types.get(et.ty); if (enum_info != .@"enum") return null; for (me.arms) |arm| { if (arm.pattern == null) continue; // default arm const variant_name = switch (arm.pattern.?.data) { .enum_literal => |el| el.name, else => continue, }; const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); if (et.tag == variant_idx) return arm.body; } // No match — try default arm for (me.arms) |arm| { if (arm.pattern == null) return arm.body; } return null; }, .int_val => |iv| { for (me.arms) |arm| { if (arm.pattern == null) continue; const rhs_val: i64 = switch (arm.pattern.?.data) { .int_literal => |il| il.value, else => continue, }; if (iv == rhs_val) return arm.body; } for (me.arms) |arm| { if (arm.pattern == null) return arm.body; } return null; }, } } fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { const header_bb = self.freshBlock("while.hdr"); const body_bb = self.freshBlock("while.body"); const exit_bb = self.freshBlock("while.exit"); // Branch to header self.builder.br(header_bb, &.{}); // Header: evaluate condition self.builder.switchToBlock(header_bb); const cond = self.lowerExpr(we.condition); self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{}); // Body self.builder.switchToBlock(body_bb); // Save and set loop targets const old_break = self.break_target; const old_continue = self.continue_target; self.break_target = exit_bb; self.continue_target = header_bb; defer { self.break_target = old_break; self.continue_target = old_continue; } self.lowerBlock(we.body); if (!self.currentBlockHasTerminator()) { self.builder.br(header_bb, &.{}); } // Continue at exit self.builder.switchToBlock(exit_bb); return self.builder.constInt(0, .void); } fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { // Lower iterable const iterable = self.lowerExpr(fe.iterable); // Get length const len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64); // Create index variable const idx_slot = self.builder.alloca(.s64); const zero = self.builder.constInt(0, .s64); self.builder.store(idx_slot, zero); const header_bb = self.freshBlock("for.hdr"); const body_bb = self.freshBlock("for.body"); const inc_bb = self.freshBlock("for.inc"); const exit_bb = self.freshBlock("for.exit"); self.builder.br(header_bb, &.{}); // Header: compare index < length self.builder.switchToBlock(header_bb); const idx_val = self.builder.load(idx_slot, .s64); const cmp = self.builder.cmpLt(idx_val, len); self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); // Body self.builder.switchToBlock(body_bb); // Bind element — resolve element type from iterable const iterable_ty = self.inferExprType(fe.iterable); const elem_ty = self.getElementType(iterable_ty); const elem = self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, elem_ty); var body_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &body_scope; body_scope.put(fe.capture_name, .{ .ref = elem, .ty = elem_ty, .is_alloca = false }); // Bind index if requested if (fe.index_name) |iname| { body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); } // Save and set loop targets const old_break = self.break_target; const old_continue = self.continue_target; self.break_target = exit_bb; self.continue_target = inc_bb; // continue → increment, not header self.lowerBlock(fe.body); self.break_target = old_break; self.continue_target = old_continue; self.scope = old_scope; body_scope.deinit(); // Fall through to increment block if (!self.currentBlockHasTerminator()) { self.builder.br(inc_bb, &.{}); } // Increment block: increment index and jump back to header self.builder.switchToBlock(inc_bb); { const cur_idx = self.builder.load(idx_slot, .s64); const one = self.builder.constInt(1, .s64); const next_idx = self.builder.add(cur_idx, one, .s64); self.builder.store(idx_slot, next_idx); self.builder.br(header_bb, &.{}); } // Continue at exit self.builder.switchToBlock(exit_bb); return self.builder.constInt(0, .void); } fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { // inline if match: evaluate at compile time, only lower the matching arm if (me.is_comptime) { if (self.evalComptimeMatch(me)) |arm_body| { return self.lowerInlineBranch(arm_body); } // Couldn't evaluate — fall through to runtime } const is_type_match = isTypeCategoryMatch(me); const subject = self.lowerExpr(me.subject); // Detect optional subject type const subject_ty = self.inferExprType(me.subject); const is_optional_match = blk: { if (!subject_ty.isBuiltin()) { const info = self.module.types.get(subject_ty); break :blk info == .optional; } break :blk false; }; // Determine if the match produces a value (has non-void arms) // For type-category matches (inside any_to_string), only produce value when force_block_value // For regular enum/optional matches, always produce value if arms are non-void const inferred_result = self.inferMatchResultType(me); const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or inferred_result != .void); const result_type: TypeId = if (is_value) inferred_result else .void; const merge_params: []const TypeId = if (is_value and result_type != .void) &.{result_type} else &.{}; const merge_bb = self.freshBlockWithParams("match.merge", merge_params); // Build arm blocks var default_bb: ?BlockId = null; var arm_blocks = std.ArrayList(BlockId).empty; defer arm_blocks.deinit(self.alloc); for (me.arms) |_| { arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable; } // Build case list and pre-collect type tags per arm var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; defer cases.deinit(self.alloc); var arm_tag_values = std.ArrayList([]const u64).empty; defer arm_tag_values.deinit(self.alloc); for (me.arms, 0..) |arm, i| { if (arm.pattern == null) { default_bb = arm_blocks.items[i]; arm_tag_values.append(self.alloc, &.{}) catch unreachable; continue; } const pat = arm.pattern.?; if (is_type_match) { // Type-category match: resolve category name to tag values const name = switch (pat.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => "", }; const tag_values = self.resolveTypeCategoryTags(name); arm_tag_values.append(self.alloc, tag_values) catch unreachable; for (tag_values) |tag| { cases.append(self.alloc, .{ .value = @intCast(tag), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } } else if (is_optional_match) { // Optional match: .some → 1 (has_value=true), .none → 0 arm_tag_values.append(self.alloc, &.{}) catch unreachable; const pat_name = switch (pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, else => "", }; const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0; cases.append(self.alloc, .{ .value = @intCast(case_val), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } else { // Enum/value match: resolve variant name to actual tag value arm_tag_values.append(self.alloc, &.{}) catch unreachable; const case_val: u64 = blk: { const pat_name = switch (pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, .int_literal => |il| break :blk @intCast(il.value), .bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0), else => break :blk @as(u64, @intCast(i)), }; // Look up variant value in the subject's type if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { for (ty_info.tagged_union.fields, 0..) |f, vi| { const vname = self.module.types.strings.get(f.name); if (std.mem.eql(u8, vname, pat_name)) { if (ty_info.tagged_union.explicit_tag_values) |vals| { if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); } break :blk @intCast(vi); } } if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(subject_ty); diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); } } else if (ty_info == .@"enum") { for (ty_info.@"enum".variants, 0..) |v, vi| { const vname = self.module.types.strings.get(v); if (std.mem.eql(u8, vname, pat_name)) { if (ty_info.@"enum".explicit_values) |vals| { if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); } break :blk @intCast(vi); } } if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(subject_ty); diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); } } } break :blk @intCast(i); }; cases.append(self.alloc, .{ .value = @intCast(case_val), .target = arm_blocks.items[i], .args = &.{}, }) catch unreachable; } } // If no default arm, create an unreachable default if (default_bb == null) { default_bb = self.freshBlock("match.unr"); } // Switch on the subject (for type match, subject IS the tag; for enum match, extract tag) const tag = if (is_type_match) subject else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else blk: { // Determine actual tag type from union info (e.g. u32 for SDL_Event) const tag_ty: TypeId = tt: { if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type; } break :tt .s32; }; break :blk self.builder.enumTag(subject, tag_ty); }; self.builder.switchBr(tag, cases.items, default_bb.?, &.{}); // Lower each arm's body for (me.arms, 0..) |arm, i| { self.builder.switchToBlock(arm_blocks.items[i]); // For type-match arms with empty tag lists, the arm is unreachable // (no switch case targets it). Skip lowering to avoid invalid IR // from runtime cast/dispatch with no matching types. if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) { self.builder.emitUnreachable(); continue; } var arm_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &arm_scope; if (arm.capture) |capture_name| { if (is_optional_match) { // For optional match, unwrap the optional value const opt_info = self.module.types.get(subject_ty); const child_ty = if (opt_info == .optional) opt_info.optional.child else .s64; const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty); arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false }); } else { // Resolve actual variant index and payload type from the subject's type var variant_idx: u32 = @intCast(i); var payload_ty: TypeId = .s64; if (arm.pattern) |arm_pat| { const pat_name = switch (arm_pat.data) { .enum_literal => |el| el.name, .identifier => |id| id.name, else => "", }; if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { for (ty_info.tagged_union.fields, 0..) |f, vi| { const vname = self.module.types.strings.get(f.name); if (std.mem.eql(u8, vname, pat_name)) { variant_idx = @intCast(vi); payload_ty = f.ty; break; } } } } } const payload = self.builder.emit(.{ .enum_payload = .{ .base = subject, .field_index = variant_idx, } }, payload_ty); arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false }); } } // Set match arm context for runtime type dispatch const saved_match_tags = self.current_match_tags; if (is_type_match) { self.current_match_tags = arm_tag_values.items[i]; } if (is_value and result_type != .void) { var v = self.lowerBlockValue(arm.body) orelse if (result_type == .string or !result_type.isBuiltin()) self.builder.constUndef(result_type) else self.builder.constInt(0, result_type); self.current_match_tags = saved_match_tags; self.scope = old_scope; arm_scope.deinit(); if (!self.currentBlockHasTerminator()) { // Coerce arm value to match result type const v_ty = self.builder.getRefType(v); v = self.coerceToType(v, v_ty, result_type); self.builder.br(merge_bb, &.{v}); } } else { self.lowerBlock(arm.body); self.current_match_tags = saved_match_tags; self.scope = old_scope; arm_scope.deinit(); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } } } // Emit default block if no explicit else arm if (default_bb != null) { var found_default = false; for (me.arms) |arm| { if (arm.pattern == null) { found_default = true; break; } } if (!found_default) { self.builder.switchToBlock(default_bb.?); if (is_type_match) { // For type-category matches, unrecognized tags should skip to merge // (e.g., optional types not covered by any_to_string categories) if (is_value and result_type != .void) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { self.builder.br(merge_bb, &.{}); } } else { // For non-exhaustive matches (union/enum with unhandled variants), // fall through to merge instead of unreachable const is_exhaustive = blk: { if (!subject_ty.isBuiltin()) { const ty_info = self.module.types.get(subject_ty); if (ty_info == .tagged_union) { break :blk cases.items.len >= ty_info.tagged_union.fields.len; } else if (ty_info == .@"enum") { break :blk cases.items.len >= ty_info.@"enum".variants.len; } } break :blk false; }; if (is_exhaustive) { self.builder.emitUnreachable(); } else if (is_value and result_type != .void) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { self.builder.br(merge_bb, &.{}); } } } } self.builder.switchToBlock(merge_bb); if (is_value and result_type != .void) { return self.builder.blockParam(merge_bb, 0, result_type); } return self.builder.constInt(0, .void); } fn lowerBreak(self: *Lowering) Ref { if (self.break_target) |target| { self.builder.br(target, &.{}); } return Ref.none; } fn lowerContinue(self: *Lowering) Ref { if (self.continue_target) |target| { self.builder.br(target, &.{}); } return Ref.none; } // ── Struct/enum/union ops ─────────────────────────────────────── fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral) 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 .s64; 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); } } } } const ty: TypeId = if (sl.struct_name) |name| blk: { const name_id = self.module.types.internString(name); break :blk self.module.types.findByName(name_id) orelse self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } else if (sl.type_expr) |te| // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr self.resolveTypeWithBindings(te) else self.target_type orelse .s64; // 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| { const saved_tt = self.target_type; self.target_type = sf.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; fields.append(self.alloc, val) 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| { const saved_tt = self.target_type; self.target_type = sf.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; fields.append(self.alloc, val) 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. 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). fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { if (func.params.len == 0) return; const first_param_ty = func.params[0].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; } } } } /// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. 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 .s64; }; if (idx < tuple.fields.len) return tuple.fields[idx]; return .s64; } } const struct_fields = self.getStructFields(ty); for (struct_fields) |f| { if (f.name == field_name_id) return f.ty; } return .s64; } fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { // 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); } } 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); } /// 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) 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 field access: .x/.y/.z/.w → index 0/1/2/3 if (!obj_ty.isBuiltin()) { const vinfo = self.module.types.get(obj_ty); if (vinfo == .vector) { const vidx: u32 = if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) 0 else if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) 1 else if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) 2 else if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) 3 else 0; 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 .s64; const tag = self.resolveVariantValue(target, el.name); return self.builder.enumInit(tag, Ref.none, target); } /// 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, ) Ref { 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); } /// 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. 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 = .s64; 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 != .s64) { 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; const val = self.lowerExpr(elem); self.target_type = old_tt; 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 .s64, }; if (std.mem.eql(u8, callee_name, "Vector")) { if (cl.args.len == 2) { const length: u32 = switch (cl.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), else => 0, }; const elem = self.resolveTypeWithBindings(cl.args[1]); if (length > 0) return self.module.types.vectorOf(elem, length); } } // Try as generic struct if (self.struct_template_map.getPtr(callee_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, cl.args); } return .s64; }, .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt), .identifier => |id| { const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .s64; }, .type_expr => return type_bridge.resolveAstType(te, &self.module.types), .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 .s64; }, else => return .s64, } } fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { 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; for (tl.elements) |elem| { const val = self.lowerExpr(elem.value); elems.append(self.alloc, val) catch unreachable; const ety = self.inferExprType(elem.value); field_type_ids.append(self.alloc, ety) 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; } } // Create a tuple type const tuple_ty = 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); var pointee_ty: TypeId = .s64; if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) { pointee_ty = info.pointer.pointee; } } return self.builder.emit(.{ .deref = .{ .operand = ptr } }, pointee_ty); } 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 .s64; } // ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ /// Intern an Obj-C selector string into a module-scoped `SEL*` slot. /// First call creates the global; subsequent calls return the same /// `GlobalId`. emit_llvm.zig walks `module.objc_selector_cache` and /// synthesizes a constructor that populates each slot via /// `sel_registerName` exactly once at module load. /// /// Slot name matches clang's convention: `OBJC_SELECTOR_REFERENCES_` /// with `:` replaced by `_` to keep the symbol name valid. fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId { if (self.module.lookupObjcSelector(sel_str)) |gid| return gid; // Mangle selector: replace colons with underscores. Apple's // toolchain does the same (foo:bar: → foo_bar_). var mangled = std.ArrayList(u8).empty; defer mangled.deinit(self.alloc); mangled.appendSlice(self.alloc, "OBJC_SELECTOR_REFERENCES_") catch unreachable; for (sel_str) |ch| { mangled.append(self.alloc, if (ch == ':') '_' else ch) catch unreachable; } const slot_name = self.module.types.internString(mangled.items); const vptr_ty = self.module.types.ptrTo(.void); const gid = self.module.addGlobal(.{ .name = slot_name, .ty = vptr_ty, .init_val = .null_val, .is_extern = false, .is_const = false, }); self.module.appendObjcSelector(sel_str, gid); return gid; } /// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern. /// Cached per Lowering instance so multiple `#objc_call` sites share /// one declaration. fn getSelRegisterNameFid(self: *Lowering) FuncId { if (self.sel_register_name_fid) |fid| return fid; var params = std.ArrayList(inst_mod.Function.Param).empty; const name_str = self.module.types.internString("name"); const ptr_ty = self.module.types.ptrTo(.u8); params.append(self.alloc, .{ .name = name_str, .ty = ptr_ty }) catch unreachable; const fn_name = self.module.types.internString("sel_registerName"); const ret_ty = self.module.types.ptrTo(.void); const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty); const func = self.module.getFunctionMut(fid); func.call_conv = .c; self.sel_register_name_fid = fid; return fid; } /// Lower `#objc_call(T)(recv, "sel:", args...)` to: /// %sel = call ptr @sel_registerName(<"sel:">) /// %ret = call @objc_msgSend(recv, %sel, args...) /// For Phase 1.3 only the (void return, no extra args) form is /// fully wired. Extra arities + non-void returns will land in /// subsequent phase-1 steps. fn lowerFfiIntrinsicCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref { if (fic.kind == .jni_call or fic.kind == .jni_static_call) { return self.lowerJniCall(fic); } if (fic.args.len < 2) { if (self.diagnostics) |d| { d.add(.err, "#objc_call requires at least a receiver and a selector", null); } return Ref.none; } // Resolve the return type from the syntactic slot. const ret_ty = self.resolveType(fic.return_type); if (fic.args.len < 2) { if (self.diagnostics) |d| { d.add(.err, "#objc_call requires at least a receiver and a selector", null); } return Ref.none; } // Receiver expression. const recv = self.lowerExpr(fic.args[0]); // Selector. Literal selectors get interned into a module- // scoped `SEL*` slot — emit_llvm.zig tags the slot into // `__DATA,__objc_selrefs` so dyld populates it at load time // (matches clang's `@selector(...)` lowering exactly). // Non-literal selectors keep the per-call `sel_registerName` // fallback. const sel_arg_node = fic.args[1]; const vptr_ty = self.module.types.ptrTo(.void); const sel = blk: { if (sel_arg_node.data == .string_literal) { const raw = sel_arg_node.data.string_literal.raw; const slot_gid = self.internObjcSelector(raw); const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty)); break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); } const sel_ref = self.lowerExpr(sel_arg_node); const sel_fid = self.getSelRegisterNameFid(); var sel_args = std.ArrayList(Ref).empty; sel_args.append(self.alloc, sel_ref) catch unreachable; const sel_owned = sel_args.toOwnedSlice(self.alloc) catch unreachable; break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty); }; // Additional args after recv + selector. var extra = std.ArrayList(Ref).empty; var ai: usize = 2; while (ai < fic.args.len) : (ai += 1) { extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable; } const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable; return self.builder.emit(.{ .objc_msg_send = .{ .recv = recv, .sel = sel, .args = extra_owned, } }, ret_ty); } fn lowerJniCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref { if (fic.args.len < 4) { if (self.diagnostics) |d| { d.add(.err, "#jni_call requires env, target, method name, and signature", null); } return Ref.none; } const ret_ty = self.resolveType(fic.return_type); const env_ref = self.lowerExpr(fic.args[0]); const target_ref = self.lowerExpr(fic.args[1]); const name_ref = self.lowerExpr(fic.args[2]); const sig_ref = self.lowerExpr(fic.args[3]); var extra = std.ArrayList(Ref).empty; var ai: usize = 4; while (ai < fic.args.len) : (ai += 1) { extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable; } const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable; return self.builder.emit(.{ .jni_msg_send = .{ .env = env_ref, .target = target_ref, .name = name_ref, .sig = sig_ref, .args = extra_owned, .is_static = fic.kind == .jni_static_call, } }, ret_ty); } // ── Calls ─────────────────────────────────────────────────────── fn lowerCall(self: *Lowering, c: *const ast.Call) Ref { // Check reflection builtins first (before lowering args — some args are type names, not values) if (c.callee.data == .identifier) { if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref; } // Check for runtime dispatch pattern BEFORE lowering args. // lowerRuntimeDispatchCall handles its own arg lowering, and pre-lowering // cast(type) val would produce a dead `call_builtin cast : void`. if (c.callee.data == .identifier) { const id_name = c.callee.data.identifier.name; const eff_name = blk: { const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; if (self.ufcs_alias_map.get(id_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; // C-import visibility: deny calls to C fn_decls not in the caller's module scope if (!self.isCImportVisible(eff_name)) { if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "C function '{s}' not visible; add #import for the module that declares it", .{eff_name}); return Ref.none; } if (self.fn_ast_map.get(eff_name)) |fd| { if (self.current_match_tags) |tags| { if (tags.len > 0 and self.hasCastWithRuntimeType(c)) { return self.lowerRuntimeDispatchCall(fd, eff_name, c, tags); } } } } // Handle closure(fn_or_lambda) — wrap bare functions into closures if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "closure")) { if (c.args.len >= 1) { const arg = c.args[0]; // If argument is a bare function name, create a proper closure from it if (arg.data == .identifier) { const fn_name = arg.data.identifier.name; if (!self.lowered_functions.contains(fn_name)) { self.lazyLowerFunction(fn_name); } if (self.resolveFuncByName(fn_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; // Build closure type from function signature var param_types_list = std.ArrayList(TypeId).empty; defer param_types_list.deinit(self.alloc); for (func.params) |p| { param_types_list.append(self.alloc, p.ty) catch unreachable; } const closure_ty = self.module.types.closureType(param_types_list.items, func.ret); const closure_info = self.module.types.get(closure_ty).closure; const tramp_id = self.createBareFnTrampoline(fid, closure_info); return self.builder.closureCreate(tramp_id, Ref.none, closure_ty); } } // Lambda or other expression — already produces closure_create return self.lowerExpr(arg); } } // Early detection of comptime-expanded calls (e.g. print) — skip arg evaluation // since lowerComptimeCall re-evaluates args from AST (avoiding double evaluation) if (c.callee.data == .identifier) { const early_name = blk: { const id_name = c.callee.data.identifier.name; const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name; if (self.ufcs_alias_map.get(id_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; if (self.fn_ast_map.get(early_name)) |fd| { if (hasComptimeParams(fd)) { return self.lowerComptimeCall(fd, c); } // Early detection of generic function calls — skip arg lowering for type params // because lowerGenericCall resolves type params from AST nodes, not lowered refs. // Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.) const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false; if (fd.type_params.len > 0 and !shadowed) { // Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2)) // Types are inferred when call args < param count (e.g., are_equal(p1, p2)) const types_explicit = c.args.len == fd.params.len; var lowered_args = std.ArrayList(Ref).empty; defer lowered_args.deinit(self.alloc); for (c.args, 0..) |arg, ai| { // Skip type param args only when types are passed explicitly if (types_explicit and ai < fd.params.len and isTypeParamDecl(&fd.params[ai], fd.type_params)) { lowered_args.append(self.alloc, Ref.none) catch unreachable; } else { const saved_target = self.target_type; lowered_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; self.target_type = saved_target; } } return self.lowerGenericCall(fd, early_name, c, lowered_args.items); } } } // Lower args (with target type propagation for xx conversions) var args = std.ArrayList(Ref).empty; defer args.deinit(self.alloc); // Try to resolve param types for target_type context const param_types = self.resolveCallParamTypes(c); // For enum_literal callees (.Variant(payload)), resolve the payload target type // from the union field type so struct literal fields get proper coercion var enum_payload_ty: ?TypeId = null; if (c.callee.data == .enum_literal) { const target = self.target_type orelse .s64; if (!target.isBuiltin()) { const info = self.module.types.get(target); if (info == .tagged_union) { const tag = self.resolveVariantIndex(target, c.callee.data.enum_literal.name); if (tag < info.tagged_union.fields.len) { enum_payload_ty = info.tagged_union.fields[tag].ty; } } } } for (c.args, 0..) |arg, ai| { // Skip spread expressions — they'll be handled by packVariadicCallArgs from AST if (arg.data == .spread_expr) { args.append(self.alloc, Ref.none) catch unreachable; continue; } const saved_target = self.target_type; if (ai < param_types.len) { self.target_type = param_types[ai]; } if (enum_payload_ty) |ept| { if (ai == 0) self.target_type = ept; } // Implicit address-of: when param expects *T and arg is an identifier // with an alloca of type T, pass the alloca pointer directly (reference // semantics, so mutations through the pointer are visible to the caller). if (ai < param_types.len and arg.data == .identifier) { const pt = param_types[ai]; if (!pt.isBuiltin()) { const pti = self.module.types.get(pt); if (pti == .pointer) { if (self.scope) |scope| { if (scope.lookup(arg.data.identifier.name)) |binding| { // Only apply when the binding type matches the pointee type if (binding.is_alloca and binding.ty == pti.pointer.pointee) { const ptr_ty = self.module.types.ptrTo(binding.ty); args.append(self.alloc, self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty)) catch unreachable; self.target_type = saved_target; continue; } } } } } } const val = self.lowerExpr(arg); self.target_type = saved_target; args.append(self.alloc, val) catch unreachable; } switch (c.callee.data) { .identifier => |id| { // Resolve local function name (bare → mangled) and UFCS aliases const func_name = blk: { // First try scope lookup for mangled local fn names const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; // Then try UFCS alias on bare name if (self.ufcs_alias_map.get(id.name)) |target| { // Resolve the alias target through scope too (target may be mangled) break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; // Handle cast(TargetType, val) — emit conversion instructions // Only for compile-time known types (type_expr or known type names) if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) { const type_arg = c.args[0]; const is_static_type = blk: { if (type_arg.data == .type_expr) break :blk true; if (type_arg.data == .identifier) { const tname = type_arg.data.identifier.name; // Check if it's a known type name (not a runtime variable) if (type_bridge.resolveTypePrimitive(tname) != null) break :blk true; if (self.type_bindings) |bindings| { if (bindings.get(tname) != null) break :blk true; } // Check if it's a registered struct/enum type name const name_id = self.module.types.internString(tname); if (self.module.types.findByName(name_id) != null) break :blk true; } break :blk false; }; if (is_static_type) { const dst_ty = self.resolveTypeArg(c.args[0]); const val = args.items[1]; // already lowered const src_ty = self.inferExprType(c.args[1]); // Unbox Any → concrete type if (src_ty == .any) { return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); } return self.coerceToType(val, src_ty, dst_ty); } // Runtime cast — fall through to builtin handling } // Check builtins first (these are handled natively by interpreter and emitter) if (resolveBuiltin(id.name)) |bid| { // free(protocol_value) → extract ctx (field 0) and free it if (bid == .free and args.items.len == 1) { const arg_ty = self.builder.getRefType(args.items[0]); if (self.getProtocolInfo(arg_ty) != null) { const void_ptr_ty = self.module.types.ptrTo(.void); const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = args.items[0], .field_index = 0 } }, void_ptr_ty); return self.builder.emit(.{ .heap_free = .{ .operand = ctx_ref } }, .void); } } const ret_ty: TypeId = switch (bid) { .malloc => .s64, // pointer .size_of => .s64, .memcpy, .memset => .s64, .sqrt, .sin, .cos, .floor => blk: { // Math builtins: return type matches argument type ($T -> T) if (c.args.len > 0) { const arg_ty = self.inferExprType(c.args[0]); if (arg_ty == .f32) break :blk TypeId.f32; } break :blk TypeId.f64; }, else => .void, }; return self.builder.callBuiltin(bid, args.items, ret_ty); } // Check scope first: local variables (closures, fn ptrs) shadow global functions if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (!binding.ty.isBuiltin()) { const ty_info = self.module.types.get(binding.ty); if (ty_info == .closure) { const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; const owned = self.alloc.dupe(Ref, args.items) catch unreachable; const ret_ty = ty_info.closure.ret; return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, ret_ty); } } } } // Check for comptime-expanded or generic functions if (self.fn_ast_map.get(func_name)) |fd| { if (hasComptimeParams(fd)) { return self.lowerComptimeCall(fd, c); } if (fd.type_params.len > 0) { // Runtime dispatch already handled above (before arg lowering) return self.lowerGenericCall(fd, func_name, c, args.items); } } // Check for #compiler free functions if (self.fn_ast_map.get(func_name)) |fd_check| { if (fd_check.body.data == .compiler_expr) { const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types) else TypeId.void; return self.builder.compilerCall(func_name, args.items, ret_ty); } } // Look up declared/extern function — try lazy lowering if not yet lowered { // First attempt: function may already be declared (from scanDecls) // but not yet lowered. Try lazy lowering if needed. if (self.fn_ast_map.contains(func_name) and !self.lowered_functions.contains(func_name)) { self.lazyLowerFunction(func_name); } if (self.resolveFuncByName(func_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; // Pack variadic args into a slice if the function has a variadic param if (self.fn_ast_map.get(func_name)) |fd| { self.packVariadicCallArgs(fd, c, &args); } // Coerce arguments to match parameter types self.coerceCallArgs(args.items, params); return self.builder.call(fid, args.items, ret_ty); } } // May be a variable holding a function pointer (non-closure) if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref; const owned = self.alloc.dupe(Ref, args.items) catch unreachable; const ret_ty = if (!binding.ty.isBuiltin()) blk: { const bti = self.module.types.get(binding.ty); break :blk if (bti == .function) bti.function.ret else .s64; } else .s64; return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty); } } // May be a global variable holding a function pointer if (self.global_names.get(id.name)) |gi| { if (!gi.ty.isBuiltin()) { const gti = self.module.types.get(gi.ty); if (gti == .function) { const callee_ref = self.builder.emit(.{ .global_get = gi.id }, gi.ty); // Coerce args to match fn-ptr param types (including implicit address-of) for (args.items, 0..) |*arg, ai| { if (ai < gti.function.params.len) { const dst_ty = gti.function.params[ai]; const src_ty = self.inferExprType(c.args[ai]); // Implicit address-of: passing T where *T expected if (!dst_ty.isBuiltin()) { const dti = self.module.types.get(dst_ty); if (dti == .pointer and dti.pointer.pointee == src_ty and src_ty != .void) { // For identifier args, pass the alloca directly (reference semantics) if (c.args[ai].data == .identifier) { if (self.scope) |scope| { if (scope.lookup(c.args[ai].data.identifier.name)) |binding| { if (binding.is_alloca) { arg.* = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, dst_ty); continue; } } } } // For other expressions, copy semantics const slot = self.builder.alloca(src_ty); self.builder.store(slot, arg.*); arg.* = slot; continue; } } arg.* = self.coerceToType(arg.*, src_ty, dst_ty); } } const owned = self.alloc.dupe(Ref, args.items) catch unreachable; return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, gti.function.ret); } } } // Unresolved function call return self.emitError(id.name, c.callee.span); }, .field_access => |fa| { // Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free if (self.matchContextAllocCall(fa, args.items)) |ref| return ref; // Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type if (fa.object.data == .call) { const inner_call = &fa.object.data.call; if (inner_call.callee.data == .identifier) { const inner_name = inner_call.callee.data.identifier.name; const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name; // Generic struct static method: Animated(Size).make(...) if (self.struct_template_map.getPtr(resolved)) |tmpl| { const inst_ty = self.instantiateGenericStruct(tmpl, inner_call.args); const inst_name = self.formatTypeName(inst_ty); // Look up template method, monomorphize, and call if (self.struct_instance_template.get(inst_name)) |tmpl_name| { const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { if (self.struct_instance_bindings.getPtr(inst_name)) |bindings| { const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ inst_name, fa.field }) catch fa.field; if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, bindings); } if (self.resolveFuncByName(mangled)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; self.coerceCallArgs(args.items, func.params); return self.builder.call(fid, args.items, func.ret); } } } } } if (self.fn_ast_map.get(resolved)) |fd| { if (fd.type_params.len > 0) { // Try instantiate as type function if (self.instantiateTypeFunction(inner_name, inner_name, fd, inner_call.args)) |result_ty| { const type_info = self.module.types.get(result_ty); if (type_info == .tagged_union) { // Qualified enum construction: Type.variant(payload) const tag = self.resolveVariantIndex(result_ty, fa.field); var payload = if (args.items.len > 0) args.items[0] else Ref.none; if (!payload.isNone()) { const fields = type_info.tagged_union.fields; if (tag < fields.len) { const field_ty = fields[tag].ty; if (field_ty != .void) { const payload_ty = self.inferExprType(c.args[0]); if (field_ty != payload_ty) { payload = self.coerceToType(payload, payload_ty, field_ty); } } } } return self.builder.enumInit(tag, payload, result_ty); } if (type_info == .@"enum") { const tag = self.resolveVariantIndex(result_ty, fa.field); return self.builder.enumInit(tag, Ref.none, result_ty); } } } } } } // Check if this is a namespace-qualified call (e.g., std.print) // If the object is an identifier/type_expr not in scope, treat as namespace prefix const is_namespace = blk: { const obj_name: ?[]const u8 = switch (fa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => null, }; if (obj_name) |name| { // Check local scope first if (self.scope) |scope| { if (scope.lookup(name) != null) break :blk false; } // Check global variables (e.g., g_font : *FontAtlas) if (self.global_names.contains(name)) break :blk false; // Not a local or global variable → namespace prefix break :blk true; } break :blk false; }; if (is_namespace) { // Namespace call: module.func(args) — don't prepend object const func_name = fa.field; // Also try qualified name: Namespace.method (for struct methods) const ns_name: ?[]const u8 = switch (fa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => null, }; const qualified_name = if (ns_name) |n| std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ n, fa.field }) catch func_name else func_name; // Check for comptime-expanded or generic functions (try both names) const effective_name = if (self.fn_ast_map.get(qualified_name) != null) qualified_name else func_name; if (self.fn_ast_map.get(effective_name)) |fd| { if (hasComptimeParams(fd)) { return self.lowerComptimeCall(fd, c); } if (fd.type_params.len > 0) { return self.lowerGenericCall(fd, effective_name, c, args.items); } } if (self.fn_ast_map.contains(effective_name) and !self.lowered_functions.contains(effective_name)) { self.lazyLowerFunction(effective_name); } if (self.resolveFuncByName(effective_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; if (self.fn_ast_map.get(effective_name)) |fd| { self.packVariadicCallArgs(fd, c, &args); } self.coerceCallArgs(args.items, params); return self.builder.call(fid, args.items, ret_ty); } // Check if this is Type.variant(payload) — qualified enum construction if (ns_name) |type_name| { const type_name_id = self.module.types.internString(type_name); if (self.module.types.findByName(type_name_id)) |union_ty| { const type_info = self.module.types.get(union_ty); if (type_info == .tagged_union) { const tag = self.resolveVariantIndex(union_ty, func_name); var payload = if (args.items.len > 0) args.items[0] else Ref.none; // Coerce payload to match field type if (!payload.isNone()) { const fields = type_info.tagged_union.fields; if (tag < fields.len) { const field_ty = fields[tag].ty; const payload_ty = self.inferExprType(c.args[0]); if (field_ty != payload_ty) { payload = self.coerceToType(payload, payload_ty, field_ty); } } } return self.builder.enumInit(tag, payload, union_ty); } if (type_info == .@"enum") { const tag = self.resolveVariantIndex(union_ty, func_name); return self.builder.enumInit(tag, Ref.none, union_ty); } } } return self.emitError(func_name, c.callee.span); } // Method call: obj.method(args) → prepend obj (or &obj for *Self receivers) // For ptr.*.method(): pass the pointer directly instead of loading + re-addressing. // This ensures mutations through self: *T are visible after the call. var obj_ty: TypeId = undefined; var obj: Ref = undefined; var effective_obj_node: *const Node = fa.object; if (fa.object.data == .deref_expr) { effective_obj_node = fa.object.data.deref_expr.operand; obj_ty = self.inferExprType(effective_obj_node); obj = self.lowerExpr(effective_obj_node); } else { obj_ty = self.inferExprType(fa.object); obj = self.lowerExpr(fa.object); } // Check if field is a closure type — call as closure, not method if (!obj_ty.isBuiltin()) { const field_name_id = self.module.types.internString(fa.field); const struct_fields = self.getStructFields(obj_ty); for (struct_fields, 0..) |f, fi| { if (f.name == field_name_id and !f.ty.isBuiltin()) { const fti = self.module.types.get(f.ty); if (fti == .closure) { // structGet requires an aggregate value; if obj is *T, load through it first. var agg = obj; const oi = self.module.types.get(obj_ty); if (oi == .pointer) { agg = self.builder.load(obj, oi.pointer.pointee); } const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty); const owned = self.alloc.dupe(Ref, args.items) catch unreachable; return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret); } } } } // Check if receiver is a protocol type → dispatch through vtable/fn_ptrs if (self.getProtocolInfo(obj_ty)) |proto_info| { return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty); } // Check if receiver is `?Protocol` — for sentinel-shaped // optionals (Protocol has ctx as first ptr field, and a // null ctx is the "none" state) the unwrap is a no-op // structurally. Treat the optional value as the protocol // value and dispatch. Calling a method on a null protocol // is undefined (same as derefing a null pointer); user // guards with `if x != null` first. if (!obj_ty.isBuiltin()) { const opt_info = self.module.types.get(obj_ty); if (opt_info == .optional) { const pay_ty = opt_info.optional.child; if (self.getProtocolInfo(pay_ty)) |proto_info| { return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty); } } } var method_args = std.ArrayList(Ref).empty; defer method_args.deinit(self.alloc); method_args.append(self.alloc, obj) catch unreachable; for (args.items) |a| { method_args.append(self.alloc, a) catch unreachable; } // Try to resolve the method by struct type name const struct_name = self.getStructTypeName(obj_ty); if (struct_name) |sname| { // Try direct qualified name: StructName.method const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; // Generic #compiler method dispatch if (self.fn_ast_map.get(qualified)) |method_fd| { if (method_fd.body.data == .compiler_expr) { return self.builder.compilerCall(qualified, method_args.items, .void); } } // Check for generic struct template method if (self.struct_instance_template.get(sname)) |tmpl_name| { // This is an instantiated generic struct — look up template method const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { // Get the stored type bindings for this instance if (self.struct_instance_bindings.getPtr(sname)) |bindings| { // Monomorphize the method with the struct's type bindings const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, bindings); } if (self.resolveFuncByName(mangled)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); self.coerceCallArgs(method_args.items, params); return self.builder.call(fid, method_args.items, ret_ty); } } } } // Try non-generic qualified method if (self.fn_ast_map.get(qualified)) |fd| { if (!self.lowered_functions.contains(qualified)) { self.lazyLowerFunction(qualified); } _ = fd; } if (self.resolveFuncByName(qualified)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); // Note: coerceCallArgs can trigger protocol thunk creation // (module.addFunction), invalidating func pointer. // Use pre-extracted params/ret_ty instead of func.* after this. self.coerceCallArgs(method_args.items, params); return self.builder.call(fid, method_args.items, ret_ty); } } // Try to resolve as bare function name (method) if (self.resolveFuncByName(fa.field)) |fid| { const ret_ty = self.module.functions.items[@intFromEnum(fid)].ret; return self.builder.call(fid, method_args.items, ret_ty); } return self.emitError(fa.field, c.callee.span); }, .enum_literal => |el| { const target_opt: ?TypeId = self.target_type; // Try struct-method dispatch first: .{...}.method() where target is a struct if (target_opt) |tgt| { if (!tgt.isBuiltin()) { const target_info = self.module.types.get(tgt); if (target_info == .@"struct") { const struct_name = self.module.types.typeName(tgt); const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name; if (self.fn_ast_map.get(qualified)) |fd| { if (fd.type_params.len > 0) { return self.lowerGenericCall(fd, qualified, c, args.items); } if (!self.lowered_functions.contains(qualified)) { self.lazyLowerFunction(qualified); } } if (self.resolveFuncByName(qualified)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; self.coerceCallArgs(args.items, params); return self.builder.call(fid, args.items, ret_ty); } } } } // .Variant(payload) — tagged enum construction. Requires target to be a tagged union. const target = blk: { if (target_opt) |tgt| { if (!tgt.isBuiltin() and self.module.types.get(tgt) == .tagged_union) break :blk tgt; } if (self.diagnostics) |diags| { diags.addFmt(.err, c.callee.span, "cannot infer enum type for '.{s}' \u{2014} use an explicit type or assign to a typed variable", .{el.name}); } return self.emitPlaceholder(el.name); }; const tag = self.resolveVariantIndex(target, el.name); var payload = if (args.items.len > 0) args.items[0] else Ref.none; // Coerce payload to match the field type if (!payload.isNone() and !target.isBuiltin()) { const info = self.module.types.get(target); if (info == .tagged_union) { const fields = info.tagged_union.fields; if (tag < fields.len) { const field_ty = fields[tag].ty; const payload_ty = self.inferExprType(c.args[0]); if (field_ty != payload_ty) { payload = self.coerceToType(payload, payload_ty, field_ty); } } } } return self.builder.enumInit(tag, payload, target); }, else => { // Indirect call through expression const callee_ref = self.lowerExpr(c.callee); const owned = self.alloc.dupe(Ref, args.items) catch unreachable; return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, .s64); }, } } /// Pattern-match `context.allocator.alloc(size)` → heap_alloc, /// `context.allocator.dealloc(ptr)` → heap_free. fn matchContextAllocCall(self: *Lowering, fa: ast.FieldAccess, call_args: []const Ref) ?Ref { // fa is the callee field_access: expecting .alloc or .dealloc if (!std.mem.eql(u8, fa.field, "alloc") and !std.mem.eql(u8, fa.field, "dealloc")) return null; // fa.object should be `context.allocator` — another field_access if (fa.object.data != .field_access) return null; const inner = fa.object.data.field_access; if (!std.mem.eql(u8, inner.field, "allocator")) return null; // inner.object should be `context` — an identifier if (inner.object.data != .identifier) return null; if (!std.mem.eql(u8, inner.object.data.identifier.name, "context")) return null; if (std.mem.eql(u8, fa.field, "alloc")) { if (call_args.len < 1) return null; const ptr_void = self.module.types.ptrTo(.void); return self.builder.emit(.{ .heap_alloc = .{ .operand = call_args[0] } }, ptr_void); } else { // dealloc if (call_args.len < 1) return null; return self.builder.emit(.{ .heap_free = .{ .operand = call_args[0] } }, .void); } } fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId { // Check foreign name map first (e.g., "c_abs" → "abs") const effective_name = self.foreign_name_map.get(name) orelse name; const name_id = self.module.types.internString(effective_name); for (self.module.functions.items, 0..) |func, i| { if (func.name == name_id) return FuncId.fromIndex(@intCast(i)); } return null; } fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { const builtins = .{ // Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin .{ "out", inst_mod.BuiltinId.out }, .{ "sqrt", inst_mod.BuiltinId.sqrt }, .{ "sin", inst_mod.BuiltinId.sin }, .{ "cos", inst_mod.BuiltinId.cos }, .{ "floor", inst_mod.BuiltinId.floor }, .{ "size_of", inst_mod.BuiltinId.size_of }, .{ "cast", inst_mod.BuiltinId.cast }, .{ "malloc", inst_mod.BuiltinId.malloc }, .{ "free", inst_mod.BuiltinId.free }, .{ "memcpy", inst_mod.BuiltinId.memcpy }, .{ "memset", inst_mod.BuiltinId.memset }, }; inline for (builtins) |entry| { if (std.mem.eql(u8, name, entry[0])) return entry[1]; } return null; } // ── Lambda/closure ──────────────────────────────────────────── const CaptureInfo = struct { name: []const u8, ty: TypeId, ref: Ref, // alloca or value ref in the parent scope is_alloca: bool, }; fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { // Lower the lambda body as a new anonymous function var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda"; self.block_counter += 1; // Collect lambda param names for exclusion from captures var param_names = std.StringHashMap(void).init(self.alloc); defer param_names.deinit(); for (lam.params) |p| { param_names.put(p.name, {}) catch {}; } // Pre-scan lambda body AST for free variables (captures) var captures = std.ArrayList(CaptureInfo).empty; defer captures.deinit(self.alloc); self.collectCaptures(lam.body, ¶m_names, &captures); // Deduplicate captures var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); var deduped = std.ArrayList(CaptureInfo).empty; defer deduped.deinit(self.alloc); for (captures.items) |cap| { if (!seen.contains(cap.name)) { seen.put(cap.name, {}) catch {}; deduped.append(self.alloc, cap) catch {}; } } const capture_list = deduped.items; // Build env struct type if there are captures var env_struct_ty: TypeId = .void; if (capture_list.len > 0) { const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable; for (capture_list, 0..) |cap, i| { var nbuf: [32]u8 = undefined; const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap"; env_field_data[i] = .{ .name = self.module.types.internString(fname), .ty = cap.ty, }; } const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env"; const env_name_id = self.module.types.internString(env_name); env_struct_ty = self.module.types.intern(.{ .@"struct" = .{ .name = env_name_id, .fields = env_field_data, } }); } // Save current builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; // Build param list — trampoline convention: env: *void is first param var params = std.ArrayList(Function.Param).empty; const env_ptr_ty = self.module.types.ptrTo(.void); params.append(self.alloc, .{ .name = self.module.types.internString("env"), .ty = env_ptr_ty, }) catch unreachable; // Get target closure param types for inference (from Closure(T1, T2) -> R annotations) const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) break :blk tti.closure.params; // Unwrap ?Closure(...) → Closure(...) if (tti == .optional) { const inner = tti.optional.child; if (!inner.isBuiltin()) { const inner_info = self.module.types.get(inner); if (inner_info == .closure) break :blk inner_info.closure.params; } } } break :blk null; } else null; for (lam.params, 0..) |p, pi| { var pty = self.resolveParamType(&p); // Infer param type from target closure type if no annotation if (p.type_expr.data == .inferred_type and target_closure_params != null) { if (pi < target_closure_params.?.len) { pty = target_closure_params.?[pi]; } } params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } const ret_ty = blk: { if (lam.return_type) |rt| { break :blk type_bridge.resolveAstType(rt, &self.module.types); } // Use target closure return type if available if (self.target_type) |tt| { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) break :blk tti.closure.ret; // Unwrap ?Closure(...) → Closure(...) if (tti == .optional) { const inner = tti.optional.child; if (!inner.isBuiltin()) { const inner_info = self.module.types.get(inner); if (inner_info == .closure) break :blk inner_info.closure.ret; } } } } // Arrow lambda without explicit return type — infer from body expression // Temporarily bind params in scope so inferExprType can resolve param types var temp_scope = Scope.init(self.alloc, self.scope); const saved = self.scope; self.scope = &temp_scope; for (lam.params) |p| { const pty = self.resolveParamType(&p); temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); } const inferred = self.inferExprType(lam.body); self.scope = saved; temp_scope.deinit(); break :blk inferred; }; const name_id = self.module.types.internString(name); const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); if (lam.call_conv == .c) { self.module.getFunctionMut(func_id).call_conv = .c; } // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope WITHOUT parent — captures are bound from env, not parent scope var lambda_scope = Scope.init(self.alloc, null); self.scope = &lambda_scope; // Bind captures from env struct (param 0) if (capture_list.len > 0) { const env_param_ref = @as(Ref, @enumFromInt(0)); // Alloca env struct locally so struct_gep can resolve the type const env_local = self.builder.alloca(env_struct_ty); // Compute env size const env_byte_size_inner = self.computeEnvSize(capture_list); const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64); // memcpy(local_alloca, env_param, size) const cp_args = self.alloc.dupe(Ref, &.{ env_local, env_param_ref, env_size_val }) catch unreachable; _ = self.builder.emit(.{ .call_builtin = .{ .builtin = inst_mod.BuiltinId.memcpy, .args = cp_args, } }, self.module.types.ptrTo(.void)); for (capture_list, 0..) |cap, i| { // GEP into env struct to get field pointer const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); // Load the captured value into a local alloca const loaded = self.builder.load(field_ptr, cap.ty); const slot = self.builder.alloca(cap.ty); self.builder.store(slot, loaded); lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true }); } } // Also need parent scope for function lookups (but not variable lookups) // Set up fn_names from parent scope chain { var s: ?*Scope = saved_scope; while (s) |scope| { var it = scope.fn_names.iterator(); while (it.next()) |e| { if (!lambda_scope.fn_names.contains(e.key_ptr.*)) { lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {}; } } s = scope.parent; } } // Bind params for (lam.params, 0..) |p, i| { const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = @as(Ref, @enumFromInt(i + 1)); // +1: env is param 0 self.builder.store(slot, param_ref); lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } // Lower body — capture last expression as return value if (ret_ty != .void) { if (self.lowerBlockValue(lam.body)) |val| { if (!self.currentBlockHasTerminator()) { const val_ty = self.builder.getRefType(val); const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } } } else { self.lowerBlock(lam.body); } self.ensureTerminator(ret_ty); self.builder.finalize(); // Restore builder state self.scope = saved_scope; lambda_scope.deinit(); self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; // Create proper closure type (user-visible params only, no env) var param_types_list = std.ArrayList(TypeId).empty; for (params.items[1..]) |p| { // skip env (index 0) param_types_list.append(self.alloc, p.ty) catch unreachable; } const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty); // Build env and closure in the caller's scope if (capture_list.len > 0) { // Alloca env struct on stack (so struct_gep can resolve the type) const env_local = self.builder.alloca(env_struct_ty); // Store captured values into env struct fields for (capture_list, 0..) |cap, i| { const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); const val = if (cap.is_alloca) self.builder.load(cap.ref, cap.ty) else cap.ref; self.builder.store(gep, val); } // Copy env to heap (so it outlives the stack frame) const env_byte_size = self.computeEnvSize(capture_list); const env_size = self.builder.constInt(@intCast(env_byte_size), .s64); const ptr_void = self.module.types.ptrTo(.void); const env_heap = self.builder.emit(.{ .heap_alloc = .{ .operand = env_size } }, ptr_void); // memcpy(heap, stack_alloca, size) const args = self.alloc.dupe(Ref, &.{ env_heap, env_local, env_size }) catch unreachable; _ = self.builder.emit(.{ .call_builtin = .{ .builtin = inst_mod.BuiltinId.memcpy, .args = args, } }, ptr_void); return self.builder.closureCreate(func_id, env_heap, closure_ty); } else { return self.builder.closureCreate(func_id, Ref.none, closure_ty); } } /// Create a trampoline function that wraps a bare function for closure auto-promotion. /// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the /// bare function with `(args...)`, ignoring the env parameter. fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId { // Build trampoline params: env + closure params var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const env_name = self.module.types.internString("env"); params.append(self.alloc, .{ .name = env_name, .ty = self.module.types.ptrTo(.void) }) catch unreachable; for (closure_info.params, 0..) |pty, i| { var buf: [32]u8 = undefined; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } // Generate unique trampoline name const bare_func = self.module.functions.items[bare_func_id.index()]; const bare_name = self.module.types.getString(bare_func.name); var name_buf: [128]u8 = undefined; const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp"; const tramp_name_id = self.module.types.internString(tramp_name); // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; // Create function const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; const func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret); const func_id = self.module.addFunction(func); self.builder.func = func_id; self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1 const entry_name = self.module.types.internString("entry"); const entry_block = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry_block); // Build call args: skip env (param 0), forward params 1..N var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); for (closure_info.params, 0..) |_, i| { call_args.append(self.alloc, Ref.fromIndex(@intCast(i + 1))) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret); // Return result (or void) if (closure_info.ret != .void) { self.builder.ret(result, closure_info.ret); } else { self.builder.retVoid(); } self.builder.finalize(); // Restore builder state self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Walk an AST node and collect free variable references (identifiers that are /// in the current scope but not in lambda params). fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void { switch (node.data) { .identifier => |id| { // Skip lambda params if (param_names.contains(id.name)) return; // Skip function names if (self.fn_ast_map.contains(id.name)) return; // Skip type names if (self.struct_template_map.contains(id.name)) return; // Check if it's a variable in the parent scope if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { captures.append(self.alloc, .{ .name = id.name, .ty = binding.ty, .ref = binding.ref, .is_alloca = binding.is_alloca, }) catch {}; } } }, .binary_op => |bo| { self.collectCaptures(bo.lhs, param_names, captures); self.collectCaptures(bo.rhs, param_names, captures); }, .unary_op => |uo| { self.collectCaptures(uo.operand, param_names, captures); }, .call => |cl| { self.collectCaptures(cl.callee, param_names, captures); for (cl.args) |arg| { self.collectCaptures(arg, param_names, captures); } }, .block => |blk| { for (blk.stmts) |stmt| { self.collectCaptures(stmt, param_names, captures); } }, .if_expr => |ie| { self.collectCaptures(ie.condition, param_names, captures); self.collectCaptures(ie.then_branch, param_names, captures); if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures); }, .while_expr => |we| { self.collectCaptures(we.condition, param_names, captures); self.collectCaptures(we.body, param_names, captures); }, .return_stmt => |rs| { if (rs.value) |v| self.collectCaptures(v, param_names, captures); }, .var_decl => |vd| { if (vd.value) |v| self.collectCaptures(v, param_names, captures); // Register the local var name so it's not captured param_names.put(vd.name, {}) catch {}; }, .const_decl => |cd| { self.collectCaptures(cd.value, param_names, captures); param_names.put(cd.name, {}) catch {}; }, .assignment => |a| { self.collectCaptures(a.target, param_names, captures); self.collectCaptures(a.value, param_names, captures); }, .destructure_decl => |dd| { self.collectCaptures(dd.value, param_names, captures); for (dd.names) |name| { param_names.put(name, {}) catch {}; } }, .field_access => |fa| { self.collectCaptures(fa.object, param_names, captures); }, .index_expr => |ie| { self.collectCaptures(ie.object, param_names, captures); self.collectCaptures(ie.index, param_names, captures); }, .struct_literal => |sl| { for (sl.field_inits) |fi| { self.collectCaptures(fi.value, param_names, captures); } }, .array_literal => |al| { for (al.elements) |elem| { self.collectCaptures(elem, param_names, captures); } }, .lambda => |inner_lam| { // For nested lambdas, the inner lambda captures from our scope too // But its own params should be excluded var inner_params = std.StringHashMap(void).init(self.alloc); defer inner_params.deinit(); // Copy current param_names var it = param_names.iterator(); while (it.next()) |e| { inner_params.put(e.key_ptr.*, {}) catch {}; } for (inner_lam.params) |p| { inner_params.put(p.name, {}) catch {}; } self.collectCaptures(inner_lam.body, &inner_params, captures); }, .match_expr => |me| { self.collectCaptures(me.subject, param_names, captures); for (me.arms) |arm| { self.collectCaptures(arm.body, param_names, captures); } }, .null_coalesce => |nc| { self.collectCaptures(nc.lhs, param_names, captures); self.collectCaptures(nc.rhs, param_names, captures); }, .deref_expr => |de| { self.collectCaptures(de.operand, param_names, captures); }, .for_expr => |fe| { self.collectCaptures(fe.iterable, param_names, captures); // Register capture name as local so it's not captured param_names.put(fe.capture_name, {}) catch {}; self.collectCaptures(fe.body, param_names, captures); }, .slice_expr => |se| { self.collectCaptures(se.object, param_names, captures); if (se.start) |s| self.collectCaptures(s, param_names, captures); if (se.end) |e| self.collectCaptures(e, param_names, captures); }, .tuple_literal => |tl| { for (tl.elements) |elem| { self.collectCaptures(elem.value, param_names, captures); } }, .force_unwrap => |fu| { self.collectCaptures(fu.operand, param_names, captures); }, .chained_comparison => |cc| { for (cc.operands) |op| { self.collectCaptures(op, param_names, captures); } }, .defer_stmt => |ds| { self.collectCaptures(ds.expr, param_names, captures); }, .ffi_intrinsic_call => |fic| { self.collectCaptures(fic.return_type, param_names, captures); for (fic.args) |arg| { self.collectCaptures(arg, param_names, captures); } }, else => {}, } } /// Compute the byte size of the env struct based on captured value types. fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize { // Must match LLVM's struct layout: fields are aligned to their natural alignment var offset: usize = 0; var max_align: usize = 1; for (capture_list) |cap| { const field_size = self.typeSizeBytes(cap.ty); const field_align = self.typeAlignBytes(cap.ty); if (field_align > max_align) max_align = field_align; // Align offset to field alignment offset = (offset + field_align - 1) & ~(field_align - 1); offset += field_size; } // Align total to max field alignment (matches LLVM's struct alignment) return (offset + max_align - 1) & ~(max_align - 1); } /// Byte size of an IR type matching LLVM's type layout. fn typeSizeBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeSizeBytes(ty); } fn typeAlignBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeAlignBytes(ty); } fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId { if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types); return .void; } // ── Chained comparison ────────────────────────────────────────── 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; } 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), }; } // ── Defer/Push/MultiAssign ────────────────────────────────────── fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void { // Push deferred expression onto the stack — will be emitted at block exit in LIFO order self.defer_stack.append(self.alloc, ds.expr) catch {}; } /// Emit deferred expressions from saved_len..current in reverse (LIFO) order, /// then truncate the defer stack back to saved_len. fn emitBlockDefers(self: *Lowering, saved_len: usize) void { // Guard: if stack was already drained (e.g., by a return that emitted all defers) if (saved_len > self.defer_stack.items.len) return; if (self.currentBlockHasTerminator()) { // Block already terminated (e.g., by return) — defers were already emitted self.defer_stack.shrinkRetainingCapacity(saved_len); return; } const stack = self.defer_stack.items; var i = stack.len; while (i > saved_len) { i -= 1; _ = self.lowerExpr(stack[i]); } self.defer_stack.shrinkRetainingCapacity(saved_len); } fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void { // push context_expr { body } // → save = global_get(context), global_set(context, new_val), body, global_set(context, save) const gi = self.global_names.get("context") orelse { // No context global — just lower the body without push/pop self.lowerBlock(ps.body); return; }; // Save current context const save = self.builder.emit(.{ .global_get = gi.id }, gi.ty); // Lower the new context value const ctx_val = self.lowerExpr(ps.context_expr); // Store into context global self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = ctx_val } }, .void); // Lower the body self.lowerBlock(ps.body); // Restore saved context self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = save } }, .void); } fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void { // Evaluate all RHS values first, then assign to LHS targets var vals = std.ArrayList(Ref).empty; defer vals.deinit(self.alloc); for (ma.values) |v| { vals.append(self.alloc, self.lowerExpr(v)) catch unreachable; } for (ma.targets, 0..) |target, i| { if (i >= vals.items.len) break; const val = vals.items[i]; switch (target.data) { .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) self.coerceToType(val, val_ty, binding.ty) else val; self.builder.store(binding.ref, store_val); } } } }, .index_expr => |ie| { 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); const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void) self.coerceToType(val, val_ty, elem_ty) else val; // For fixed-size arrays, use the alloca pointer directly const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } else { const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); self.builder.store(gep, store_val); } }, .field_access => |fa| { const obj_ptr = self.lowerExprAsPtr(fa.object); const obj_ty = self.inferExprType(fa.object); const field_name_id = self.module.types.internString(fa.field); const struct_fields = self.getStructFields(obj_ty); var field_idx: u32 = 0; var field_ty: TypeId = .s64; for (struct_fields, 0..) |f, fi| { if (f.name == field_name_id) { field_idx = @intCast(fi); field_ty = f.ty; break; } } const gep = self.builder.structGepTyped(obj_ptr, field_idx, field_ty, obj_ty); const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != field_ty and val_ty != .void and field_ty != .void) self.coerceToType(val, val_ty, field_ty) else val; self.builder.store(gep, store_val); }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); const pointee_ty = blk: { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) break :blk info.pointer.pointee; } break :blk ptr_ty; }; const val_ty = self.builder.getRefType(val); const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) self.coerceToType(val, val_ty, pointee_ty) else val; self.builder.store(ptr, store_val); }, else => { _ = self.emitError("multi_assign_target", target.span); }, } } } fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { // Lower the RHS expression (must produce a tuple) const saved_fbv = self.force_block_value; self.force_block_value = true; const ref = self.lowerExpr(dd.value); self.force_block_value = saved_fbv; const ty = self.builder.getRefType(ref); // Get tuple field info if (ty.isBuiltin()) return; const ti = self.module.types.get(ty); if (ti != .tuple) return; const tuple = ti.tuple; if (dd.names.len > tuple.fields.len) return; // Extract each field and bind to a new variable for (dd.names, 0..) |name, i| { if (std.mem.eql(u8, name, "_")) continue; // discard const field_ty = tuple.fields[i]; const field_val = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = ty, } }, field_ty); const slot = self.builder.alloca(field_ty); self.builder.store(slot, field_val); if (self.scope) |scope| { scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true }); } } } // ── Comptime lowering ──────────────────────────────────────────── /// Lower a `#run expr` that appears as a top-level constant binding: /// NAME :: #run expr; /// Creates a comptime function wrapping the expression (for later /// interpretation), plus a global constant to hold the result. fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void { const ret_ty = self.resolveType(type_ann); const func_id = self.createComptimeFunction(name, expr, ret_ty); // Add a global constant whose initializer will be filled by the interpreter. const name_id = self.module.types.internString(name); const gid = self.module.addGlobal(.{ .name = name_id, .ty = ret_ty, .init_val = null, // will be filled by interpreter at emit time .is_const = true, .comptime_func = func_id, }); // Register for runtime lookup: identifier resolution emits global_get self.global_names.put(name, .{ .id = gid, .ty = ret_ty }) catch {}; } /// Lower a standalone `#run expr;` at the top level (side-effect only). /// Creates a comptime function that the interpreter should execute. fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void { _ = self.createComptimeFunction("__run", expr, .void); } /// Lower a `#run expr` that appears inline within an expression. /// Creates a comptime function and emits a `call` to it, so the /// interpreter can evaluate it and replace with the constant result. fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref { const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr); const func_id = self.createComptimeFunction("__ct", expr, ret_ty); // Emit a call to the comptime function. At interpretation time, // this will be evaluated and the result inlined as a constant. return self.builder.call(func_id, &.{}, ret_ty); } /// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get /// a string, parses it as sx code, and lowers each statement inline. fn lowerInsertExpr(self: *Lowering, expr: *const Node) void { _ = self.lowerInsertExprValue(expr); } /// Like lowerInsertExpr but returns the value of the last parsed expression. fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref { // Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal) const substituted = if (self.comptime_param_nodes) |cpn| self.substituteComptimeNodes(expr, cpn) catch expr else expr; // Step 2: Evaluate the expression to get a string const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void); // Step 3: Parse the string as sx code and lower each statement // The last expression's value is captured as the return value var p = parser_mod.Parser.init(self.alloc, code_str); var last_val: Ref = self.builder.constInt(0, .void); while (p.current.tag != .eof) { const stmt = p.parseStmt() catch break; if (p.current.tag == .eof) { // Last statement — try to capture as expression value // Note: tryLowerAsExpr internally calls lowerStmt for statement nodes, // so we must NOT call lowerStmt again in the else branch. if (self.tryLowerAsExpr(stmt)) |val| { last_val = val; } } else { self.lowerStmt(stmt); } } return last_val; } /// Evaluate an expression at compile time, returning its string value. /// Returns null if evaluation fails. fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 { // Case 1: String literal — return it directly (no need for interpreter) if (expr.data == .string_literal) { const lit = expr.data.string_literal; const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; return self.alloc.dupeZ(u8, str) catch null; } // Case 2: Evaluate via IR interpreter // Build a targeted comptime module with only the needed functions var ct_module = Module.init(self.alloc); var ct_lowering = Lowering.init(&ct_module); ct_lowering.main_file = null; // no main file filtering ct_lowering.comptime_param_nodes = self.comptime_param_nodes; ct_lowering.fn_ast_map = self.fn_ast_map; // share AST map for lazy resolution // Lower only the functions reachable from this expression. // For a call like build_format(fmt), we need build_format's AST. if (expr.data == .call) { self.lowerComptimeDeps(&ct_lowering, expr); } // Create a comptime function that evaluates the expression const ct_func_id = ct_lowering.createComptimeFunction("__insert", expr, .string); // Run the interpreter var interp = interp_mod.Interpreter.init(&ct_module, self.alloc); defer interp.deinit(); const result = interp.call(ct_func_id, &.{}) catch return null; // Extract string value const str = result.asString(&interp) orelse switch (result) { .string => |s| s, else => return null, }; return self.alloc.dupeZ(u8, str) catch null; } /// Lower the direct callee of a comptime expression into the ct module. /// Transitive dependencies are resolved lazily via the shared fn_ast_map. fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void { if (expr.data != .call) return; if (expr.data.call.callee.data != .identifier) return; const name = expr.data.call.callee.data.identifier.name; if (resolveBuiltin(name) != null) return; if (self.fn_ast_map.get(name)) |fd| { if (ct.resolveFuncByName(name) == null) { ct.lowerFunction(fd, name, false); } } } /// Substitute comptime parameter identifiers with their actual AST nodes. fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node { // Direct identifier match if (node.data == .identifier) { if (cpn.get(node.data.identifier.name)) |replacement| { return replacement; } } // Recurse into call arguments if (node.data == .call) { var changed = false; const new_args = try self.alloc.alloc(*Node, node.data.call.args.len); for (node.data.call.args, 0..) |arg, i| { const substituted = try self.substituteComptimeNodes(arg, cpn); new_args[i] = @constCast(substituted); if (substituted != arg) changed = true; } if (changed) { const new_node = try self.alloc.create(Node); new_node.* = .{ .span = node.span, .data = .{ .call = .{ .callee = node.data.call.callee, .args = new_args, } }, }; return new_node; } } return node; } /// Lower a call to a function with comptime params by inlining its body. /// Comptime params are substituted, `#insert` expressions are evaluated. fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { // Build comptime param substitution map: param_name → call_site AST node var cpn = std.StringHashMap(*const Node).init(self.alloc); var call_arg_idx: usize = 0; for (fd.params) |param| { if (param.is_variadic) { // Variadic param: pack remaining call args into []Any slice self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx); break; // variadic is always the last param } if (call_arg_idx >= call_node.args.len) break; if (param.is_comptime) { cpn.put(param.name, call_node.args[call_arg_idx]) catch {}; call_arg_idx += 1; } else { const arg_val = self.lowerExpr(call_node.args[call_arg_idx]); const pty = self.resolveParamType(¶m); const slot = self.builder.alloca(pty); self.builder.store(slot, arg_val); if (self.scope) |scope| { scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); } call_arg_idx += 1; } } // Also bind comptime params as local string variables (for `fmt` used in runtime code) var cpn_iter = cpn.iterator(); while (cpn_iter.next()) |entry| { const param_name = entry.key_ptr.*; const param_node = entry.value_ptr.*; if (param_node.data == .string_literal) { // Create a local string variable with the literal value const str_ref = self.lowerExpr(param_node); const slot = self.builder.alloca(.string); self.builder.store(slot, str_ref); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true }); } } } // Install comptime param nodes and lower the function body inline const saved_cpn = self.comptime_param_nodes; self.comptime_param_nodes = cpn; defer self.comptime_param_nodes = saved_cpn; // Lower the body — capture return value for functions with return type const ret_ty = self.resolveReturnType(fd); if (ret_ty != .void) { if (self.lowerBlockValue(fd.body)) |val| { return val; } } else { self.lowerBlock(fd.body); } return self.builder.constInt(0, .void); } /// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, /// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { const any_slice_ty = self.module.types.sliceOf(.any); const n = if (call_args.len > start_idx) call_args.len - start_idx else 0; if (n == 0) { // Empty slice: {null, 0} const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any)); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(any_slice_ty); // Store ptr (field 0) and len (field 1) into the slice alloca const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, zero_len); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); } return; } // Allocate stack array [N x Any] const array_ty = self.module.types.arrayOf(.any, @intCast(n)); const array_slot = self.builder.alloca(array_ty); // Box each arg and store into array for (call_args[start_idx..], 0..) |arg, i| { var val = self.lowerExpr(arg); var source_ty = self.inferExprType(arg); // If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that if (source_ty == .s64) { const ref_ty = self.builder.getRefType(val); if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) { source_ty = ref_ty; } else if (!ref_ty.isBuiltin()) { const ri = self.module.types.get(ref_ty); if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) { source_ty = ref_ty; } } } // Auto-unwrap optionals: box inner value if present, else box string "null" if (!source_ty.isBuiltin()) { const opt_info = self.module.types.get(source_ty); if (opt_info == .optional) { const child_ty = opt_info.optional.child; const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); const some_bb = self.freshBlock("opt.some"); const none_bb = self.freshBlock("opt.none"); const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); const boxed_inner = self.builder.boxAny(unwrapped, child_ty); self.builder.br(merge_bb, &.{boxed_inner}); self.builder.switchToBlock(none_bb); const null_str_id = self.module.types.internString("null"); const null_str = self.builder.constString(null_str_id); const boxed_null = self.builder.boxAny(null_str, .string); self.builder.br(merge_bb, &.{boxed_null}); self.builder.switchToBlock(merge_bb); val = self.builder.blockParam(merge_bb, 0, TypeId.any); source_ty = .any; } } const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty); // GEP to array[i] and store const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any)); self.builder.store(elem_ptr, boxed); } // Build slice {ptr_to_first_element, len} const slice_slot = self.builder.alloca(any_slice_ty); // Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any) const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any)); const len_ref = self.builder.constInt(@intCast(n), .s64); // Store into slice fields const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); self.builder.store(len_gep, len_ref); if (self.scope) |scope| { scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); } } /// Pack variadic args into a slice for regular function calls. /// Detects variadic params in the function decl, packs remaining args into a typed slice, /// and replaces the args list with [fixed_args..., slice_ref]. fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { // Find variadic param index var variadic_idx: ?usize = null; var elem_ty: TypeId = .any; for (fd.params, 0..) |p, i| { if (p.is_variadic) { variadic_idx = i; elem_ty = self.resolveTypeWithBindings(p.type_expr); break; } } const vi = variadic_idx orelse return; // no variadic param // Number of non-variadic args const fixed_count = vi; const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0; const slice_ty = self.module.types.sliceOf(elem_ty); // Check for spread operator: sum(..arr) — single spread arg becomes the slice directly if (variadic_count == 1 and fixed_count < c.args.len) { const arg_node = c.args[fixed_count]; if (arg_node.data == .spread_expr) { const spread = arg_node.data.spread_expr; const arr_val = self.lowerExpr(spread.operand); const arr_ty = self.inferExprType(spread.operand); const arr_info = self.module.types.get(arr_ty); // Convert array to slice const slice_val = switch (arr_info) { .array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty), .slice => arr_val, else => arr_val, }; args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } } if (variadic_count == 0) { // Empty slice const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty)); const zero_len = self.builder.constInt(0, .s64); const slice_slot = self.builder.alloca(slice_ty); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); self.builder.store(ptr_gep, null_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); self.builder.store(len_gep, zero_len); const slice_val = self.builder.load(slice_slot, slice_ty); // Replace args: keep fixed args, append slice args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; return; } // Determine if we need to box as Any (for ..Any params) or use raw type const is_any = (elem_ty == .any); // Allocate stack array [N x ElemType] const array_elem = if (is_any) TypeId.any else elem_ty; const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count)); const array_slot = self.builder.alloca(array_ty); // Store each variadic arg into array for (0..variadic_count) |i| { var val = args.items[fixed_count + i]; if (is_any) { var source_ty = self.inferExprType(c.args[fixed_count + i]); // If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that if (source_ty == .s64) { const ref_ty = self.builder.getRefType(val); if (ref_ty != .s64 and ref_ty != .void) source_ty = ref_ty; } // Auto-unwrap optionals: box inner value if present, else box string "null" if (!source_ty.isBuiltin()) { const opt_info = self.module.types.get(source_ty); if (opt_info == .optional) { const child_ty = opt_info.optional.child; // Branch: has_value? → box inner : box "null" const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); const some_bb = self.freshBlock("opt.some"); const none_bb = self.freshBlock("opt.none"); const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); // Some: unwrap and box inner value self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); const boxed_inner = self.builder.boxAny(unwrapped, child_ty); self.builder.br(merge_bb, &.{boxed_inner}); // None: box the string "null" self.builder.switchToBlock(none_bb); const null_str_id = self.module.types.internString("null"); const null_str = self.builder.constString(null_str_id); const boxed_null = self.builder.boxAny(null_str, .string); self.builder.br(merge_bb, &.{boxed_null}); // Merge self.builder.switchToBlock(merge_bb); val = self.builder.blockParam(merge_bb, 0, TypeId.any); source_ty = .any; // already boxed } } if (source_ty != .any) { val = self.builder.boxAny(val, source_ty); } } const idx_ref = self.builder.constInt(@intCast(i), .s64); const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem)); self.builder.store(elem_ptr, val); } // Build slice {ptr, len} const slice_slot = self.builder.alloca(slice_ty); const zero = self.builder.constInt(0, .s64); const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem)); const len_ref = self.builder.constInt(@intCast(variadic_count), .s64); const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty); self.builder.store(ptr_gep, data_ptr); const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); self.builder.store(len_gep, len_ref); const slice_val = self.builder.load(slice_slot, slice_ty); // Replace args: keep fixed args, append slice args.shrinkRetainingCapacity(fixed_count); args.append(self.alloc, slice_val) catch unreachable; } // ── Generic monomorphization ────────────────────────────────── /// Lower a call to a generic function by monomorphizing it with inferred type arguments. fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref { // Infer type param bindings from call arguments var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); // Determine if type args are passed explicitly: // If call_node.args.len == fd.params.len, the caller passed type args explicitly // (e.g., are_equal(Point, p1, p2)). Otherwise, types are inferred from value args // (e.g., are_equal(p1, p2)). const types_passed_explicitly = call_node.args.len == fd.params.len; for (fd.type_params) |tp| { var found = false; // Strategy 1: Direct type param declaration ($T: Type) // The param whose name matches the type param IS the declaration. // The call arg at that position is a type expression — resolve it directly. // Only applies when type args are passed explicitly in the call. if (types_passed_explicitly) { for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { // This param IS the type param declaration if (pi < call_node.args.len) { const ty = self.resolveTypeArg(call_node.args[pi]); bindings.put(tp.name, ty) catch {}; found = true; } break; } } } if (found) continue; // Strategy 2: Infer from params that USE the type param (e.g., a: $T, b: T, items: []$T) // Check ALL params whose type matches the type param name, pick widest type. // When types are inferred (not explicit), use a separate arg index that // skips type param declarations to correctly map params to call args. var inferred_ty: ?TypeId = null; var s2_arg_idx: usize = 0; for (fd.params) |param| { const is_type_decl = isTypeParamDecl(¶m, fd.type_params); defer if (!is_type_decl) { s2_arg_idx += 1; }; if (is_type_decl) { if (types_passed_explicitly) s2_arg_idx += 1; continue; } const matched = self.matchTypeParam(param.type_expr, tp.name); if (matched) { if (s2_arg_idx < call_node.args.len) { const arg_ty = self.inferExprType(call_node.args[s2_arg_idx]); const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name); if (extracted) |ety| { if (inferred_ty) |prev| { if (ety == .f64 and prev != .f64) { inferred_ty = ety; } else if (ety == .f32 and prev != .f64 and prev != .f32) { inferred_ty = ety; } } else { inferred_ty = ety; } } } } } if (inferred_ty) |ty| { bindings.put(tp.name, ty) catch {}; } } // Build mangled name: "func_name__Type1_Type2" var mangled_buf: [256]u8 = undefined; var mangled_len: usize = 0; for (base_name) |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } for (fd.type_params) |tp| { // Append separator for ("__") |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } // Append type name const ty = bindings.get(tp.name) orelse .s64; const type_name_str = self.mangleTypeName(ty); for (type_name_str) |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } } const mangled_name = mangled_buf[0..mangled_len]; // Check cache if (!self.lowered_functions.contains(mangled_name)) { // Monomorphize: create a new function with the mangled name and lower with type bindings self.monomorphizeFunction(fd, mangled_name, &bindings); } // Resolve the monomorphized function and call it (stripping type args) if (self.resolveFuncByName(mangled_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; // Build value-only args (skip type param declaration args) // Use separate index for lowered_args since type params don't consume call args var value_args = std.ArrayList(Ref).empty; defer value_args.deinit(self.alloc); var arg_idx: usize = 0; for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) { // Only skip in lowered_args if types were passed explicitly in the call if (types_passed_explicitly) arg_idx += 1; continue; } if (arg_idx < lowered_args.len) { value_args.append(self.alloc, lowered_args[arg_idx]) catch unreachable; } arg_idx += 1; } self.coerceCallArgs(value_args.items, params); return self.builder.call(fid, value_args.items, ret_ty); } return self.emitError(base_name, call_node.callee.span); } /// Create a monomorphized instance of a generic function. /// Check if a call has a `cast(runtime_var, val)` argument (runtime type dispatch pattern). fn hasCastWithRuntimeType(self: *Lowering, c: *const ast.Call) bool { for (c.args) |arg| { if (arg.data == .call) { if (arg.data.call.callee.data == .identifier) { const name = arg.data.call.callee.data.identifier.name; if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { const type_arg = arg.data.call.args[0]; if (type_arg.data == .identifier) { // It's a runtime type if it's in scope as a variable if (self.scope) |scope| { if (scope.lookup(type_arg.data.identifier.name) != null) return true; } } } } } } return false; } /// Generate runtime dispatch for a generic call inside a type-match arm. /// For each type tag in match_tags, monomorphizes the generic function and calls it. fn lowerRuntimeDispatchCall( self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, match_tags: []const u64, ) Ref { // Find the cast arg: cast(type_var, any_val) var cast_arg_idx: usize = 0; var type_tag_node: ?*const Node = null; var any_val_node: ?*const Node = null; for (call_node.args, 0..) |arg, i| { if (arg.data == .call and arg.data.call.callee.data == .identifier) { const name = arg.data.call.callee.data.identifier.name; if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) { cast_arg_idx = i; type_tag_node = arg.data.call.args[0]; any_val_node = arg.data.call.args[1]; break; } } } // Lower the type tag (runtime value) and Any value BEFORE the switch const type_tag = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span)); const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span)); // Lower non-cast arguments once (before the switch) var other_args = std.ArrayList(?Ref).empty; defer other_args.deinit(self.alloc); for (call_node.args, 0..) |arg, i| { if (i == cast_arg_idx) { other_args.append(self.alloc, null) catch unreachable; // placeholder } else { other_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; } } // Resolve return type (using first available binding) const ret_ty: TypeId = blk: { if (fd.return_type) |rt| { if (rt.data == .type_expr) { if (type_bridge.resolveAstType(rt, &self.module.types) != .s64) { break :blk type_bridge.resolveAstType(rt, &self.module.types); } } } break :blk .string; // default for to_string functions }; const merge_bb = self.freshBlock("dispatch.merge"); const default_bb = self.freshBlock("dispatch.default"); // Build switch cases var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; defer cases.deinit(self.alloc); // For each type tag, create a case block var case_blocks = std.ArrayList(BlockId).empty; defer case_blocks.deinit(self.alloc); for (match_tags) |tag| { const case_bb = self.freshBlock("dispatch.case"); case_blocks.append(self.alloc, case_bb) catch unreachable; cases.append(self.alloc, .{ .value = @intCast(tag), .target = case_bb, .args = &.{}, }) catch unreachable; } // Create a result alloca BEFORE the switch (must be before terminator) var result_slot: ?Ref = null; if (ret_ty != .void) { result_slot = self.builder.alloca(ret_ty); } self.builder.switchBr(type_tag, cases.items, default_bb, &.{}); for (match_tags, 0..) |tag, ti| { self.builder.switchToBlock(case_blocks.items[ti]); const ty_id = TypeId.fromIndex(@intCast(tag)); // Unbox the Any value to the concrete type const unboxed = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, ty_id); if (fd.type_params.len > 0) { // Generic function: build type bindings + monomorphize var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); // Find which type param the cast arg corresponds to if (cast_arg_idx < fd.params.len) { const param_te = fd.params[cast_arg_idx].type_expr; if (param_te.data == .type_expr) { // Direct: `param: $T` → T = ty_id const tp_name = param_te.data.type_expr.name; for (fd.type_params) |tp| { if (std.mem.eql(u8, tp.name, tp_name)) { bindings.put(tp.name, ty_id) catch {}; break; } } } else if (param_te.data == .slice_type_expr) { // Compound: `param: []$T` → T = element type of ty_id const elem_te = param_te.data.slice_type_expr.element_type; if (elem_te.data == .type_expr) { const tp_name = elem_te.data.type_expr.name; for (fd.type_params) |tp| { if (std.mem.eql(u8, tp.name, tp_name)) { const elem_ty = self.getElementType(ty_id); bindings.put(tp.name, if (elem_ty != .void) elem_ty else ty_id) catch {}; break; } } } } else if (param_te.data == .pointer_type_expr) { // Compound: `param: *$T` → T = pointee type of ty_id const pointee_te = param_te.data.pointer_type_expr.pointee_type; if (pointee_te.data == .type_expr) { const tp_name = pointee_te.data.type_expr.name; for (fd.type_params) |tp| { if (std.mem.eql(u8, tp.name, tp_name)) { if (!ty_id.isBuiltin()) { const pinfo = self.module.types.get(ty_id); if (pinfo == .pointer) { bindings.put(tp.name, pinfo.pointer.pointee) catch {}; break; } } bindings.put(tp.name, ty_id) catch {}; break; } } } } } // Build mangled name var mangled_buf: [256]u8 = undefined; var mangled_len: usize = 0; for (base_name) |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } for (fd.type_params) |tp| { for ("__") |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } const bound_ty = bindings.get(tp.name) orelse ty_id; const type_name_str = self.mangleTypeName(bound_ty); for (type_name_str) |ch| { if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; } } } const mangled_name = mangled_buf[0..mangled_len]; // Monomorphize if not already done if (!self.lowered_functions.contains(mangled_name)) { self.monomorphizeFunction(fd, mangled_name, &bindings); } // Build call args (replace cast arg with unboxed value, skip type param decl args) if (self.resolveFuncByName(mangled_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const callee_ret = func.ret; const callee_params = func.params; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); for (fd.params, 0..) |p, pi| { if (isTypeParamDecl(&p, fd.type_params)) continue; if (pi == cast_arg_idx) { call_args.append(self.alloc, unboxed) catch unreachable; } else if (pi < other_args.items.len) { if (other_args.items[pi]) |ref| { call_args.append(self.alloc, ref) catch unreachable; } } } self.coerceCallArgs(call_args.items, callee_params); const result = self.builder.call(fid, call_args.items, callee_ret); if (result_slot) |slot| { self.builder.store(slot, result); } } } else { // Non-generic function: call directly with per-tag unboxing + coercion const resolve_name = base_name; if (!self.lowered_functions.contains(resolve_name)) { self.lazyLowerFunction(resolve_name); } if (self.resolveFuncByName(resolve_name)) |fid| { const callee_ret = self.module.functions.items[@intFromEnum(fid)].ret; const callee_params = self.module.functions.items[@intFromEnum(fid)].params; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); for (fd.params, 0..) |_, pi| { if (pi == cast_arg_idx) { // Coerce unboxed value (typed as ty_id) to param type var arg = unboxed; if (pi < callee_params.len) { arg = self.coerceToType(arg, ty_id, callee_params[pi].ty); } call_args.append(self.alloc, arg) catch unreachable; } else if (pi < other_args.items.len) { if (other_args.items[pi]) |ref| { call_args.append(self.alloc, ref) catch unreachable; } } } // Coerce non-cast args (source type unknown, use s64 default) for (0..@min(call_args.items.len, callee_params.len)) |ci| { if (ci != cast_arg_idx) { call_args.items[ci] = self.coerceToType(call_args.items[ci], .s64, callee_params[ci].ty); } } const result = self.builder.call(fid, call_args.items, callee_ret); if (result_slot) |slot| { self.builder.store(slot, result); } } } self.builder.br(merge_bb, &.{}); } // Default block: store a default value and branch to merge self.builder.switchToBlock(default_bb); if (result_slot) |slot| { const empty_id = self.module.types.internString(""); const default_val = if (ret_ty == .string) self.builder.constString(empty_id) else self.zeroValue(ret_ty); self.builder.store(slot, default_val); } self.builder.br(merge_bb, &.{}); // Merge block: load result self.builder.switchToBlock(merge_bb); if (result_slot) |slot| { return self.builder.load(slot, ret_ty); } return self.builder.constInt(0, .void); } fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { // Mark as lowered before lowering (prevents infinite recursion) // Need to dupe the name since mangled_name may be stack-allocated const owned_name = self.alloc.dupe(u8, mangled_name) catch return; self.lowered_functions.put(owned_name, {}) catch {}; // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; const saved_bindings = self.type_bindings; const saved_defer_base = self.func_defer_base; const saved_block_terminated = self.block_terminated; const saved_target = self.target_type; self.func_defer_base = self.defer_stack.items.len; self.block_terminated = false; // Install type bindings self.type_bindings = bindings.*; // Resolve return type with type bindings active. The body's tail // expression inherits this as its target_type so bare `.{...}` // literals resolve to the monomorphised return type instead of // whatever leaked in from the caller (e.g. caller's xx target). const ret_ty = self.resolveReturnType(fd); self.target_type = ret_ty; // Build param list (substituting type params, skipping type param declarations) var params = std.ArrayList(Function.Param).empty; for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) continue; const pty = self.resolveParamType(&p); params.append(self.alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; } // Create the monomorphized function const name_id = self.module.types.internString(owned_name); const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); _ = func_id; // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create scope and bind params var scope = Scope.init(self.alloc, null); defer scope.deinit(); self.scope = &scope; { var param_idx: u32 = 0; for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) continue; const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(param_idx); self.builder.store(slot, param_ref); scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); param_idx += 1; } } // Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32) if (fd.body.data == .builtin_expr) { // Emit builtin call with param 0, then return if (resolveBuiltin(fd.name)) |bid| { const param0 = Ref.fromIndex(0); const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty); self.builder.ret(result, ret_ty); } else { self.ensureTerminator(ret_ty); } self.builder.finalize(); } else { // Lower the function body if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { const val_ty = self.builder.getRefType(val); const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } else { self.ensureTerminator(ret_ty); } } } else { self.lowerBlock(fd.body); self.ensureTerminator(ret_ty); } self.builder.finalize(); } // Restore builder state self.type_bindings = saved_bindings; self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; self.target_type = saved_target; self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; } // ── Reflection builtins ──────────────────────────────────────── /// Try to lower a call as a reflection builtin (expanded inline during lowering). /// Returns null if the call is not a recognized reflection builtin. fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { if (std.mem.eql(u8, name, "size_of")) { // size_of(T) → const_int(sizeof(T)) const ty = self.resolveTypeArg(c.args[0]); const size: i64 = @intCast(self.typeSizeBytes(ty)); return self.builder.constInt(size, .s64); } if (std.mem.eql(u8, name, "field_count")) { // field_count(T) → const_int(N) const ty = self.resolveTypeArg(c.args[0]); const info = self.module.types.get(ty); const count: i64 = switch (info) { .@"struct" => |s| @intCast(s.fields.len), .@"union" => |u| @intCast(u.fields.len), .tagged_union => |u| @intCast(u.fields.len), .@"enum" => |e| @intCast(e.variants.len), .array => |a| @intCast(a.length), .vector => |v| @intCast(v.length), else => 0, }; return self.builder.constInt(count, .s64); } if (std.mem.eql(u8, name, "type_name")) { // type_name(T) → const_string("TypeName") const ty = self.resolveTypeArg(c.args[0]); const tn_str = self.formatTypeName(ty); const sid = self.module.types.internString(tn_str); return self.builder.constString(sid); } if (std.mem.eql(u8, name, "is_flags")) { const ty = self.resolveTypeArg(c.args[0]); if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .@"enum") return self.builder.constBool(info.@"enum".is_flags); } return self.builder.constBool(false); } if (std.mem.eql(u8, name, "field_name")) { // field_name(T, i) → field_name_get instruction if (c.args.len < 2) return self.builder.constString(self.module.types.internString("")); const ty = self.resolveTypeArg(c.args[0]); const idx = self.lowerExpr(c.args[1]); return self.builder.emit(.{ .field_name_get = .{ .base = .none, .index = idx, .struct_type = ty, } }, .string); } if (std.mem.eql(u8, name, "field_value")) { // field_value(s, i) → field_value_get instruction (structs/unions) // → index_get + box_any (slices/arrays) if (c.args.len < 2) return self.builder.constInt(0, .any); const base = self.lowerExpr(c.args[0]); const idx = self.lowerExpr(c.args[1]); const struct_ty = self.inferExprType(c.args[0]); // For slices, arrays, and vectors, use index_get to access elements if (!struct_ty.isBuiltin()) { const ti = self.module.types.get(struct_ty); if (ti == .slice or ti == .array or ti == .vector) { const elem_ty = self.getElementType(struct_ty); const elem = self.builder.emit(.{ .index_get = .{ .lhs = base, .rhs = idx } }, elem_ty); return self.builder.boxAny(elem, elem_ty); } } return self.builder.emit(.{ .field_value_get = .{ .base = base, .index = idx, .struct_type = struct_ty, } }, .any); } if (std.mem.eql(u8, name, "type_of")) { // type_of(val) — extract Any tag or produce compile-time constant if (c.args.len < 1) return self.builder.constInt(0, .s64); const arg_ty = self.inferExprType(c.args[0]); if (arg_ty == .any) { // Runtime: extract tag field (field 0 of Any {tag: s64, value: s64}) const val = self.lowerExpr(c.args[0]); return self.builder.structGet(val, 0, .s64); } else { // Static: emit type tag as constant return self.builder.constInt(@intCast(@intFromEnum(arg_ty)), .s64); } } if (std.mem.eql(u8, name, "field_index")) { // field_index(T, val) → extract tag from tagged union if (c.args.len < 2) return self.builder.constInt(0, .s64); const val = self.lowerExpr(c.args[1]); // For tagged unions: extract field 0 (the tag) return self.builder.emit(.{ .enum_tag = .{ .operand = val } }, .s64); } if (std.mem.eql(u8, name, "field_value_int")) { // field_value_int(T, i) → lookup enum variant value by index if (c.args.len < 2) return self.builder.constInt(0, .s64); const ty = self.resolveTypeArg(c.args[0]); const idx = self.lowerExpr(c.args[1]); // For enums with explicit values, build a global value array and index into it if (!ty.isBuiltin()) { const ti = self.module.types.get(ty); if (ti == .@"enum") { if (ti.@"enum".explicit_values) |vals| { // Build inline switch: for each index, return the explicit value // Simple approach: build an array of constants and use index_get var elems = std.ArrayList(Ref).empty; defer elems.deinit(self.alloc); for (vals) |v| { elems.append(self.alloc, self.builder.constInt(v, .s64)) catch unreachable; } const arr_ty = self.module.types.arrayOf(.s64, @intCast(vals.len)); const arr = self.builder.structInit(elems.items, arr_ty); return self.builder.emit(.{ .index_get = .{ .lhs = arr, .rhs = idx } }, .s64); } } } // Default: return the index itself (regular enums) return idx; } return null; } /// Resolve a type argument from a call expression. Handles: /// - Type param bindings ($T → concrete type via type_bindings) /// - Direct type names (Vec4 → lookup in TypeTable) /// - type_expr AST nodes fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { switch (node.data) { .identifier => |id| { // Check type bindings first (from generic monomorphization) if (self.type_bindings) |tb| { if (tb.get(id.name)) |ty| return ty; } // Try as a named type by name (resolveAstType doesn't handle .identifier) const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .s64; }, .type_expr => |te| { if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty; return type_bridge.resolveAstType(node, &self.module.types); }, .call => |cl| { // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) return self.resolveTypeCallWithBindings(&cl); }, else => return .s64, } } /// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64"). fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 { // Builtin types: use their canonical name if (ty == .s8) return "s8"; if (ty == .s16) return "s16"; if (ty == .s32) return "s32"; if (ty == .s64) return "s64"; if (ty == .u8) return "u8"; if (ty == .u16) return "u16"; if (ty == .u32) return "u32"; if (ty == .u64) return "u64"; if (ty == .f32) return "f32"; if (ty == .f64) return "f64"; if (ty == .bool) return "bool"; if (ty == .void) return "void"; if (ty == .string) return "string"; if (ty == .any) return "Any"; if (ty == .usize) return "usize"; if (ty == .isize) return "isize"; const info = self.module.types.get(ty); return switch (info) { .@"struct" => |s| self.module.types.getString(s.name), .@"union" => |u| self.module.types.getString(u.name), .tagged_union => |u| self.module.types.getString(u.name), .@"enum" => |e| self.module.types.getString(e.name), .pointer => |p| blk: { const inner = self.formatTypeName(p.pointee); break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer"; }, .many_pointer => |p| blk: { const inner = self.formatTypeName(p.element); break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer"; }, .slice => |s| blk: { const inner = self.formatTypeName(s.element); break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice"; }, .array => |a| blk: { const inner = self.formatTypeName(a.element); break :blk std.fmt.allocPrint(self.alloc, "[{d}]{s}", .{ a.length, inner }) catch "array"; }, .signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed", .unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned", .optional => |o| blk: { const inner = self.formatTypeName(o.child); break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional"; }, .vector => |v| blk: { const inner = self.formatTypeName(v.element); break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector"; }, else => @tagName(info), }; } /// Format a function type string like "() -> s32" or "(s32, s32) -> s32". fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 { var buf: [512]u8 = undefined; var pos: usize = 0; buf[pos] = '('; pos += 1; for (fd.params, 0..) |p, i| { if (i > 0) { @memcpy(buf[pos..][0..2], ", "); pos += 2; } const pty = self.resolveParamType(&p); const name = self.formatTypeName(pty); @memcpy(buf[pos..][0..name.len], name); pos += name.len; } buf[pos] = ')'; pos += 1; const ret_ty = self.resolveReturnType(fd); if (ret_ty != .void) { @memcpy(buf[pos..][0..4], " -> "); pos += 4; const rname = self.formatTypeName(ret_ty); @memcpy(buf[pos..][0..rname.len], rname); pos += rname.len; } const result = self.alloc.alloc(u8, pos) catch unreachable; @memcpy(result, buf[0..pos]); return result; } /// Format a type name for function name mangling (identifier-safe). /// E.g. *Point → "ptr_Point", []s32 → "slice_s32", [3]f64 → "array_3_f64". /// Check if a param type expression references a type param name (possibly nested). fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool { return switch (type_node.data) { .type_expr => |te| std.mem.eql(u8, te.name, tp_name), .identifier => |id| std.mem.eql(u8, id.name, tp_name), .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), else => false, }; } fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool { return switch (type_node.data) { .type_expr => |te| std.mem.eql(u8, te.name, tp_name), .identifier => |id| std.mem.eql(u8, id.name, tp_name), .slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name), .pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name), .many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name), .optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name), .array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name), else => false, }; } /// Extract the concrete type that corresponds to a type param from an arg type. /// E.g., param type []$T with arg type []s64 → T = s64. fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId { return switch (type_node.data) { .type_expr => |te| if (std.mem.eql(u8, te.name, tp_name)) arg_ty else null, .identifier => |id| if (std.mem.eql(u8, id.name, tp_name)) arg_ty else null, .slice_type_expr => |st| blk: { // arg_ty should be a slice → extract element type if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name), else => null, }; }, .pointer_type_expr => |pt| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name), else => null, }; }, .many_pointer_type_expr => |mp| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name), else => null, }; }, .optional_type_expr => |ot| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name), else => null, }; }, .array_type_expr => |at| blk: { if (arg_ty.isBuiltin()) break :blk null; const info = self.module.types.get(arg_ty); break :blk switch (info) { .array => |a| self.extractTypeParam(at.element_type, a.element, tp_name), else => null, }; }, else => null, }; } fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 { // Builtin types if (ty == .s8) return "s8"; if (ty == .s16) return "s16"; if (ty == .s32) return "s32"; if (ty == .s64) return "s64"; if (ty == .u8) return "u8"; if (ty == .u16) return "u16"; if (ty == .u32) return "u32"; if (ty == .u64) return "u64"; if (ty == .f32) return "f32"; if (ty == .f64) return "f64"; if (ty == .bool) return "bool"; if (ty == .void) return "void"; if (ty == .string) return "string"; if (ty == .any) return "Any"; if (ty == .usize) return "usize"; if (ty == .isize) return "isize"; const info = self.module.types.get(ty); return switch (info) { .@"struct" => |s| self.module.types.getString(s.name), .@"union" => |u| self.module.types.getString(u.name), .tagged_union => |u| self.module.types.getString(u.name), .@"enum" => |e| self.module.types.getString(e.name), .pointer => |p| blk: { const inner = self.mangleTypeName(p.pointee); break :blk std.fmt.allocPrint(self.alloc, "ptr_{s}", .{inner}) catch "pointer"; }, .many_pointer => |p| blk: { const inner = self.mangleTypeName(p.element); break :blk std.fmt.allocPrint(self.alloc, "mptr_{s}", .{inner}) catch "many_pointer"; }, .slice => |s| blk: { const inner = self.mangleTypeName(s.element); break :blk std.fmt.allocPrint(self.alloc, "SL_{s}", .{inner}) catch "slice"; }, .array => |a| blk: { const inner = self.mangleTypeName(a.element); break :blk std.fmt.allocPrint(self.alloc, "AR_{d}_{s}", .{ a.length, inner }) catch "array"; }, .signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed", .unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned", .optional => |o| blk: { const inner = self.mangleTypeName(o.child); break :blk std.fmt.allocPrint(self.alloc, "opt_{s}", .{inner}) catch "optional"; }, .vector => |v| blk: { const inner = self.mangleTypeName(v.element); break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector"; }, .closure => |c| self.mangleParamList("cl", c.params, c.ret), .function => |f| self.mangleParamList("fn", f.params, f.ret), .tuple => |t| blk: { var buf = std.ArrayList(u8).empty; buf.appendSlice(self.alloc, "tu") catch break :blk "tuple"; for (t.fields) |fid| { buf.append(self.alloc, '_') catch break :blk "tuple"; buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple"; } break :blk buf.items; }, else => @tagName(info), }; } /// Collect impl entries visible from `current_source_file` — defined in /// the current file or in any module the current file transitively /// imports. Falls open (returns all entries) when the source-file /// context or import graph isn't wired (e.g. comptime callers). fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void { const here = self.current_source_file orelse { out.appendSlice(self.alloc, entries) catch {}; return; }; const graph = self.import_graph orelse { out.appendSlice(self.alloc, entries) catch {}; return; }; // BFS over the import graph to compute the visible set. var visible = std.StringHashMap(void).init(self.alloc); defer visible.deinit(); visible.put(here, {}) catch {}; var queue = std.ArrayList([]const u8).empty; defer queue.deinit(self.alloc); queue.append(self.alloc, here) catch {}; var head: usize = 0; while (head < queue.items.len) : (head += 1) { const node = queue.items[head]; const direct = graph.get(node) orelse continue; var it = direct.iterator(); while (it.next()) |kv| { const next = kv.key_ptr.*; if (visible.contains(next)) continue; visible.put(next, {}) catch {}; queue.append(self.alloc, next) catch {}; } } for (entries) |e| { if (visible.contains(e.defining_module)) { out.append(self.alloc, e) catch {}; } } } fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 { var buf = std.ArrayList(u8).empty; buf.appendSlice(self.alloc, prefix) catch return prefix; for (params) |p| { buf.append(self.alloc, '_') catch return prefix; buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix; } buf.appendSlice(self.alloc, "__") catch return prefix; buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix; return buf.items; } /// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. /// Returns a list of TypeId index values that match the category. fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { var tags = std.ArrayList(u64).empty; // Fixed builtin categories if (std.mem.eql(u8, name, "int")) { tags.append(self.alloc, TypeId.s8.index()) catch {}; tags.append(self.alloc, TypeId.s16.index()) catch {}; tags.append(self.alloc, TypeId.s32.index()) catch {}; tags.append(self.alloc, TypeId.s64.index()) catch {}; tags.append(self.alloc, TypeId.u8.index()) catch {}; tags.append(self.alloc, TypeId.u16.index()) catch {}; tags.append(self.alloc, TypeId.u32.index()) catch {}; tags.append(self.alloc, TypeId.u64.index()) catch {}; tags.append(self.alloc, TypeId.usize.index()) catch {}; tags.append(self.alloc, TypeId.isize.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "float")) { tags.append(self.alloc, TypeId.f32.index()) catch {}; tags.append(self.alloc, TypeId.f64.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "bool")) { tags.append(self.alloc, TypeId.bool.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "string")) { tags.append(self.alloc, TypeId.string.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "void")) { tags.append(self.alloc, TypeId.void.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) { tags.append(self.alloc, TypeId.any.index()) catch {}; return tags.items; } // Dynamic categories: scan TypeTable for matching types const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector }; const cat: ?Category = if (std.mem.eql(u8, name, "struct")) .@"struct" else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union")) .@"enum" else if (std.mem.eql(u8, name, "slice")) .slice else if (std.mem.eql(u8, name, "array")) .array else if (std.mem.eql(u8, name, "pointer")) .pointer else if (std.mem.eql(u8, name, "vector")) .vector else null; if (cat) |c| { for (self.module.types.infos.items, 0..) |info, idx| { const matches = switch (c) { .@"struct" => info == .@"struct", .@"enum" => info == .@"enum" or info == .tagged_union, .@"union" => info == .@"union" or info == .tagged_union, .slice => info == .slice, .array => info == .array, .pointer => info == .pointer or info == .many_pointer, .vector => info == .vector, }; if (matches) { tags.append(self.alloc, @intCast(idx)) catch {}; } } } // Specific type name (e.g., Point, Color) — look up in type registry if (tags.items.len == 0) { const name_id = self.module.types.internString(name); if (self.module.types.findByName(name_id)) |tid| { tags.append(self.alloc, tid.index()) catch {}; } } return tags.items; } /// Check if a match expression is a type-category match (patterns are type/category names). fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { // Infer result type from the first non-null arm body. // If we skip null_literal arms and find a concrete type T, and there // were null arms, the result is ?T (optional). var has_null = false; for (me.arms) |arm| { const last_node = if (arm.body.data == .block) blk: { if (arm.body.data.block.stmts.len > 0) { break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1]; } break :blk arm.body; } else arm.body; if (last_node.data == .null_literal) { has_null = true; continue; } // First non-null arm determines the type (same as old behavior) const arm_ty = self.inferExprType(last_node); if (has_null and arm_ty != .void) { return self.module.types.optionalOf(arm_ty); } return arm_ty; } return .void; } fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { for (me.arms) |arm| { if (arm.pattern) |pat| { const name = switch (pat.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => continue, }; const categories = [_][]const u8{ "int", "float", "bool", "string", "void", "type", "Type", "struct", "enum", "union", "slice", "array", "pointer", "vector", }; for (categories) |cat| { if (std.mem.eql(u8, name, cat)) return true; } // Also match specific struct/enum type names (e.g., case Point:) if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true; } } return false; } /// Resolve parameter types for a call expression (for target_type context). /// Returns empty slice if the function can't be resolved. fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call) []const TypeId { // Method calls: obj.method(args) — resolve param types from the method signature, // skipping the first param (self) since it's prepended later. if (c.callee.data == .field_access) { const fa = c.callee.data.field_access; const obj_ty = self.inferExprType(fa.object); // Protocol-typed receiver: look up the method on the protocol decl. The // protocol's ProtocolMethodInfo.param_types already excludes self. if (self.getProtocolInfo(obj_ty)) |proto_info| { for (proto_info.methods) |m| { if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; } } // Optional-protocol receiver (`?GPU`): same as above but the // protocol type sits inside the optional's payload. if (!obj_ty.isBuiltin()) { const opt_info = self.module.types.get(obj_ty); if (opt_info == .optional) { if (self.getProtocolInfo(opt_info.optional.child)) |proto_info| { for (proto_info.methods) |m| { if (std.mem.eql(u8, m.name, fa.field)) return m.param_types; } } } } // Closure-typed struct field: `c.on(args)` lowers to call_closure on // the field value. Pick up the callee's param types from the closure // type so each arg gets the right target_type during lowering. if (!obj_ty.isBuiltin()) { const field_name_id = self.module.types.internString(fa.field); const struct_fields = self.getStructFields(obj_ty); for (struct_fields) |f| { if (f.name == field_name_id and !f.ty.isBuiltin()) { const fti = self.module.types.get(f.ty); if (fti == .closure) return fti.closure.params; if (fti == .function) return fti.function.params; } } } if (self.getStructTypeName(obj_ty)) |sname| { const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{}; // Try already-lowered functions first if (self.resolveFuncByName(qualified)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; if (func.params.len > 0) { // Skip self param — caller args don't include self var types_list = std.ArrayList(TypeId).empty; for (func.params[1..]) |p| { types_list.append(self.alloc, p.ty) catch unreachable; } return types_list.items; } } // Try AST map (not yet lowered) if (self.fn_ast_map.get(qualified)) |fd| { if (fd.params.len > 0) { var types_list = std.ArrayList(TypeId).empty; for (fd.params[1..]) |p| { types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; } return types_list.items; } } // Try generic struct template method: List__Container.append → List.append // with type bindings from the struct instantiation if (self.struct_instance_template.get(sname)) |tmpl_name| { const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{}; if (self.fn_ast_map.get(tmpl_qualified)) |fd| { if (fd.params.len > 0) { // Temporarily set type_bindings so resolveParamType can substitute T → concrete type const saved_bindings = self.type_bindings; if (self.struct_instance_bindings.getPtr(sname)) |bindings| { self.type_bindings = bindings.*; } var types_list = std.ArrayList(TypeId).empty; for (fd.params[1..]) |p| { types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; } self.type_bindings = saved_bindings; return types_list.items; } } } } return &.{}; } if (c.callee.data != .identifier) return &.{}; const bare_name = c.callee.data.identifier.name; const name = blk: { const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; if (self.ufcs_alias_map.get(bare_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; // Check declared functions if (self.resolveFuncByName(name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; // Return param types (allocated as slice of TypeId) var types_list = std.ArrayList(TypeId).empty; for (func.params) |p| { types_list.append(self.alloc, p.ty) catch unreachable; } return types_list.items; } // Check AST map for function signatures if (self.fn_ast_map.get(name)) |fd| { var types_list = std.ArrayList(TypeId).empty; for (fd.params) |p| { types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; } return types_list.items; } // Check global function pointer variables if (self.global_names.get(bare_name)) |gi| { if (!gi.ty.isBuiltin()) { const ti = self.module.types.get(gi.ty); if (ti == .function) { return ti.function.params; } } } // Check local scope for function pointer variables if (self.scope) |scope| { if (scope.lookup(bare_name)) |binding| { if (!binding.ty.isBuiltin()) { const ti = self.module.types.get(binding.ty); if (ti == .function) { return ti.function.params; } } } } return &.{}; } /// Check if a param is a type param declaration ($T: Type). /// A type param declaration has param.name == one of the type_params names. fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool { for (type_params) |tp| { if (std.mem.eql(u8, param.name, tp.name)) return true; } return false; } /// Check if a function has comptime (non-Type) value parameters. fn hasComptimeParams(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (p.is_comptime) return true; } return false; } /// Creates a temporary function marked `is_comptime = true` that wraps /// the given expression as its return value. Returns the FuncId. pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId { var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix; self.comptime_counter += 1; // Save current builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const saved_scope = self.scope; // Create the comptime function (no params, returns ret_ty) const name_id = self.module.types.internString(name); const func_id = self.builder.beginFunction(name_id, &.{}, ret_ty); // Mark as comptime self.module.getFunctionMut(func_id).is_comptime = true; // Create entry block const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); self.builder.switchToBlock(entry); // Create a scope that chains to the enclosing scope (so the // expression can reference names visible at the #run site). var ct_scope = Scope.init(self.alloc, saved_scope); self.scope = &ct_scope; // Lower the expression and return it const result = self.lowerExpr(expr); if (ret_ty == .void) { self.builder.retVoid(); } else { self.builder.ret(result, ret_ty); } self.builder.finalize(); // Restore builder state self.scope = saved_scope; ct_scope.deinit(); self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } // ── Block helpers ─────────────────────────────────────────────── fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { return self.freshBlockWithParams(prefix, &.{}); } fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix; self.block_counter += 1; const name_id = self.module.types.internString(name); return self.builder.appendBlock(name_id, params); } fn currentBlockHasTerminator(self: *Lowering) bool { const func = self.builder.module.getFunctionMut(self.builder.func.?); const block_idx = self.builder.current_block orelse return true; const block = &func.blocks.items[block_idx.index()]; if (block.insts.items.len > 0) { const last_op = block.insts.items[block.insts.items.len - 1].op; return switch (last_op) { .ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true, else => false, }; } return false; } // ── Type resolution ───────────────────────────────────────────── // Delegates to type_bridge for full AST type node resolution. fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); } // Arrow functions without explicit return type: infer from body expression if (fd.is_arrow) { return self.inferExprType(fd.body); } // No annotation, not arrow: an explicit `return ` statement // wins. Otherwise default to void — the body's tail expression is // a side-effect statement, not an implicit return. if (self.findReturnValueType(fd.body)) |ty| return ty; return .void; } /// Walk a function body and return the type of the first `return ;` /// statement encountered. Does not descend into nested function or lambda /// declarations (those have their own return types). fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId { return switch (node.data) { .return_stmt => |rs| if (rs.value) |v| self.inferExprType(v) else null, .block => |blk| blk: { for (blk.stmts) |s| { if (self.findReturnValueType(s)) |t| break :blk t; } break :blk null; }, .if_expr => |ie| blk: { if (self.findReturnValueType(ie.then_branch)) |t| break :blk t; if (ie.else_branch) |eb| { if (self.findReturnValueType(eb)) |t| break :blk t; } break :blk null; }, .while_expr => |we| self.findReturnValueType(we.body), .for_expr => |fe| self.findReturnValueType(fe.body), .match_expr => |me| blk: { for (me.arms) |arm| { if (self.findReturnValueType(arm.body)) |t| break :blk t; } break :blk null; }, else => null, }; } fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { const elem_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { // Variadic param (..T) → receives a []T slice return self.module.types.sliceOf(elem_ty); } return elem_ty; } fn resolveType(self: *Lowering, type_ann: ?*const Node) TypeId { if (type_ann) |n| return self.resolveTypeWithBindings(n); return .s64; } /// Resolve a type node, checking type_bindings first for generic type params. fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { if (self.type_bindings) |tb| { switch (node.data) { .type_expr => |te| { // Check bindings for any type_expr name — not just those // marked is_generic. The return type `T` in `-> T` may // not have the `$` prefix, so is_generic is false, but // it still refers to the type param. if (tb.get(te.name)) |ty| return ty; }, .identifier => |id| { if (tb.get(id.name)) |ty| return ty; }, // Compound types: resolve inner types with bindings .slice_type_expr => |st| { const elem = self.resolveTypeWithBindings(st.element_type); return self.module.types.sliceOf(elem); }, .pointer_type_expr => |pt| { const pointee = self.resolveTypeWithBindings(pt.pointee_type); return self.module.types.ptrTo(pointee); }, .many_pointer_type_expr => |mp| { const elem = self.resolveTypeWithBindings(mp.element_type); return self.module.types.manyPtrTo(elem); }, .optional_type_expr => |ot| { const child = self.resolveTypeWithBindings(ot.inner_type); return self.module.types.optionalOf(child); }, .array_type_expr => |at| { const elem = self.resolveTypeWithBindings(at.element_type); const len: u32 = blk: { if (at.length.data == .int_literal) break :blk @intCast(at.length.data.int_literal.value); break :blk 0; }; return self.module.types.arrayOf(elem, len); }, .parameterized_type_expr => |pt| { return self.resolveParameterizedWithBindings(&pt); }, .call => |cl| { // Handle List(T), Vector(N, T) etc. as type constructor calls return self.resolveTypeCallWithBindings(&cl); }, else => {}, } } // Even without active type_bindings, handle parameterized types with struct templates if (node.data == .parameterized_type_expr) { return self.resolveParameterizedWithBindings(&node.data.parameterized_type_expr); } if (node.data == .call) { return self.resolveTypeCallWithBindings(&node.data.call); } // Handle compound types that may contain generic structs (e.g., *List(ViewChild)) // These need the lowerer's resolveType to properly instantiate generics. switch (node.data) { .pointer_type_expr => |pt| { const pointee = self.resolveTypeWithBindings(pt.pointee_type); return self.module.types.ptrTo(pointee); }, .slice_type_expr => |st| { const elem = self.resolveTypeWithBindings(st.element_type); return self.module.types.sliceOf(elem); }, .many_pointer_type_expr => |mp| { const elem = self.resolveTypeWithBindings(mp.element_type); return self.module.types.manyPtrTo(elem); }, .optional_type_expr => |ot| { const child = self.resolveTypeWithBindings(ot.inner_type); return self.module.types.optionalOf(child); }, .array_type_expr => |at| { const elem = self.resolveTypeWithBindings(at.element_type); const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0; return self.module.types.arrayOf(elem, len); }, else => {}, } // Check type aliases before falling through to type_bridge if (node.data == .type_expr) { if (self.type_alias_map.get(node.data.type_expr.name)) |alias_ty| return alias_ty; } return type_bridge.resolveAstType(node, &self.module.types); } /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { const callee_name: []const u8 = switch (cl.callee.data) { .identifier => |id| id.name, .field_access => |fa| fa.field, else => return .s64, }; // Built-in: Vector(N, T) if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { const length: u32 = switch (cl.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), .identifier => |id| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, else => 0, }; const elem = self.resolveTypeWithBindings(cl.args[1]); return self.module.types.vectorOf(elem, length); } // User-defined generic struct if (self.struct_template_map.getPtr(callee_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, cl.args); } // User-defined type-returning function: Complex(u32), Sx(f32) // Also resolve via scope fn_names (local functions get mangled names) const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name; if (self.fn_ast_map.get(resolved_name)) |fd| { if (fd.type_params.len > 0) { if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| { return ty; } } } // Try as a named type const name_id = self.module.types.internString(callee_name); return self.module.types.findByName(name_id) orelse .s64; } /// Resolve a parameterized type expr, substituting bindings for type/value params. /// Handles both built-in types (Vector) and user-defined generic structs. fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr) TypeId { const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; const table = &self.module.types; // Vector(N, T) — built-in parameterized type if (std.mem.eql(u8, base_name, "Vector")) { if (pt.args.len == 2) { // Resolve length: literal, or bound comptime value const length: u32 = switch (pt.args[0].data) { .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), .identifier => |id| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(id.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, .type_expr => |te| blk: { if (self.comptime_value_bindings) |cvb| { if (cvb.get(te.name)) |v| break :blk @intCast(@as(u64, @bitCast(v))); } break :blk 0; }, else => 0, }; // Resolve element type through bindings const elem = self.resolveTypeWithBindings(pt.args[1]); return table.vectorOf(elem, length); } } // User-defined generic struct: look up template and instantiate if (self.struct_template_map.getPtr(base_name)) |tmpl| { return self.instantiateGenericStruct(tmpl, pt.args); } // Fallback: register as named type placeholder const name_id = table.internString(pt.name); return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } /// Instantiate a generic struct template with concrete args. /// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) } fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { const table = &self.module.types; // Build mangled name dynamically: StructName__arg1_arg2 var name_parts = std.ArrayList(u8).empty; name_parts.appendSlice(self.alloc, tmpl.name) catch {}; // Bind type params to args and build name suffix const saved_type_bindings = self.type_bindings; const saved_value_bindings = self.comptime_value_bindings; var tb = std.StringHashMap(TypeId).init(self.alloc); var cvb = std.StringHashMap(i64).init(self.alloc); for (tmpl.type_params, 0..) |tp, i| { if (i >= args.len) break; name_parts.appendSlice(self.alloc, "__") catch {}; if (tp.is_type_param) { const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; const tname = self.formatTypeName(ty); name_parts.appendSlice(self.alloc, tname) catch {}; } else { // Value param (e.g., $N: u32) — extract integer const val: i64 = switch (args[i].data) { .int_literal => |lit| lit.value, else => 0, }; cvb.put(tp.name, val) catch {}; var val_buf: [32]u8 = undefined; const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; name_parts.appendSlice(self.alloc, val_str) catch {}; } } const mangled_name = name_parts.items; // Check if already instantiated const name_id = table.internString(mangled_name); if (table.findByName(name_id)) |existing| { // Already registered — check if it has fields const info = table.get(existing); if (info == .@"struct" and info.@"struct".fields.len > 0) { return existing; } } // Set up bindings and resolve fields self.type_bindings = tb; self.comptime_value_bindings = cvb; var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| { const field_ty = self.resolveTypeWithBindings(ftype_node); fields.append(self.alloc, .{ .name = table.internString(fname), .ty = field_ty, }) catch unreachable; } // Restore bindings self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; // Register the monomorphized struct const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(info); table.update(id, info); // Store the type bindings and template name for method resolution const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id; self.struct_instance_bindings.put(owned_mangled, tb) catch {}; self.struct_instance_template.put(owned_mangled, tmpl.name) catch {}; return id; } /// Instantiate a type-returning function: `Foo :: Complex(u32)` where /// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }` /// Walks the function body to find the returned struct/enum, resolves field types /// with the provided type bindings, and registers the result. fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { const table = &self.module.types; // Build type bindings from params + args const saved_type_bindings = self.type_bindings; const saved_value_bindings = self.comptime_value_bindings; var tb = std.StringHashMap(TypeId).init(self.alloc); var cvb = std.StringHashMap(i64).init(self.alloc); // Build mangled name var name_parts = std.ArrayList(u8).empty; name_parts.appendSlice(self.alloc, template_name) catch {}; for (fd.type_params, 0..) |tp, i| { if (i >= args.len) break; name_parts.appendSlice(self.alloc, "__") catch {}; // Check if this is a Type param ($T: Type) or a value param ($N: u32) const is_type_param = if (tp.constraint.data == .type_expr) std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type") else true; // default to type param if (is_type_param) { const ty = self.resolveTypeWithBindings(args[i]); tb.put(tp.name, ty) catch {}; const tname = self.formatTypeName(ty); name_parts.appendSlice(self.alloc, tname) catch {}; } else { const val: i64 = switch (args[i].data) { .int_literal => |lit| lit.value, else => 0, }; cvb.put(tp.name, val) catch {}; var val_buf: [32]u8 = undefined; const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0"; name_parts.appendSlice(self.alloc, val_str) catch {}; } } const mangled_name = name_parts.items; // Check if already instantiated const mangled_name_id = table.internString(mangled_name); if (table.findByName(mangled_name_id)) |existing| { const info = table.get(existing); if ((info == .@"struct" and info.@"struct".fields.len > 0) or info == .@"union" or info == .tagged_union) { return existing; } } // Activate bindings self.type_bindings = tb; self.comptime_value_bindings = cvb; defer { self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; } // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") // or just the template name itself (inline use like "Sx(f32)") const has_alias = !std.mem.eql(u8, alias_name, template_name); // Try struct first if (findStructInBody(fd.body)) |struct_decl| { // Resolve struct fields with type bindings active var struct_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (struct_decl.field_names, struct_decl.field_types) |fname, ftype_node| { const field_ty = self.resolveTypeWithBindings(ftype_node); struct_fields.append(self.alloc, .{ .name = table.internString(fname), .ty = field_ty, }) catch {}; } // Always register under mangled name const mangled_info: types.TypeInfo = .{ .@"struct" = .{ .name = mangled_name_id, .fields = struct_fields.items, } }; const mangled_id = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); table.update(mangled_id, mangled_info); // If there's a real alias, also register under alias name and in alias map if (has_alias) { const alias_name_id = table.internString(alias_name); const alias_info: types.TypeInfo = .{ .@"struct" = .{ .name = alias_name_id, .fields = struct_fields.items, } }; const alias_id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(alias_info); table.update(alias_id, alias_info); // Store defaults if any if (struct_decl.field_defaults.len > 0) { self.struct_defaults_map.put(alias_name, struct_decl.field_defaults) catch {}; } return alias_id; } return mangled_id; } // Try tagged enum/union if (findUnionInBody(fd.body)) |enum_decl| { return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl); } return null; } /// Instantiate a tagged enum from a type function body. fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId { const table = &self.module.types; // Build variant fields (tagged enum variants stored as StructInfo.Field) var variant_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (ed.variant_names, 0..) |vname, i| { const payload_ty: TypeId = if (i < ed.variant_types.len and ed.variant_types[i] != null) self.resolveTypeWithBindings(ed.variant_types[i].?) else .void; variant_fields.append(self.alloc, .{ .name = table.internString(vname), .ty = payload_ty, }) catch {}; } const alias_name_id = table.internString(alias_name); const info: types.TypeInfo = .{ .tagged_union = .{ .name = alias_name_id, .fields = variant_fields.items, .tag_type = .s64, } }; const id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(info); table.update(id, info); // Also register under mangled name if (!std.mem.eql(u8, alias_name, mangled_name)) { const mangled_name_id = table.internString(mangled_name); const mangled_info: types.TypeInfo = .{ .tagged_union = .{ .name = mangled_name_id, .fields = variant_fields.items, .tag_type = .s64, } }; const mid = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info); table.update(mid, mangled_info); } return id; } /// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr). fn findStructInBody(body: *const Node) ?ast.StructDecl { if (body.data == .struct_decl) return body.data.struct_decl; if (body.data == .block) { for (body.data.block.stmts) |stmt| { if (stmt.data == .return_stmt) { if (stmt.data.return_stmt.value) |val| { if (val.data == .struct_decl) return val.data.struct_decl; } } if (stmt.data == .struct_decl) return stmt.data.struct_decl; } } return null; } /// Walk an AST body to find a tagged enum declaration. fn findUnionInBody(body: *const Node) ?ast.EnumDecl { const isTaggedEnum = struct { fn check(node: *const Node) ?ast.EnumDecl { if (node.data == .enum_decl and node.data.enum_decl.variant_types.len > 0) { return node.data.enum_decl; } return null; } }; if (isTaggedEnum.check(body)) |ed| return ed; const stmts = if (body.data == .block) body.data.block.stmts else return null; for (stmts) |stmt| { if (stmt.data == .return_stmt) { if (stmt.data.return_stmt.value) |val| { if (isTaggedEnum.check(val)) |ed| return ed; } } if (isTaggedEnum.check(stmt)) |ed| return ed; } return null; } // ── Type registration ─────────────────────────────────────────── /// Register a struct declaration's fields and methods in the IR type table. fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void { const table = &self.module.types; const name_id = table.internString(sd.name); // Generic structs: store as owned template, don't resolve fields yet if (sd.type_params.len > 0) { const owned_name = self.alloc.dupe(u8, sd.name) catch return; // Build owned type_params const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return; for (sd.type_params, 0..) |tp, i| { tps[i] = .{ .name = self.alloc.dupe(u8, tp.name) catch return, // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params // Only value params like $N: u32 are non-type .is_type_param = if (tp.constraint.data == .type_expr) blk: { const cname = tp.constraint.data.type_expr.name; // "Type" or a protocol name → type param break :blk std.mem.eql(u8, cname, "Type") or self.protocol_decl_map.contains(cname) or self.protocol_ast_map.contains(cname); } else false, }; } // Copy field names const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return; for (sd.field_names, 0..) |fn_str, i| { fnames[i] = self.alloc.dupe(u8, fn_str) catch return; } // Field type nodes: these are *Node pointers into the AST. // Copy the slice of pointers (the nodes themselves are heap-allocated). const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return; self.struct_template_map.put(owned_name, .{ .name = owned_name, .type_params = tps, .field_names = fnames, .field_type_nodes = ftype_nodes, }) catch {}; // Register methods under "TemplateName.method" in fn_ast_map for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { const method_fd = &method_node.data.fn_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; self.fn_ast_map.put(qualified, method_fd) catch {}; } } return; } // Build field list, expanding #using entries var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; var field_idx: usize = 0; var using_idx: usize = 0; const total_explicit = sd.field_names.len; while (field_idx < total_explicit or using_idx < sd.using_entries.len) { // Insert #using fields at their declared positions while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) { const ue = sd.using_entries[using_idx]; const used_name_id = table.internString(ue.type_name); if (table.findByName(used_name_id)) |used_ty| { const used_info = table.get(used_ty); if (used_info == .@"struct") { for (used_info.@"struct".fields) |f| { fields.append(self.alloc, f) catch unreachable; } } } using_idx += 1; } if (field_idx < total_explicit) { const field_ty = self.resolveType(sd.field_types[field_idx]); fields.append(self.alloc, .{ .name = table.internString(sd.field_names[field_idx]), .ty = field_ty, }) catch unreachable; field_idx += 1; } else break; } // Append remaining #using entries after all explicit fields while (using_idx < sd.using_entries.len) { const ue = sd.using_entries[using_idx]; const used_name_id = table.internString(ue.type_name); if (table.findByName(used_name_id)) |used_ty| { const used_info = table.get(used_ty); if (used_info == .@"struct") { for (used_info.@"struct".fields) |f| { fields.append(self.alloc, f) catch unreachable; } } } using_idx += 1; } // Qualify inline __anon type names: __anon → StructName.field_name for (sd.field_names, 0..) |fname, fi| { if (fi < fields.items.len) { const field_ty = fields.items[fi].ty; if (!field_ty.isBuiltin()) { self.qualifyAnonType(table, field_ty, sd.name, fname); } } } // Check if a forward-reference placeholder already exists (with empty fields) // If so, update it in-place rather than creating a duplicate const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(info); table.update(id, info); // Store field defaults for struct literal lowering if (sd.field_defaults.len > 0) { var has_any_default = false; for (sd.field_defaults) |d| { if (d != null) { has_any_default = true; break; } } if (has_any_default) { self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {}; } } // Register struct methods as StructName.method in fn_ast_map for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { const method_fd = &method_node.data.fn_decl; // Build qualified name: StructName.method const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; self.fn_ast_map.put(qualified, method_fd) catch {}; // Declare extern stub (body is lowered lazily on demand) self.declareFunction(method_fd, qualified); } } // Register struct-level constants (e.g., GRAVITY :f32: 9.81) for (sd.constants) |const_node| { if (const_node.data == .const_decl) { const cd = const_node.data.const_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue; const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table) else null; self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; } } } /// Rename an __anon type to a qualified name: ParentStruct.field_name /// Also renames variant payload struct types from __anon.X to ParentStruct.field_name.X fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void { const ti = table.get(ty); switch (ti) { .@"union" => |u| { const old_name = table.getString(u.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.update(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } }); }, .tagged_union => |u| { const old_name = table.getString(u.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); // Rename variant payload structs: __anon.X → ParentStruct.field.X for (u.fields) |f| { if (!f.ty.isBuiltin()) { const finfo = table.get(f.ty); if (finfo == .@"struct") { const sname = table.getString(finfo.@"struct".name); if (std.mem.startsWith(u8, sname, "__anon.")) { const suffix = sname["__anon".len..]; // .VariantName const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue; const sq_id = table.internString(sq); table.update(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } }); } } } } table.update(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } }); }, .@"enum" => |e| { const old_name = table.getString(e.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.update(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } }); }, .@"struct" => |s| { const old_name = table.getString(s.name); if (!std.mem.eql(u8, old_name, "__anon")) return; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; const qname_id = table.internString(qualified); table.update(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } }); }, else => {}, } } /// Register a protocol declaration as a struct type in the IR type table. /// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... } /// Non-inline protocols: { ctx: *void, __vtable: *void } /// Also stores protocol info for dispatch and vtable struct type for vtable protocols. fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void { // Parameterised protocols are compile-time-only — no vtable, no boxed // instance struct. Methods reference unbound type params (e.g. // `convert :: () -> Target`) that only get a concrete TypeId per // (Source, Target) pair at xx resolution time. Stash the AST so // `param_impl_map` lookup can resolve method signatures lazily. if (pd.type_params.len > 0) { self.protocol_ast_map.put(pd.name, pd) catch {}; return; } const table = &self.module.types; const name_id = table.internString(pd.name); var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; // First field: ctx: *void const void_ptr_ty = table.ptrTo(.void); fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty, }) catch unreachable; if (pd.is_inline) { // One fn-ptr field per protocol method for (pd.methods) |method| { fields.append(self.alloc, .{ .name = table.internString(method.name), .ty = void_ptr_ty, // fn ptrs are opaque pointers }) catch unreachable; } } else { // Vtable pointer fields.append(self.alloc, .{ .name = table.internString("__vtable"), .ty = void_ptr_ty, }) catch unreachable; } const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); table.update(id, struct_info); // Build protocol method info for dispatch var method_infos = std.ArrayList(ProtocolMethodInfo).empty; for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty; for (method.params) |p| { // Resolve param type; Self → *void for protocol context. // Type aliases (e.g. `ShaderHandle :: u32`) need to be // resolved through type_alias_map before falling through // to type_bridge — otherwise they're treated as named // empty structs and the LLVM call gets `{}` parameters. const pty = blk: { if (p.data == .type_expr) { if (std.mem.eql(u8, p.data.type_expr.name, "Self")) { break :blk void_ptr_ty; } if (self.type_alias_map.get(p.data.type_expr.name)) |aliased| { break :blk aliased; } } break :blk type_bridge.resolveAstType(p, table); }; ptypes.append(self.alloc, pty) catch unreachable; } const ret = if (method.return_type) |rt| blk: { if (rt.data == .type_expr) { if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) { break :blk void_ptr_ty; } if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| { break :blk aliased; } } break :blk type_bridge.resolveAstType(rt, table); } else .void; method_infos.append(self.alloc, .{ .name = method.name, .param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable, .ret_type = ret, }) catch unreachable; } self.protocol_decl_map.put(pd.name, .{ .name = pd.name, .is_inline = pd.is_inline, .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, }) catch {}; self.protocol_ast_map.put(pd.name, pd) catch {}; // For vtable protocols, create the vtable struct type if (!pd.is_inline) { var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (pd.methods) |method| { vtable_fields.append(self.alloc, .{ .name = table.internString(method.name), .ty = void_ptr_ty, }) catch unreachable; } var vtable_name_buf: [128]u8 = undefined; const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable"; const vtable_name_id = table.internString(vtable_name); const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } }; const vtable_ty = table.intern(vtable_info); self.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {}; } } /// Register an impl block: register its methods as TypeName.method in fn_ast_map. fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void { // Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`): // record into `param_impl_map` for compile-time resolution by `lowerXX`. // Methods are NOT registered in fn_ast_map — they're monomorphised lazily // per (Source, Target) pair at the xx call site. if (ib.protocol_type_args.len > 0) { self.registerParamImpl(ib, decl); return; } // Collect explicitly implemented method names var impl_methods = std.StringHashMap(void).init(self.alloc); defer impl_methods.deinit(); for (ib.methods) |method_node| { if (method_node.data == .fn_decl) { const method_fd = &method_node.data.fn_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue; self.fn_ast_map.put(qualified, method_fd) catch {}; self.import_flags.put(qualified, is_imported) catch {}; self.declareFunction(method_fd, qualified); impl_methods.put(method_fd.name, {}) catch {}; } } // Synthesize default methods from protocol declaration if (self.protocol_ast_map.get(ib.protocol_name)) |pd| { for (pd.methods) |method| { if (method.default_body != null and !impl_methods.contains(method.name)) { // Create a synthesized fn_decl for the default method const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type); const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue; self.fn_ast_map.put(qualified, synth_fd) catch {}; self.import_flags.put(qualified, is_imported) catch {}; self.declareFunction(synth_fd, qualified); } } } } /// Register a parameterised-protocol impl into `param_impl_map`. /// Resolves the protocol's type args + the source type, mangles them, and /// stashes the impl's method fn_decls for later monomorphisation by /// `lowerXX`. Same-module duplicate impls produce a diagnostic here; /// cross-module duplicates are detected at the xx resolution site. fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node) void { const table = &self.module.types; // Resolve the protocol's type-arg list to concrete TypeIds. var arg_tys = std.ArrayList(TypeId).empty; for (ib.protocol_type_args) |arg_node| { const t = type_bridge.resolveAstType(arg_node, table); arg_tys.append(self.alloc, t) catch return; } // Resolve the source type. Parser stores it on `target_type_expr` for // parameterised impls (back-compat `target_type` string is kept for // simple cases but the canonical form is the TypeExpr). const src_ty: TypeId = if (ib.target_type_expr) |te| type_bridge.resolveAstType(te, table) else if (ib.target_type.len > 0) type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table) else return; // Mangle into the lookup key. var key_buf = std.ArrayList(u8).empty; key_buf.appendSlice(self.alloc, ib.protocol_name) catch return; for (arg_tys.items) |t| { key_buf.append(self.alloc, 0) catch return; key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return; } key_buf.append(self.alloc, 0) catch return; key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return; const key = key_buf.items; // Collect method fn_decl pointers. var methods = std.ArrayList(*const ast.FnDecl).empty; for (ib.methods) |method_node| { if (method_node.data == .fn_decl) { methods.append(self.alloc, &method_node.data.fn_decl) catch {}; } } const defining_module: []const u8 = self.current_source_file orelse ""; const entry: ParamImplEntry = .{ .methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return, .source_ty = src_ty, .target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return, .defining_module = defining_module, .span = decl.span, }; const gop = self.param_impl_map.getOrPut(key) catch return; if (!gop.found_existing) { gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty; } else { // Same-file duplicate is an immediate error. Cross-file overlaps // are deferred to the xx resolution site (Phase 5) so the impl // surface can be richer than any one file's view. for (gop.value_ptr.items) |existing| { if (std.mem.eql(u8, existing.defining_module, defining_module)) { if (self.diagnostics) |diags| { diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{ ib.protocol_name, self.mangleTypeName(src_ty), defining_module, }); } return; } } } gop.value_ptr.append(self.alloc, entry) catch return; } /// Synthesize a fn_decl from a protocol default method for a concrete type. fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl { // Build parameter list: self: *TargetType, then the protocol method params var params_list = std.ArrayList(ast.Param).empty; defer params_list.deinit(self.alloc); // Add self parameter: self: *TargetType const self_type_node = self.alloc.create(ast.Node) catch unreachable; const pointee_node = self.alloc.create(ast.Node) catch unreachable; pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } }; self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = pointee_node, } } }; params_list.append(self.alloc, .{ .name = "self", .name_span = .{ .start = 0, .end = 0 }, .type_expr = self_type_node, }) catch unreachable; // Add remaining params from the protocol method for (method.params, method.param_names) |pty, pname| { params_list.append(self.alloc, .{ .name = pname, .name_span = .{ .start = 0, .end = 0 }, .type_expr = pty, }) catch unreachable; } const fd = self.alloc.create(ast.FnDecl) catch unreachable; fd.* = .{ .name = method.name, .params = self.alloc.dupe(ast.Param, params_list.items) catch unreachable, .body = method.default_body.?, .return_type = method.return_type, }; return fd; } // ── Protocol dispatch ────────────────────────────────────────── /// Check if a type name is a registered protocol. fn isProtocolType(self: *Lowering, type_name: []const u8) bool { return self.protocol_decl_map.contains(type_name); } /// Get protocol info for a TypeId (if it's a protocol type). fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { if (ty.isBuiltin()) return null; const info = self.module.types.get(ty); if (info != .@"struct") return null; const name = self.module.types.getString(info.@"struct".name); return self.protocol_decl_map.get(name); } /// Get or create thunks for a (protocol, concrete_type) pair. /// Returns a slice of FuncIds, one per protocol method. fn getOrCreateThunks(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8) []const FuncId { // Key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch return &.{}; if (self.protocol_thunk_map.get(key)) |thunks| return thunks; const pd = self.protocol_decl_map.get(proto_name) orelse return &.{}; var thunk_ids = std.ArrayList(FuncId).empty; defer thunk_ids.deinit(self.alloc); for (pd.methods) |method| { const thunk_id = self.createProtocolThunk(proto_name, concrete_type_name, method); thunk_ids.append(self.alloc, thunk_id) catch unreachable; } const owned = self.alloc.dupe(FuncId, thunk_ids.items) catch unreachable; self.protocol_thunk_map.put(key, owned) catch {}; return owned; } /// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret /// The thunk calls ConcreteType.method(ctx, args...). fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId { // Build params: ctx: *void + method params var params = std.ArrayList(inst_mod.Function.Param).empty; defer params.deinit(self.alloc); const void_ptr = self.module.types.ptrTo(.void); params.append(self.alloc, .{ .name = self.module.types.internString("ctx"), .ty = void_ptr }) catch unreachable; for (method.param_types, 0..) |pty, i| { var buf: [32]u8 = undefined; const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; } // Generate unique name var name_buf: [192]u8 = undefined; const thunk_name = std.fmt.bufPrint(&name_buf, "__thunk_{s}_{s}_{s}", .{ concrete_type_name, proto_name, method.name }) catch "__thunk"; const thunk_name_id = self.module.types.internString(thunk_name); // Save builder state const saved_func = self.builder.func; const saved_block = self.builder.current_block; const saved_counter = self.builder.inst_counter; const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; const func = inst_mod.Function.init(thunk_name_id, owned_params, method.ret_type); const func_id = self.module.addFunction(func); self.builder.func = func_id; self.builder.inst_counter = @intCast(owned_params.len); const entry_block = self.builder.appendBlock(self.module.types.internString("entry"), &.{}); self.builder.switchToBlock(entry_block); // Ensure the concrete method is lowered const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ concrete_type_name, method.name }) catch method.name; if (self.fn_ast_map.contains(qualified) and !self.lowered_functions.contains(qualified)) { self.lazyLowerFunction(qualified); } // Call the concrete method: ConcreteType.method(ctx, args...) if (self.resolveFuncByName(qualified)) |concrete_fid| { const concrete_func = &self.module.functions.items[@intFromEnum(concrete_fid)]; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); // Pass ctx (ref 0) as first arg (it's the concrete *Type disguised as *void) // If the concrete method expects a value (e.g., f32) not a pointer, load from ctx const ctx_ref = Ref.fromIndex(0); if (concrete_func.params.len > 0) { const first_concrete_ty = concrete_func.params[0].ty; const first_info = self.module.types.get(first_concrete_ty); if (first_info != .pointer) { // Concrete expects value — load from ctx pointer call_args.append(self.alloc, self.builder.load(ctx_ref, first_concrete_ty)) catch unreachable; } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } } else { call_args.append(self.alloc, ctx_ref) catch unreachable; } for (method.param_types, 0..) |proto_pty, i| { var arg_ref = Ref.fromIndex(@intCast(i + 1)); // If protocol param is a pointer (Self→*void) but concrete method // expects a value type, load the value from the pointer. const concrete_idx = i + 1; // +1 for self/ctx if (concrete_idx < concrete_func.params.len) { const concrete_pty = concrete_func.params[concrete_idx].ty; const proto_info = self.module.types.get(proto_pty); const concrete_info = self.module.types.get(concrete_pty); if (proto_info == .pointer and concrete_info != .pointer) { arg_ref = self.builder.load(arg_ref, concrete_pty); } } call_args.append(self.alloc, arg_ref) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; const concrete_ret = concrete_func.ret; const result = self.builder.call(concrete_fid, owned_args, concrete_ret); if (method.ret_type != .void) { // If protocol returns *void (Self) but concrete returns a value type, // box the value: alloca+store and return the pointer const ret_info = self.module.types.get(method.ret_type); const concrete_ret_info = self.module.types.get(concrete_ret); if (ret_info == .pointer and concrete_ret_info != .pointer) { const slot = self.builder.alloca(concrete_ret); self.builder.store(slot, result); self.builder.ret(slot, method.ret_type); } else { self.builder.ret(result, method.ret_type); } } else { self.builder.retVoid(); } } else { // Can't resolve concrete method — emit unreachable _ = self.builder.emit(.{ .@"unreachable" = {} }, .void); } self.builder.finalize(); // Restore builder state self.builder.func = saved_func; self.builder.current_block = saved_block; self.builder.inst_counter = saved_counter; return func_id; } /// Build a protocol value from a concrete pointer. /// For inline protocols: struct_init { ctx, thunk1, thunk2, ... } /// For vtable protocols: struct_init { ctx, vtable_ptr } where vtable is stack-allocated /// When `heap_copy` is true, the concrete data is heap-copied so the protocol value /// outlives the current stack frame (used when source is a value, not an explicit pointer). /// When false, the pointer is used directly (user manages the pointee's lifetime). fn buildProtocolValue(self: *Lowering, concrete_ptr: Ref, proto_name: []const u8, concrete_type_name: []const u8, proto_ty: TypeId, concrete_ty: TypeId, heap_copy: bool) Ref { const pd = self.protocol_decl_map.get(proto_name) orelse return concrete_ptr; const thunks = self.getOrCreateThunks(proto_name, concrete_type_name); if (thunks.len != pd.methods.len) return concrete_ptr; const void_ptr_ty = self.module.types.ptrTo(.void); // When source is a value (not an explicit pointer), heap-allocate // so the protocol value outlives the current stack frame. // When source is an explicit pointer (xx @obj), use it directly — // the user is responsible for the pointee's lifetime. var ctx_ptr = concrete_ptr; if (heap_copy) { const concrete_size = self.module.types.typeSizeBytes(concrete_ty); const size_ref = self.builder.constInt(@intCast(concrete_size), .s64); const heap_ptr = self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty); const memcpy_args = self.alloc.dupe(Ref, &.{ heap_ptr, concrete_ptr, size_ref }) catch unreachable; _ = self.builder.emit(.{ .call_builtin = .{ .builtin = inst_mod.BuiltinId.memcpy, .args = memcpy_args, } }, void_ptr_ty); ctx_ptr = heap_ptr; } if (pd.is_inline) { // Inline: { ctx, fn1, fn2, ... } var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); field_vals.append(self.alloc, ctx_ptr) catch unreachable; for (thunks) |thunk_id| { const fn_ref = self.builder.emit(.{ .func_ref = thunk_id }, void_ptr_ty); field_vals.append(self.alloc, fn_ref) catch unreachable; } const owned = self.alloc.dupe(Ref, field_vals.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = owned } }, proto_ty); } else { // Vtable: { ctx, vtable_ptr } // Vtable is a global constant (same function pointers for every instance // of the same Protocol+ConcreteType pair). Cached per pair. const vtable_ty = self.protocol_vtable_type_map.get(proto_name) orelse return concrete_ptr; // Build cache key: "Proto\x00Type" const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch unreachable; const vtable_global_id = self.protocol_vtable_global_map.get(key) orelse blk: { // Create vtable global with function pointer initializer const global_name = std.fmt.allocPrint(self.alloc, "__{s}__{s}__vtable", .{ proto_name, concrete_type_name }) catch unreachable; const global_name_id = self.module.types.strings.intern(self.alloc, global_name); const thunk_ids = self.alloc.dupe(FuncId, thunks) catch unreachable; const gid = self.module.addGlobal(.{ .name = global_name_id, .ty = vtable_ty, .init_val = .{ .vtable = thunk_ids }, .is_const = true, }); self.protocol_vtable_global_map.put(key, gid) catch {}; break :blk gid; }; // Reference the vtable global's address const vtable_ptr_ty = self.module.types.ptrTo(vtable_ty); const vtable_addr = self.builder.emit(.{ .global_addr = vtable_global_id }, vtable_ptr_ty); // Build protocol struct: { ctx, &vtable } var proto_fields = std.ArrayList(Ref).empty; defer proto_fields.deinit(self.alloc); proto_fields.append(self.alloc, ctx_ptr) catch unreachable; proto_fields.append(self.alloc, vtable_addr) catch unreachable; const proto_owned = self.alloc.dupe(Ref, proto_fields.items) catch unreachable; return self.builder.emit(.{ .struct_init = .{ .fields = proto_owned } }, proto_ty); } } /// Emit protocol method dispatch for a protocol-typed receiver. /// Returns the call result ref. fn emitProtocolDispatch(self: *Lowering, receiver: Ref, proto_info: ProtocolDeclInfo, method_name: []const u8, args: []const Ref, proto_ty: TypeId) Ref { // Find method index var method_idx: ?usize = null; var method_info: ?ProtocolMethodInfo = null; for (proto_info.methods, 0..) |m, i| { if (std.mem.eql(u8, m.name, method_name)) { method_idx = i; method_info = m; break; } } const mi = method_info orelse return self.emitError(method_name, null); const midx = method_idx orelse 0; // Extract ctx from protocol struct (field 0) const void_ptr = self.module.types.ptrTo(.void); const ctx = self.builder.structGet(receiver, 0, void_ptr); // Extract fn_ptr const fn_ptr = if (proto_info.is_inline) blk: { // Inline: fn_ptr at field 1+method_idx break :blk self.builder.structGet(receiver, @intCast(1 + midx), void_ptr); } else blk: { // Vtable: load vtable struct, extract fn_ptr at method_idx const vtable_ptr = self.builder.structGet(receiver, 1, void_ptr); const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitError("vtable", null); const vtable = self.builder.emit(.{ .deref = .{ .operand = vtable_ptr } }, vtable_ty); break :blk self.builder.structGet(vtable, @intCast(midx), void_ptr); }; _ = proto_ty; // Build call args: ctx + user args // Protocol method params use *void for Self-typed params. If the caller passes // a struct value, we need to alloca+store and pass the pointer instead. // Also coerce argument types to match declared param types (e.g., s64 → s32). var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); call_args.append(self.alloc, ctx) catch unreachable; for (args, 0..) |a, i| { const expected_ty = if (i < mi.param_types.len) mi.param_types[i] else void_ptr; const arg_ty = self.builder.getRefType(a); // Untargeted `null` lowers as const_null with type .void. Re-emit it // as a null of the expected pointer type instead of alloca'ing void. if (arg_ty == .void and expected_ty == void_ptr) { call_args.append(self.alloc, self.builder.constNull(void_ptr)) catch unreachable; continue; } // A protocol method that expects `*void` accepts any single-pointer // value directly (`*T`, `[*]T`). Only wrap non-pointer values in an // alloca-slot — wrapping a pointer would pass the stack slot's // address instead of the actual pointer, and the callee would read // 8 bytes of pointer plus garbage from beyond the stack. const is_pointer_ty = if (!arg_ty.isBuiltin()) blk: { const info = self.module.types.get(arg_ty); break :blk info == .pointer or info == .many_pointer; } else false; if (expected_ty == void_ptr and arg_ty != void_ptr and !is_pointer_ty) { const slot = self.builder.alloca(arg_ty); self.builder.store(slot, a); call_args.append(self.alloc, slot) catch unreachable; } else { // Coerce to match declared parameter type (critical for WASM strict signatures) const coerced = self.coerceToType(a, arg_ty, expected_ty); call_args.append(self.alloc, coerced) catch unreachable; } } const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable; const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type); // If protocol method returns *void (Self) and the caller expects a value type, // unbox: load the concrete value from the returned pointer. Real pointer // returns (declared `-> *T` for non-Self T) are NOT auto-loaded — the // pointee may be a single byte and reading `sizeof(target)` past it // segfaults. Self is encoded as `*void`, so test against that exact type. if (mi.ret_type == void_ptr) { if (self.target_type) |target| { const target_info = self.module.types.get(target); if (target_info != .pointer) { return self.builder.load(raw_result, target); } } } return raw_result; } /// Resolve the concrete type name for protocol erasure. /// Handles both direct types and pointer-to-types. fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { if (ty.isBuiltin()) { // Primitive types like s64 — check if they have toName() return self.module.types.typeName(ty); } const info = self.module.types.get(ty); if (info == .pointer) { // *ConcreteType → resolve pointee const pointee = info.pointer.pointee; if (pointee.isBuiltin()) return self.module.types.typeName(pointee); const pi = self.module.types.get(pointee); if (pi == .@"struct") return self.module.types.getString(pi.@"struct".name); return null; } if (info == .@"struct") return self.module.types.getString(info.@"struct".name); return null; } // ── Helpers ───────────────────────────────────────────────────── /// Infer the type of an expression from its AST node (used for untyped var decls). fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { .string_literal => .string, .int_literal => .s64, .float_literal => .f64, .bool_literal => .bool, .null_literal => .void, .binary_op => |bop| switch (bop.op) { .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op => .bool, else => self.inferExprType(bop.lhs), }, .unary_op => |uop| switch (uop.op) { .not => .bool, .negate => self.inferExprType(uop.operand), .xx => self.target_type orelse .s64, .address_of => blk: { const inner = self.inferExprType(uop.operand); break :blk self.module.types.ptrTo(inner); }, else => .s64, }, .if_expr => |ie| { // If-else: infer from then branch if (ie.else_branch != null) { return self.inferExprType(ie.then_branch); } return .void; }, .block => |blk| { // Block type is the type of the last expression if (blk.stmts.len > 0) { return self.inferExprType(blk.stmts[blk.stmts.len - 1]); } return .void; }, .call => |c| { if (c.callee.data == .identifier) { const bare_name = c.callee.data.identifier.name; // Resolve local function name (bare → mangled) and UFCS aliases const name = blk: { const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; if (self.ufcs_alias_map.get(bare_name)) |target| { break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; } break :blk scoped; }; if (resolveBuiltin(bare_name)) |bid| { return switch (bid) { .sqrt, .sin, .cos, .floor => blk: { if (c.args.len > 0) { const arg_ty = self.inferExprType(c.args[0]); if (arg_ty == .f32) break :blk TypeId.f32; } break :blk TypeId.f64; }, .size_of, .malloc => .s64, .cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .s64, else => .s64, }; } // Check if it's a generic function — infer return type via type bindings if (self.fn_ast_map.get(name)) |fd| { if (fd.type_params.len > 0) { return self.inferGenericReturnType(fd, &c); } } // Check declared functions for return type if (self.resolveFuncByName(name)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } // Check if callee is a local closure variable — extract return type if (self.scope) |scope| { if (scope.lookup(bare_name)) |binding| { if (!binding.ty.isBuiltin()) { const ti = self.module.types.get(binding.ty); if (ti == .closure) return ti.closure.ret; } } } } else if (c.callee.data == .field_access) { const cfa = c.callee.data.field_access; // Check if receiver is a protocol type → return protocol method type const recv_ty = self.inferExprType(cfa.object); { if (self.getProtocolInfo(recv_ty)) |proto_info| { for (proto_info.methods) |m| { if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; } } } // Instance method call: obj.method(args) → look up StructName.method { var obj_ty = recv_ty; if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .pointer) obj_ty = oi.pointer.pointee; } if (!obj_ty.isBuiltin()) { const oi = self.module.types.get(obj_ty); if (oi == .@"struct") { const struct_name = self.module.types.getString(oi.@"struct".name); const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; // Generic #compiler method dispatch — return type from declaration if (self.fn_ast_map.get(qualified)) |method_fd| { if (method_fd.body.data == .compiler_expr) { if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types); return .void; } } if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } } } } // Type.variant(args) — qualified enum construction const type_name = switch (cfa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => null, }; if (type_name) |tn| { const type_name_id = self.module.types.internString(tn); if (self.module.types.findByName(type_name_id)) |ty| { const ti = self.module.types.get(ty); if (ti == .tagged_union or ti == .@"enum") return ty; } // Check for qualified function call const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } } } else if (c.callee.data == .enum_literal) { // .Variant(args) — dot-shorthand enum construction return self.target_type orelse .s64; } return .s64; }, .field_access => |fa| { var obj_ty = self.inferExprType(fa.object); // Auto-deref: if object is a pointer, resolve through it (matches lowerFieldAccess behavior) if (!obj_ty.isBuiltin()) { const ptr_info = self.module.types.get(obj_ty); if (ptr_info == .pointer) { obj_ty = ptr_info.pointer.pointee; } } // Optional chaining: ?T.field → ?FieldType (flattened if field is already optional) const is_opt_chain = fa.is_optional; if (is_opt_chain and !obj_ty.isBuiltin()) { const opt_info = self.module.types.get(obj_ty); if (opt_info == .optional) { obj_ty = opt_info.optional.child; } } if (std.mem.eql(u8, fa.field, "len")) return if (is_opt_chain) self.module.types.optionalOf(.s64) else .s64; if (std.mem.eql(u8, fa.field, "ptr")) { // .ptr on slice/string → [*]element_type const elem_ty = self.getElementType(obj_ty); const mp_ty = self.module.types.manyPtrTo(elem_ty); return if (is_opt_chain) self.module.types.optionalOf(mp_ty) else mp_ty; } if (!obj_ty.isBuiltin()) { const field_name_id = self.module.types.internString(fa.field); // Check union fields (tagged enum payloads) + promoted struct fields const info = self.module.types.get(obj_ty); const u_fields2: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => null, }; if (u_fields2) |ufields| { for (ufields) |f| { if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty; // Check promoted fields from anonymous struct variants if (!f.ty.isBuiltin()) { const fi = self.module.types.get(f.ty); if (fi == .@"struct") { for (fi.@"struct".fields) |sf| { if (sf.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(sf.ty) else sf.ty; } } } } } // Check vector element access (.x/.y/.z/.w) if (info == .vector) { const elem = info.vector.element; return if (is_opt_chain) self.optionalOfFlattened(elem) else elem; } // Check struct fields const fields = self.getStructFields(obj_ty); for (fields) |f| { if (f.name == field_name_id) return if (is_opt_chain) self.optionalOfFlattened(f.ty) else f.ty; } } return .s64; }, .identifier => |id| { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { return binding.ty; } } // Check global variables (e.g., `context : Context`) if (self.global_names.get(id.name)) |gi| { return gi.ty; } // Check module-level value constants (e.g., WIDTH :f32: 800) if (self.module_const_map.get(id.name)) |ci| { return ci.ty; } return .s64; }, .type_expr => |te| { // type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type) if (self.scope) |scope| { if (scope.lookup(te.name)) |binding| { return binding.ty; } } return .s64; }, .enum_literal => { // Enum literals depend on context — use target_type if available return self.target_type orelse .s64; }, .struct_literal => |sl| { if (sl.struct_name) |name| { const name_id = self.module.types.internString(name); return self.module.types.findByName(name_id) orelse self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } return self.target_type orelse .s64; }, .tuple_literal => |tl| { var field_types = std.ArrayList(TypeId).empty; defer field_types.deinit(self.alloc); for (tl.elements) |elem| { field_types.append(self.alloc, self.inferExprType(elem.value)) catch unreachable; } return self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, field_types.items) catch unreachable, .names = null, } }); }, .index_expr => |ie| { const obj_ty = self.inferExprType(ie.object); return self.getElementType(obj_ty); }, .slice_expr => |se| { const obj_ty = self.inferExprType(se.object); if (obj_ty == .string) return .string; return self.module.types.sliceOf(self.getElementType(obj_ty)); }, .deref_expr => |de| { const ptr_ty = self.inferExprType(de.operand); if (!ptr_ty.isBuiltin()) { const info = self.module.types.get(ptr_ty); if (info == .pointer) return info.pointer.pointee; } return .s64; }, .chained_comparison => .bool, // Statements don't produce values .assignment, .var_decl, .const_decl, .fn_decl, .return_stmt, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, => .void, else => .s64, }; } /// Infer the return type of a generic function call by resolving type bindings. fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId { if (fd.return_type == null) return .void; // Build ALL type bindings from call args before resolving return type var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc); defer tmp_bindings.deinit(); for (fd.type_params) |tp| { // Strategy 1: direct type param decl ($T: Type) — param.name == tp.name var found = false; for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { if (pi < c.args.len) { const ty = self.resolveTypeArg(c.args[pi]); tmp_bindings.put(tp.name, ty) catch {}; } found = true; break; } } if (found) continue; // Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest var inferred_ty: ?TypeId = null; for (fd.params, 0..) |param, pi| { if (param.type_expr.data == .type_expr) { const te = param.type_expr.data.type_expr; if (std.mem.eql(u8, te.name, tp.name)) { if (pi < c.args.len) { const arg_ty = self.inferExprType(c.args[pi]); if (inferred_ty) |prev| { if (arg_ty == .f64 and prev != .f64) { inferred_ty = arg_ty; } else if (arg_ty == .f32 and prev != .f64 and prev != .f32) { inferred_ty = arg_ty; } } else { inferred_ty = arg_ty; } } } } } if (inferred_ty) |ty| { tmp_bindings.put(tp.name, ty) catch {}; } } // Resolve return type with all bindings if (tmp_bindings.count() > 0) { const saved = self.type_bindings; self.type_bindings = tmp_bindings; const ret = self.resolveTypeWithBindings(fd.return_type.?); self.type_bindings = saved; return ret; } return .s64; } /// Lower the `xx` operator (type coercion). /// Uses self.target_type for context when available. Handles: /// - Any → concrete type: unbox_any /// - int → int: widen/narrow /// - int ↔ float: int_to_float/float_to_int fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref { // Use the operand's *actual* lowered Ref type rather than reaching // back through inferExprType — the latter doesn't cover every // expression shape (notably lambdas), and a wrong src_ty here can // route the cast through coerceToType (e.g. a bogus s64→ptr bitcast) // and silently skip the user-space Into fallback. const src_ty = self.builder.getRefType(operand); const target_explicit = self.target_type != null; const dst_ty = self.target_type orelse .s64; // Any → concrete type: unbox if (src_ty == .any) { // When inside a float match arm covering both f32 and f64, // and target is f64, we need a mini-dispatch to unbox correctly. // f32 values are stored as zext(bitcast(f32→i32), i64) in Any, // so bitcasting i64→f64 directly gives wrong results for f32. if (dst_ty == .f64) { if (self.current_match_tags) |tags| { var has_f32 = false; var has_f64 = false; for (tags) |t| { const tid = TypeId.fromIndex(@intCast(t)); if (tid == .f32) has_f32 = true; if (tid == .f64) has_f64 = true; } if (has_f32 and has_f64) { return self.lowerAnyToF64Dispatch(operand); } if (has_f32 and !has_f64) { // Only f32 values: unbox as f32, then widen const f32_val = self.builder.emit(.{ .unbox_any = .{ .operand = operand, } }, .f32); return self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64); } } } return self.builder.emit(.{ .unbox_any = .{ .operand = operand, } }, dst_ty); } // Same type: no-op if (src_ty == dst_ty) return operand; // Concrete → Protocol: build protocol value if (self.getProtocolInfo(dst_ty)) |_| { return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty); } const result = self.coerceToType(operand, src_ty, dst_ty); // User-space fallback via `impl Into(Target) for Source`. Only fires // when the target was explicitly named (not the .s64 default), src and // dst differ, and the built-in ladder made no progress. Built-ins // always win. if (target_explicit and src_ty != dst_ty and result == operand) { if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| { return converted; } // Pointer-target fallback: `xx ` whose surrounding context // expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate) // can be satisfied by `impl Into(T) for src` plus an implicit // alloca+store on the result. Lets users write // `fn(xx () => { ... })` instead of materialising a named Block local // just to take its address. if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .pointer) { const pointee = dst_info.pointer.pointee; if (pointee != src_ty) { if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| { const slot = self.builder.alloca(pointee); self.builder.store(slot, converted); return slot; } } } } } return result; } /// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise /// the impl's `convert` method and emit a direct call. Returns null when /// no impl matches (caller falls back to the built-in result, which is /// the unchanged operand — Phase 3 emits no diagnostic for v0). fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref { // Reentrancy guard — pack (src, dst) into a u64. const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index()); if (self.xx_reentrancy.contains(guard_key)) { if (self.diagnostics) |diags| { diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } // Build lookup key: "Into\x00\x00". // Hardcoded to the "Into" protocol for v1. Generalising to other // parameterised protocols would walk protocol_decl_map looking for // protocols that take a single type-param and have a `convert` method. const proto_name = "Into"; const pd = self.protocol_ast_map.get(proto_name) orelse return null; if (pd.type_params.len != 1) return null; var key_buf = std.ArrayList(u8).empty; key_buf.appendSlice(self.alloc, proto_name) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null; const key = key_buf.items; const entries = self.param_impl_map.get(key) orelse return null; if (entries.items.len == 0) return null; // Filter by import visibility: only impls in modules that the current // file transitively imports (or the current file itself) are reachable. // Falls open when import_graph isn't wired (e.g. comptime callers). var visible_impls = std.ArrayList(ParamImplEntry).empty; defer visible_impls.deinit(self.alloc); self.findVisibleImpls(entries.items, &visible_impls); if (visible_impls.items.len == 0) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } if (visible_impls.items.len > 1) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), visible_impls.items[0].defining_module, visible_impls.items[1].defining_module, }); } return operand; } const entry = visible_impls.items[0]; // Find the `convert` method on this impl. var convert_fd: ?*const ast.FnDecl = null; for (entry.methods) |m| { if (std.mem.eql(u8, m.name, "convert")) { convert_fd = m; break; } } const fd = convert_fd orelse return null; // Bind Target → dst_ty. var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); bindings.put(pd.type_params[0].name, dst_ty) catch return null; // Mangled name: ".convert__". const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }) catch return null; self.xx_reentrancy.put(guard_key, {}) catch {}; defer _ = self.xx_reentrancy.remove(guard_key); if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, &bindings); } const fid = self.resolveFuncByName(mangled) orelse return null; const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; var args = [_]Ref{operand}; self.coerceCallArgs(args[0..], params); return self.builder.call(fid, args[0..], ret_ty); } /// Build a protocol value from a concrete value via xx conversion. fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref { const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return operand; const proto_name = self.module.types.getString(dst_info.@"struct".name); // Determine concrete type name and type — resolve through pointer if needed var concrete_ptr = operand; var concrete_type_name: ?[]const u8 = null; var concrete_ty: TypeId = src_ty; var heap_copy = false; if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); if (src_info == .pointer) { // xx @acc — operand is already a pointer (user manages lifetime) const pointee = src_info.pointer.pointee; concrete_type_name = self.resolveConcreteTypeName(pointee); concrete_ty = pointee; heap_copy = false; } else if (src_info == .@"struct") { // xx acc — operand is a value, need to take address + heap-copy concrete_type_name = self.module.types.getString(src_info.@"struct".name); concrete_ty = src_ty; heap_copy = true; // Alloca + store to get a pointer (will be heap-copied in buildProtocolValue) const slot = self.builder.alloca(src_ty); self.builder.store(slot, operand); concrete_ptr = slot; } } // Also try from the operand node for struct literals: xx Accumulator.{ total = 0 } if (concrete_type_name == null) { concrete_type_name = self.inferConcreteTypeName(operand_node); if (concrete_type_name != null) heap_copy = true; } if (concrete_type_name) |ctn| { return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); } return operand; } /// Try to infer the concrete type name from an AST node (for struct literals etc.) fn inferConcreteTypeName(self: *Lowering, node: *const Node) ?[]const u8 { return switch (node.data) { .struct_literal => |sl| if (sl.struct_name) |n| n else null, .unary_op => |uop| if (uop.op == .address_of) self.inferConcreteTypeName(uop.operand) else null, .identifier => |id| blk: { // Check if identifier's type resolves to a struct if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (!binding.ty.isBuiltin()) { const bi = self.module.types.get(binding.ty); if (bi == .@"struct") break :blk self.module.types.getString(bi.@"struct".name); if (bi == .pointer) { const pointee = bi.pointer.pointee; if (!pointee.isBuiltin()) { const pi = self.module.types.get(pointee); if (pi == .@"struct") break :blk self.module.types.getString(pi.@"struct".name); } } } } } break :blk null; }, else => null, }; } /// Generate a mini-dispatch for unboxing Any to f64 when the value might be f32 or f64. /// Uses alloca-based merge: create result slot, branch, store in each arm, load after merge. fn lowerAnyToF64Dispatch(self: *Lowering, any_val: Ref) Ref { // Create result alloca BEFORE the branch const result_slot = self.builder.alloca(.f64); // Extract type tag from Any const tag = self.builder.structGet(any_val, 0, .s64); const f32_bb = self.freshBlock("f32.unbox"); const f64_bb = self.freshBlock("f64.unbox"); const merge_bb = self.freshBlock("float.merge"); // Branch: tag == f32_tag ? f32_bb : f64_bb const f32_tag = self.builder.constInt(TypeId.f32.index(), .s64); const cond = self.builder.emit(.{ .cmp_eq = .{ .lhs = tag, .rhs = f32_tag } }, .bool); self.builder.condBr(cond, f32_bb, &.{}, f64_bb, &.{}); // f32 block: unbox as f32, fpext to f64, store self.builder.switchToBlock(f32_bb); const f32_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f32); const f64_from_f32 = self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64); self.builder.store(result_slot, f64_from_f32); self.builder.br(merge_bb, &.{}); // f64 block: unbox as f64 directly, store self.builder.switchToBlock(f64_bb); const f64_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f64); self.builder.store(result_slot, f64_val); self.builder.br(merge_bb, &.{}); // Merge block: load result self.builder.switchToBlock(merge_bb); return self.builder.load(result_slot, .f64); } /// Produce a default value for a type, applying struct field defaults. /// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied. /// For other types, returns a zero value. fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); if (info != .@"struct" and info != .tuple) return self.zeroValue(ty); // For tuples, build a zero-initialized tuple if (info == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (info.tuple.fields) |f| { field_vals.append(self.alloc, self.zeroValue(f)) catch unreachable; } return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } // Check for struct defaults const struct_name_str = self.module.types.getString(info.@"struct".name); const field_defaults = self.struct_defaults_map.get(struct_name_str) orelse return self.builder.constUndef(ty); const fields = info.@"struct".fields; var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (fields, 0..) |f, i| { if (i < field_defaults.len) { if (field_defaults[i]) |default_expr| { const saved_tt = self.target_type; self.target_type = f.ty; const val = self.lowerExpr(default_expr); self.target_type = saved_tt; field_vals.append(self.alloc, val) catch unreachable; } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } /// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U) fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId { if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) return ty; } return self.module.types.optionalOf(ty); } /// Produce a zero/default value for any type — constInt(0) for integers, /// constNull for pointers, constUndef for structs/complex types. fn zeroValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); return switch (info) { // Arbitrary-width integer types (u1, u2, s4, ...) interned as // `.signed`/`.unsigned` variants — fall through `isBuiltin()`. .signed, .unsigned => self.builder.constInt(0, ty), .pointer, .tuple, .optional => self.builder.constNull(ty), .@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty), else => self.builder.constUndef(ty), }; } /// Auto-initialize the global `context` with a default GPA allocator at the start of main(). /// Emits IR instructions equivalent to: /// __default_gpa : GPA = .{ alloc_count = 0 }; /// context = Context.{ allocator = GPA.create(@__default_gpa), data = null }; fn emitDefaultContextInit(self: *Lowering) void { // Look up the context global const ctx_gi = self.global_names.get("context") orelse return; const ctx_ty = ctx_gi.ty; // Look up GPA type const gpa_ty = self.module.types.findByName(self.module.types.internString("GPA")) orelse return; // Look up Allocator type const alloc_ty = self.module.types.findByName(self.module.types.internString("Allocator")) orelse return; // Get GPA→Allocator thunks const thunks = self.getOrCreateThunks("Allocator", "GPA"); if (thunks.len < 2) return; // 1. Stack-allocate GPA with alloc_count = 0 const gpa_slot = self.builder.alloca(gpa_ty); const zero = self.builder.constInt(0, .s64); const gpa_val = self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &.{zero}) catch return, } }, gpa_ty); self.builder.store(gpa_slot, gpa_val); // 2. Build Allocator inline protocol value: { ctx: *void, alloc_fn, dealloc_fn } const void_ptr_ty = self.module.types.ptrTo(.void); const gpa_ptr = gpa_slot; // alloca already gives us *GPA, all pointers are compatible const alloc_fn = self.builder.emit(.{ .func_ref = thunks[0] }, void_ptr_ty); const dealloc_fn = self.builder.emit(.{ .func_ref = thunks[1] }, void_ptr_ty); const alloc_val = self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &.{ gpa_ptr, alloc_fn, dealloc_fn }) catch return, } }, alloc_ty); // 3. Build Context struct: { allocator, data: null } const null_ptr = self.builder.constNull(void_ptr_ty); const ctx_val = self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &.{ alloc_val, null_ptr }) catch return, } }, ctx_ty); // 4. Store into context global self.builder.emitVoid(.{ .global_set = .{ .global = ctx_gi.id, .value = ctx_val } }, .void); } fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref { switch (ci.value.data) { .int_literal => |lit| { // If declared type is float, convert integer value to float constant if (ci.ty == .f32 or ci.ty == .f64) { return self.builder.constFloat(@floatFromInt(lit.value), ci.ty); } return self.builder.constInt(lit.value, ci.ty); }, .float_literal => |lit| return self.builder.constFloat(lit.value, ci.ty), .bool_literal => |lit| return self.builder.emit(.{ .const_bool = lit.value }, .bool), .string_literal => |lit| { 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); return self.builder.constString(sid); }, .undef_literal => return self.builder.constUndef(ci.ty), else => { // Complex expressions (struct_literal, call, etc.) — lower on demand const saved_target = self.target_type; self.target_type = ci.ty; const result = self.lowerExpr(ci.value); self.target_type = saved_target; return result; }, } } fn emitPlaceholder(self: *Lowering, name: []const u8) Ref { const sid = self.module.types.internString(name); return self.builder.emit(.{ .placeholder = sid }, .s64); } /// Check if a name refers to a known type (primitive or registered struct/enum/union). /// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names. fn isKnownTypeName(self: *Lowering, name: []const u8) bool { if (type_bridge.resolveTypePrimitive(name) != null) return true; if (self.type_bindings) |bindings| { if (bindings.get(name) != null) return true; } const name_id = self.module.types.internString(name); return self.module.types.findByName(name_id) != null; } fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "unresolved: '{s}'", .{name}); } return self.emitPlaceholder(name); } fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { if (self.diagnostics) |diags| { const ty_name = self.formatTypeName(obj_ty); diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name }); } return self.emitPlaceholder(field); } /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { if (src_ty == dst_ty) return val; // Unbox Any → concrete type if (src_ty == .any and dst_ty != .any) { return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty); } // Box concrete → Any if (dst_ty == .any and src_ty != .any) { return self.builder.boxAny(val, src_ty); } // Optional → Concrete unwrapping (flow-sensitive narrowing coercion) if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); if (src_info == .optional) { const child_ty = src_info.optional.child; if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) { const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); return self.coerceToType(unwrapped, child_ty, dst_ty); } } } // void → Optional: produce null (void is the type of null_literal) if (src_ty == .void and !dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .optional) { return self.builder.constNull(dst_ty); } } // Concrete → Optional wrapping if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .optional) { const child_ty = dst_info.optional.child; // Coerce the value to the inner type first const coerced = self.coerceToType(val, src_ty, child_ty); return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty); } } // Concrete → Protocol (auto type erasure) if (self.getProtocolInfo(dst_ty)) |_| { const dst_info = self.module.types.get(dst_ty); if (dst_info == .@"struct") { const proto_name = self.module.types.getString(dst_info.@"struct".name); if (self.resolveConcreteTypeName(src_ty)) |ctn| { // If src is a pointer, use directly; otherwise alloca+store + heap-copy var concrete_ptr = val; var concrete_ty = src_ty; var heap_copy = false; if (!src_ty.isBuiltin()) { const si = self.module.types.get(src_ty); if (si == .pointer) { concrete_ty = si.pointer.pointee; heap_copy = false; } else { const slot = self.builder.alloca(src_ty); self.builder.store(slot, val); concrete_ptr = slot; heap_copy = true; } } return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); } } } const src_float = isFloat(src_ty); const dst_float = isFloat(dst_ty); const src_int = self.isIntEx(src_ty); const dst_int = self.isIntEx(dst_ty); const src_ptr = !src_ty.isBuiltin() and self.module.types.get(src_ty) == .pointer; const dst_ptr = !dst_ty.isBuiltin() and self.module.types.get(dst_ty) == .pointer; // Int → Float if (src_int and dst_float) { return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } // Float → Int if (src_float and dst_int) { return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } // Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot. // Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches // to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level // since LLVMBuildBitCast itself doesn't accept ptr↔int. if ((src_ptr and dst_int) or (src_int and dst_ptr)) { return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } // Same kind — widen/narrow based on bit width const src_bits = self.typeBitsEx(src_ty); const dst_bits = self.typeBitsEx(dst_ty); if (src_bits > 0 and dst_bits > 0) { if (dst_bits < src_bits) { return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } else if (dst_bits > src_bits) { return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty); } } return val; } /// Get the alloca Ref for an expression, if it's a simple variable reference. /// Returns null for complex expressions (field access, function calls, etc.) fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref { const name = switch (node.data) { .identifier => |id| id.name, .type_expr => |te| te.name, else => return null, }; if (self.scope) |scope| { if (scope.lookup(name)) |binding| { if (binding.is_alloca) return binding.ref; } } return null; } /// Get the element type for a slice/array/string type. Returns .s64 for unknown types. fn getElementType(self: *Lowering, ty: TypeId) TypeId { if (ty == .string) return .u8; if (ty.isBuiltin()) return .s64; const info = self.module.types.get(ty); return switch (info) { .slice => |s| s.element, .array => |a| a.element, .vector => |v| v.element, .many_pointer => |p| p.element, else => .s64, }; } fn isFloat(ty: TypeId) bool { return ty == .f32 or ty == .f64; } fn isInt(ty: TypeId) bool { return switch (ty) { .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true, else => false, }; } fn isIntEx(self: *Lowering, ty: TypeId) bool { if (isInt(ty)) return true; if (!ty.isBuiltin()) { const info = self.module.types.get(ty); return switch (info) { .signed, .unsigned => true, else => false, }; } return false; } fn typeBits(ty: TypeId) u32 { return switch (ty) { .bool => 1, .s8, .u8 => 8, .s16, .u16 => 16, .s32, .u32 => 32, .s64, .u64 => 64, .usize, .isize => 0, // target-dependent — use typeBitsEx .f32 => 32, .f64 => 64, else => 0, }; } fn typeBitsEx(self: *Lowering, ty: TypeId) u32 { if (ty == .usize or ty == .isize) return @as(u32, self.module.types.pointer_size) * 8; const b = typeBits(ty); if (b > 0) return b; if (!ty.isBuiltin()) { const info = self.module.types.get(ty); return switch (info) { .signed => |w| @as(u32, w), .unsigned => |w| @as(u32, w), else => 0, }; } return 0; } /// Coerce call arguments in-place to match function parameter types. fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void { for (0..@min(args.len, params.len)) |i| { const src_ty = self.builder.getRefType(args[i]); const dst_ty = params[i].ty; if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); const dst_info = self.module.types.get(dst_ty); // Array → many_pointer decay: alloca the array, GEP to first element if (src_info == .array and dst_info == .many_pointer) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); const zero = self.builder.constInt(0, .s64); args[i] = self.builder.emit(.{ .index_gep = .{ .lhs = slot, .rhs = zero } }, dst_ty); continue; } // Implicit address-of: passing T value where *T is expected → alloca + store // Only when the pointee type matches the source type. if (dst_info == .pointer and src_info != .pointer and dst_info.pointer.pointee == src_ty) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); args[i] = slot; continue; } } args[i] = self.coerceToType(args[i], src_ty, dst_ty); } } fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { if (self.currentBlockHasTerminator()) return; if (ret_ty == .void) { self.builder.retVoid(); } else { // Use const_undef for complex types (string, struct, etc.) const default_val = if (ret_ty == .string or !ret_ty.isBuiltin()) self.builder.constUndef(ret_ty) else self.builder.constInt(0, ret_ty); self.builder.ret(default_val, ret_ty); } } };