Files
sx/src/ir/lower.zig
agra 561ad03a7c android: Platform-owned entry bridge + .android OS enum variant
User writes BOTH `main` and a 3-line `android_main(app)` trampoline.
The library provides `sx_android_bootstrap(app)` (stashes the NDK app
pointer into a platform-owned global) and `AndroidPlatform` impl of
the Platform protocol. The library NEVER references `main` — the OS-
shape entry symbol lives in user code where the other entry symbols
already live. iOS / SDL3 keep their existing shape; only Android adds
the trampoline.

Cross-cutting bits this commit ships:

  library/modules/compiler.sx
    Add `android` variant to `OperatingSystem`.

  src/ir/lower.zig
    - injectComptimeConstants: map TargetConfig.isAndroid() → .android.
    - New Pass 4 `checkRequiredEntryPoints`: emit a clean diagnostic
      when `--target android` is requested but `android_main` isn't
      defined, instead of letting the user crash on a dlopen-time
      missing-symbol error.

  library/modules/platform/android.sx
    AndroidPlatform impl of the Platform protocol — EGL bringup on
    `APP_CMD_INIT_WINDOW`, ALooper(0) polling, dispatches the user's
    frame closure each ~16 ms tick. `sx_android_bootstrap(app)` is the
    only function exposed for the entry trampoline.

  examples/99-android-egl-clear.sx
    Rewritten to use the new pattern: minimum `main` + `android_main`
    pair, AndroidPlatform-driven render loop. Doubles as the usage
    reference users hand off to the compiler diagnostic.

Verified on Pixel 7 Pro: purple clear-color frame, periodic
`rendered 60 frames` logcat lines. iOS-sim chess + 86/86 regression
tests pass.
2026-05-19 00:23:33 +03:00

9314 lines
446 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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<arg_mangled>\x00<src_mangled>" → 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),
.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;
}
// ── 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, &param_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);
},
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(&param);
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(&param, 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 <value>` 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 <value>;`
/// 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 <expr>` 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<dst_mangled>\x00<src_mangled>".
// 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: "<src>.convert__<dst>".
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);
// 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);
}
// 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);
}
}
};