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.
9314 lines
446 KiB
Zig
9314 lines
446 KiB
Zig
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, ¶m_names, &captures);
|
||
|
||
// Deduplicate captures
|
||
var seen = std.StringHashMap(void).init(self.alloc);
|
||
defer seen.deinit();
|
||
var deduped = std.ArrayList(CaptureInfo).empty;
|
||
defer deduped.deinit(self.alloc);
|
||
for (captures.items) |cap| {
|
||
if (!seen.contains(cap.name)) {
|
||
seen.put(cap.name, {}) catch {};
|
||
deduped.append(self.alloc, cap) catch {};
|
||
}
|
||
}
|
||
const capture_list = deduped.items;
|
||
|
||
// Build env struct type if there are captures
|
||
var env_struct_ty: TypeId = .void;
|
||
if (capture_list.len > 0) {
|
||
const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable;
|
||
for (capture_list, 0..) |cap, i| {
|
||
var nbuf: [32]u8 = undefined;
|
||
const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap";
|
||
env_field_data[i] = .{
|
||
.name = self.module.types.internString(fname),
|
||
.ty = cap.ty,
|
||
};
|
||
}
|
||
const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env";
|
||
const env_name_id = self.module.types.internString(env_name);
|
||
env_struct_ty = self.module.types.intern(.{ .@"struct" = .{
|
||
.name = env_name_id,
|
||
.fields = env_field_data,
|
||
} });
|
||
}
|
||
|
||
// Save current builder state
|
||
const saved_func = self.builder.func;
|
||
const saved_block = self.builder.current_block;
|
||
const saved_counter = self.builder.inst_counter;
|
||
const saved_scope = self.scope;
|
||
|
||
// Build param list — trampoline convention: env: *void is first param
|
||
var params = std.ArrayList(Function.Param).empty;
|
||
const env_ptr_ty = self.module.types.ptrTo(.void);
|
||
params.append(self.alloc, .{
|
||
.name = self.module.types.internString("env"),
|
||
.ty = env_ptr_ty,
|
||
}) catch unreachable;
|
||
// Get target closure param types for inference (from Closure(T1, T2) -> R annotations)
|
||
const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: {
|
||
if (!tt.isBuiltin()) {
|
||
const tti = self.module.types.get(tt);
|
||
if (tti == .closure) break :blk tti.closure.params;
|
||
// Unwrap ?Closure(...) → Closure(...)
|
||
if (tti == .optional) {
|
||
const inner = tti.optional.child;
|
||
if (!inner.isBuiltin()) {
|
||
const inner_info = self.module.types.get(inner);
|
||
if (inner_info == .closure) break :blk inner_info.closure.params;
|
||
}
|
||
}
|
||
}
|
||
break :blk null;
|
||
} else null;
|
||
for (lam.params, 0..) |p, pi| {
|
||
var pty = self.resolveParamType(&p);
|
||
// Infer param type from target closure type if no annotation
|
||
if (p.type_expr.data == .inferred_type and target_closure_params != null) {
|
||
if (pi < target_closure_params.?.len) {
|
||
pty = target_closure_params.?[pi];
|
||
}
|
||
}
|
||
params.append(self.alloc, .{
|
||
.name = self.module.types.internString(p.name),
|
||
.ty = pty,
|
||
}) catch unreachable;
|
||
}
|
||
|
||
const ret_ty = blk: {
|
||
if (lam.return_type) |rt| {
|
||
break :blk type_bridge.resolveAstType(rt, &self.module.types);
|
||
}
|
||
// Use target closure return type if available
|
||
if (self.target_type) |tt| {
|
||
if (!tt.isBuiltin()) {
|
||
const tti = self.module.types.get(tt);
|
||
if (tti == .closure) break :blk tti.closure.ret;
|
||
// Unwrap ?Closure(...) → Closure(...)
|
||
if (tti == .optional) {
|
||
const inner = tti.optional.child;
|
||
if (!inner.isBuiltin()) {
|
||
const inner_info = self.module.types.get(inner);
|
||
if (inner_info == .closure) break :blk inner_info.closure.ret;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Arrow lambda without explicit return type — infer from body expression
|
||
// Temporarily bind params in scope so inferExprType can resolve param types
|
||
var temp_scope = Scope.init(self.alloc, self.scope);
|
||
const saved = self.scope;
|
||
self.scope = &temp_scope;
|
||
for (lam.params) |p| {
|
||
const pty = self.resolveParamType(&p);
|
||
temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false });
|
||
}
|
||
const inferred = self.inferExprType(lam.body);
|
||
self.scope = saved;
|
||
temp_scope.deinit();
|
||
break :blk inferred;
|
||
};
|
||
const name_id = self.module.types.internString(name);
|
||
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
||
if (lam.call_conv == .c) {
|
||
self.module.getFunctionMut(func_id).call_conv = .c;
|
||
}
|
||
|
||
// Create entry block
|
||
const entry_name = self.module.types.internString("entry");
|
||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||
self.builder.switchToBlock(entry);
|
||
|
||
// Create scope WITHOUT parent — captures are bound from env, not parent scope
|
||
var lambda_scope = Scope.init(self.alloc, null);
|
||
self.scope = &lambda_scope;
|
||
|
||
// Bind captures from env struct (param 0)
|
||
if (capture_list.len > 0) {
|
||
const env_param_ref = @as(Ref, @enumFromInt(0));
|
||
// Alloca env struct locally so struct_gep can resolve the type
|
||
const env_local = self.builder.alloca(env_struct_ty);
|
||
// Compute env size
|
||
const env_byte_size_inner = self.computeEnvSize(capture_list);
|
||
const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64);
|
||
// memcpy(local_alloca, env_param, size)
|
||
const cp_args = self.alloc.dupe(Ref, &.{ env_local, env_param_ref, env_size_val }) catch unreachable;
|
||
_ = self.builder.emit(.{ .call_builtin = .{
|
||
.builtin = inst_mod.BuiltinId.memcpy,
|
||
.args = cp_args,
|
||
} }, self.module.types.ptrTo(.void));
|
||
|
||
for (capture_list, 0..) |cap, i| {
|
||
// GEP into env struct to get field pointer
|
||
const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
||
// Load the captured value into a local alloca
|
||
const loaded = self.builder.load(field_ptr, cap.ty);
|
||
const slot = self.builder.alloca(cap.ty);
|
||
self.builder.store(slot, loaded);
|
||
lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true });
|
||
}
|
||
}
|
||
|
||
// Also need parent scope for function lookups (but not variable lookups)
|
||
// Set up fn_names from parent scope chain
|
||
{
|
||
var s: ?*Scope = saved_scope;
|
||
while (s) |scope| {
|
||
var it = scope.fn_names.iterator();
|
||
while (it.next()) |e| {
|
||
if (!lambda_scope.fn_names.contains(e.key_ptr.*)) {
|
||
lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {};
|
||
}
|
||
}
|
||
s = scope.parent;
|
||
}
|
||
}
|
||
|
||
// Bind params
|
||
for (lam.params, 0..) |p, i| {
|
||
const pty = self.resolveParamType(&p);
|
||
const slot = self.builder.alloca(pty);
|
||
const param_ref = @as(Ref, @enumFromInt(i + 1)); // +1: env is param 0
|
||
self.builder.store(slot, param_ref);
|
||
lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
||
}
|
||
|
||
// Lower body — capture last expression as return value
|
||
if (ret_ty != .void) {
|
||
if (self.lowerBlockValue(lam.body)) |val| {
|
||
if (!self.currentBlockHasTerminator()) {
|
||
const val_ty = self.builder.getRefType(val);
|
||
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
||
self.builder.ret(coerced, ret_ty);
|
||
}
|
||
}
|
||
} else {
|
||
self.lowerBlock(lam.body);
|
||
}
|
||
self.ensureTerminator(ret_ty);
|
||
self.builder.finalize();
|
||
|
||
// Restore builder state
|
||
self.scope = saved_scope;
|
||
lambda_scope.deinit();
|
||
self.builder.func = saved_func;
|
||
self.builder.current_block = saved_block;
|
||
self.builder.inst_counter = saved_counter;
|
||
|
||
// Create proper closure type (user-visible params only, no env)
|
||
var param_types_list = std.ArrayList(TypeId).empty;
|
||
for (params.items[1..]) |p| { // skip env (index 0)
|
||
param_types_list.append(self.alloc, p.ty) catch unreachable;
|
||
}
|
||
const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty);
|
||
|
||
// Build env and closure in the caller's scope
|
||
if (capture_list.len > 0) {
|
||
// Alloca env struct on stack (so struct_gep can resolve the type)
|
||
const env_local = self.builder.alloca(env_struct_ty);
|
||
|
||
// Store captured values into env struct fields
|
||
for (capture_list, 0..) |cap, i| {
|
||
const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
||
const val = if (cap.is_alloca)
|
||
self.builder.load(cap.ref, cap.ty)
|
||
else
|
||
cap.ref;
|
||
self.builder.store(gep, val);
|
||
}
|
||
|
||
// Copy env to heap (so it outlives the stack frame)
|
||
const env_byte_size = self.computeEnvSize(capture_list);
|
||
const env_size = self.builder.constInt(@intCast(env_byte_size), .s64);
|
||
const ptr_void = self.module.types.ptrTo(.void);
|
||
const env_heap = self.builder.emit(.{ .heap_alloc = .{ .operand = env_size } }, ptr_void);
|
||
// memcpy(heap, stack_alloca, size)
|
||
const args = self.alloc.dupe(Ref, &.{ env_heap, env_local, env_size }) catch unreachable;
|
||
_ = self.builder.emit(.{ .call_builtin = .{
|
||
.builtin = inst_mod.BuiltinId.memcpy,
|
||
.args = args,
|
||
} }, ptr_void);
|
||
|
||
return self.builder.closureCreate(func_id, env_heap, closure_ty);
|
||
} else {
|
||
return self.builder.closureCreate(func_id, Ref.none, closure_ty);
|
||
}
|
||
}
|
||
|
||
/// Create a trampoline function that wraps a bare function for closure auto-promotion.
|
||
/// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the
|
||
/// bare function with `(args...)`, ignoring the env parameter.
|
||
fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId {
|
||
// Build trampoline params: env + closure params
|
||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||
defer params.deinit(self.alloc);
|
||
const env_name = self.module.types.internString("env");
|
||
params.append(self.alloc, .{ .name = env_name, .ty = self.module.types.ptrTo(.void) }) catch unreachable;
|
||
for (closure_info.params, 0..) |pty, i| {
|
||
var buf: [32]u8 = undefined;
|
||
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
||
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
||
}
|
||
|
||
// Generate unique trampoline name
|
||
const bare_func = self.module.functions.items[bare_func_id.index()];
|
||
const bare_name = self.module.types.getString(bare_func.name);
|
||
var name_buf: [128]u8 = undefined;
|
||
const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp";
|
||
const tramp_name_id = self.module.types.internString(tramp_name);
|
||
|
||
// Save builder state
|
||
const saved_func = self.builder.func;
|
||
const saved_block = self.builder.current_block;
|
||
const saved_counter = self.builder.inst_counter;
|
||
|
||
// Create function
|
||
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
||
const func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret);
|
||
const func_id = self.module.addFunction(func);
|
||
self.builder.func = func_id;
|
||
self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1
|
||
const entry_name = self.module.types.internString("entry");
|
||
const entry_block = self.builder.appendBlock(entry_name, &.{});
|
||
self.builder.switchToBlock(entry_block);
|
||
|
||
// Build call args: skip env (param 0), forward params 1..N
|
||
var call_args = std.ArrayList(Ref).empty;
|
||
defer call_args.deinit(self.alloc);
|
||
for (closure_info.params, 0..) |_, i| {
|
||
call_args.append(self.alloc, Ref.fromIndex(@intCast(i + 1))) catch unreachable;
|
||
}
|
||
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
||
const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret);
|
||
|
||
// Return result (or void)
|
||
if (closure_info.ret != .void) {
|
||
self.builder.ret(result, closure_info.ret);
|
||
} else {
|
||
self.builder.retVoid();
|
||
}
|
||
self.builder.finalize();
|
||
|
||
// Restore builder state
|
||
self.builder.func = saved_func;
|
||
self.builder.current_block = saved_block;
|
||
self.builder.inst_counter = saved_counter;
|
||
|
||
return func_id;
|
||
}
|
||
|
||
/// Walk an AST node and collect free variable references (identifiers that are
|
||
/// in the current scope but not in lambda params).
|
||
fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void {
|
||
switch (node.data) {
|
||
.identifier => |id| {
|
||
// Skip lambda params
|
||
if (param_names.contains(id.name)) return;
|
||
// Skip function names
|
||
if (self.fn_ast_map.contains(id.name)) return;
|
||
// Skip type names
|
||
if (self.struct_template_map.contains(id.name)) return;
|
||
// Check if it's a variable in the parent scope
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(id.name)) |binding| {
|
||
captures.append(self.alloc, .{
|
||
.name = id.name,
|
||
.ty = binding.ty,
|
||
.ref = binding.ref,
|
||
.is_alloca = binding.is_alloca,
|
||
}) catch {};
|
||
}
|
||
}
|
||
},
|
||
.binary_op => |bo| {
|
||
self.collectCaptures(bo.lhs, param_names, captures);
|
||
self.collectCaptures(bo.rhs, param_names, captures);
|
||
},
|
||
.unary_op => |uo| {
|
||
self.collectCaptures(uo.operand, param_names, captures);
|
||
},
|
||
.call => |cl| {
|
||
self.collectCaptures(cl.callee, param_names, captures);
|
||
for (cl.args) |arg| {
|
||
self.collectCaptures(arg, param_names, captures);
|
||
}
|
||
},
|
||
.block => |blk| {
|
||
for (blk.stmts) |stmt| {
|
||
self.collectCaptures(stmt, param_names, captures);
|
||
}
|
||
},
|
||
.if_expr => |ie| {
|
||
self.collectCaptures(ie.condition, param_names, captures);
|
||
self.collectCaptures(ie.then_branch, param_names, captures);
|
||
if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures);
|
||
},
|
||
.while_expr => |we| {
|
||
self.collectCaptures(we.condition, param_names, captures);
|
||
self.collectCaptures(we.body, param_names, captures);
|
||
},
|
||
.return_stmt => |rs| {
|
||
if (rs.value) |v| self.collectCaptures(v, param_names, captures);
|
||
},
|
||
.var_decl => |vd| {
|
||
if (vd.value) |v| self.collectCaptures(v, param_names, captures);
|
||
// Register the local var name so it's not captured
|
||
param_names.put(vd.name, {}) catch {};
|
||
},
|
||
.const_decl => |cd| {
|
||
self.collectCaptures(cd.value, param_names, captures);
|
||
param_names.put(cd.name, {}) catch {};
|
||
},
|
||
.assignment => |a| {
|
||
self.collectCaptures(a.target, param_names, captures);
|
||
self.collectCaptures(a.value, param_names, captures);
|
||
},
|
||
.destructure_decl => |dd| {
|
||
self.collectCaptures(dd.value, param_names, captures);
|
||
for (dd.names) |name| {
|
||
param_names.put(name, {}) catch {};
|
||
}
|
||
},
|
||
.field_access => |fa| {
|
||
self.collectCaptures(fa.object, param_names, captures);
|
||
},
|
||
.index_expr => |ie| {
|
||
self.collectCaptures(ie.object, param_names, captures);
|
||
self.collectCaptures(ie.index, param_names, captures);
|
||
},
|
||
.struct_literal => |sl| {
|
||
for (sl.field_inits) |fi| {
|
||
self.collectCaptures(fi.value, param_names, captures);
|
||
}
|
||
},
|
||
.array_literal => |al| {
|
||
for (al.elements) |elem| {
|
||
self.collectCaptures(elem, param_names, captures);
|
||
}
|
||
},
|
||
.lambda => |inner_lam| {
|
||
// For nested lambdas, the inner lambda captures from our scope too
|
||
// But its own params should be excluded
|
||
var inner_params = std.StringHashMap(void).init(self.alloc);
|
||
defer inner_params.deinit();
|
||
// Copy current param_names
|
||
var it = param_names.iterator();
|
||
while (it.next()) |e| {
|
||
inner_params.put(e.key_ptr.*, {}) catch {};
|
||
}
|
||
for (inner_lam.params) |p| {
|
||
inner_params.put(p.name, {}) catch {};
|
||
}
|
||
self.collectCaptures(inner_lam.body, &inner_params, captures);
|
||
},
|
||
.match_expr => |me| {
|
||
self.collectCaptures(me.subject, param_names, captures);
|
||
for (me.arms) |arm| {
|
||
self.collectCaptures(arm.body, param_names, captures);
|
||
}
|
||
},
|
||
.null_coalesce => |nc| {
|
||
self.collectCaptures(nc.lhs, param_names, captures);
|
||
self.collectCaptures(nc.rhs, param_names, captures);
|
||
},
|
||
.deref_expr => |de| {
|
||
self.collectCaptures(de.operand, param_names, captures);
|
||
},
|
||
.for_expr => |fe| {
|
||
self.collectCaptures(fe.iterable, param_names, captures);
|
||
// Register capture name as local so it's not captured
|
||
param_names.put(fe.capture_name, {}) catch {};
|
||
self.collectCaptures(fe.body, param_names, captures);
|
||
},
|
||
.slice_expr => |se| {
|
||
self.collectCaptures(se.object, param_names, captures);
|
||
if (se.start) |s| self.collectCaptures(s, param_names, captures);
|
||
if (se.end) |e| self.collectCaptures(e, param_names, captures);
|
||
},
|
||
.tuple_literal => |tl| {
|
||
for (tl.elements) |elem| {
|
||
self.collectCaptures(elem.value, param_names, captures);
|
||
}
|
||
},
|
||
.force_unwrap => |fu| {
|
||
self.collectCaptures(fu.operand, param_names, captures);
|
||
},
|
||
.chained_comparison => |cc| {
|
||
for (cc.operands) |op| {
|
||
self.collectCaptures(op, param_names, captures);
|
||
}
|
||
},
|
||
.defer_stmt => |ds| {
|
||
self.collectCaptures(ds.expr, param_names, captures);
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
|
||
/// Compute the byte size of the env struct based on captured value types.
|
||
fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize {
|
||
// Must match LLVM's struct layout: fields are aligned to their natural alignment
|
||
var offset: usize = 0;
|
||
var max_align: usize = 1;
|
||
for (capture_list) |cap| {
|
||
const field_size = self.typeSizeBytes(cap.ty);
|
||
const field_align = self.typeAlignBytes(cap.ty);
|
||
if (field_align > max_align) max_align = field_align;
|
||
// Align offset to field alignment
|
||
offset = (offset + field_align - 1) & ~(field_align - 1);
|
||
offset += field_size;
|
||
}
|
||
// Align total to max field alignment (matches LLVM's struct alignment)
|
||
return (offset + max_align - 1) & ~(max_align - 1);
|
||
}
|
||
|
||
/// Byte size of an IR type matching LLVM's type layout.
|
||
fn typeSizeBytes(self: *Lowering, ty: TypeId) usize {
|
||
return self.module.types.typeSizeBytes(ty);
|
||
}
|
||
|
||
fn typeAlignBytes(self: *Lowering, ty: TypeId) usize {
|
||
return self.module.types.typeAlignBytes(ty);
|
||
}
|
||
|
||
fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId {
|
||
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types);
|
||
return .void;
|
||
}
|
||
|
||
// ── Chained comparison ──────────────────────────────────────────
|
||
|
||
fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref {
|
||
// a < b < c → (a < b) and (b < c)
|
||
// Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice.
|
||
if (cc.operands.len < 2 or cc.ops.len == 0) {
|
||
return self.builder.constBool(true);
|
||
}
|
||
|
||
var refs = std.ArrayList(Ref).empty;
|
||
defer refs.deinit(self.alloc);
|
||
for (cc.operands) |op| {
|
||
refs.append(self.alloc, self.lowerExpr(op)) catch unreachable;
|
||
}
|
||
|
||
var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]);
|
||
|
||
var i: usize = 1;
|
||
while (i < cc.ops.len) : (i += 1) {
|
||
const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]);
|
||
result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref {
|
||
return switch (op) {
|
||
.eq => self.builder.cmpEq(lhs, rhs),
|
||
.neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||
.lt => self.builder.cmpLt(lhs, rhs),
|
||
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||
.gt => self.builder.cmpGt(lhs, rhs),
|
||
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||
else => self.builder.constBool(false),
|
||
};
|
||
}
|
||
|
||
// ── Defer/Push/MultiAssign ──────────────────────────────────────
|
||
|
||
fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void {
|
||
// Push deferred expression onto the stack — will be emitted at block exit in LIFO order
|
||
self.defer_stack.append(self.alloc, ds.expr) catch {};
|
||
}
|
||
|
||
/// Emit deferred expressions from saved_len..current in reverse (LIFO) order,
|
||
/// then truncate the defer stack back to saved_len.
|
||
fn emitBlockDefers(self: *Lowering, saved_len: usize) void {
|
||
// Guard: if stack was already drained (e.g., by a return that emitted all defers)
|
||
if (saved_len > self.defer_stack.items.len) return;
|
||
if (self.currentBlockHasTerminator()) {
|
||
// Block already terminated (e.g., by return) — defers were already emitted
|
||
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
||
return;
|
||
}
|
||
const stack = self.defer_stack.items;
|
||
var i = stack.len;
|
||
while (i > saved_len) {
|
||
i -= 1;
|
||
_ = self.lowerExpr(stack[i]);
|
||
}
|
||
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
||
}
|
||
|
||
fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
||
// push context_expr { body }
|
||
// → save = global_get(context), global_set(context, new_val), body, global_set(context, save)
|
||
const gi = self.global_names.get("context") orelse {
|
||
// No context global — just lower the body without push/pop
|
||
self.lowerBlock(ps.body);
|
||
return;
|
||
};
|
||
// Save current context
|
||
const save = self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
||
// Lower the new context value
|
||
const ctx_val = self.lowerExpr(ps.context_expr);
|
||
// Store into context global
|
||
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = ctx_val } }, .void);
|
||
// Lower the body
|
||
self.lowerBlock(ps.body);
|
||
// Restore saved context
|
||
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = save } }, .void);
|
||
}
|
||
|
||
fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||
// Evaluate all RHS values first, then assign to LHS targets
|
||
var vals = std.ArrayList(Ref).empty;
|
||
defer vals.deinit(self.alloc);
|
||
for (ma.values) |v| {
|
||
vals.append(self.alloc, self.lowerExpr(v)) catch unreachable;
|
||
}
|
||
|
||
for (ma.targets, 0..) |target, i| {
|
||
if (i >= vals.items.len) break;
|
||
const val = vals.items[i];
|
||
switch (target.data) {
|
||
.identifier => |id| {
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(id.name)) |binding| {
|
||
if (binding.is_alloca) {
|
||
const val_ty = self.builder.getRefType(val);
|
||
const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void)
|
||
self.coerceToType(val, val_ty, binding.ty)
|
||
else
|
||
val;
|
||
self.builder.store(binding.ref, store_val);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
.index_expr => |ie| {
|
||
const idx = self.lowerExpr(ie.index);
|
||
const obj_ty = self.inferExprType(ie.object);
|
||
const elem_ty = self.getElementType(obj_ty);
|
||
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
||
const val_ty = self.builder.getRefType(val);
|
||
const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void)
|
||
self.coerceToType(val, val_ty, elem_ty)
|
||
else
|
||
val;
|
||
// For fixed-size arrays, use the alloca pointer directly
|
||
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
||
const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null;
|
||
if (obj_alloca) |alloca_ref| {
|
||
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty);
|
||
self.builder.store(gep, store_val);
|
||
} else {
|
||
const obj = self.lowerExpr(ie.object);
|
||
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty);
|
||
self.builder.store(gep, store_val);
|
||
}
|
||
},
|
||
.field_access => |fa| {
|
||
const obj_ptr = self.lowerExprAsPtr(fa.object);
|
||
const obj_ty = self.inferExprType(fa.object);
|
||
const field_name_id = self.module.types.internString(fa.field);
|
||
const struct_fields = self.getStructFields(obj_ty);
|
||
var field_idx: u32 = 0;
|
||
var field_ty: TypeId = .s64;
|
||
for (struct_fields, 0..) |f, fi| {
|
||
if (f.name == field_name_id) {
|
||
field_idx = @intCast(fi);
|
||
field_ty = f.ty;
|
||
break;
|
||
}
|
||
}
|
||
const gep = self.builder.structGepTyped(obj_ptr, field_idx, field_ty, obj_ty);
|
||
const val_ty = self.builder.getRefType(val);
|
||
const store_val = if (val_ty != field_ty and val_ty != .void and field_ty != .void)
|
||
self.coerceToType(val, val_ty, field_ty)
|
||
else
|
||
val;
|
||
self.builder.store(gep, store_val);
|
||
},
|
||
.deref_expr => |de| {
|
||
const ptr = self.lowerExpr(de.operand);
|
||
const pointee_ty = blk: {
|
||
const ptr_ty = self.inferExprType(de.operand);
|
||
if (!ptr_ty.isBuiltin()) {
|
||
const info = self.module.types.get(ptr_ty);
|
||
if (info == .pointer) break :blk info.pointer.pointee;
|
||
}
|
||
break :blk ptr_ty;
|
||
};
|
||
const val_ty = self.builder.getRefType(val);
|
||
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
||
self.coerceToType(val, val_ty, pointee_ty)
|
||
else
|
||
val;
|
||
self.builder.store(ptr, store_val);
|
||
},
|
||
else => {
|
||
_ = self.emitError("multi_assign_target", target.span);
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void {
|
||
// Lower the RHS expression (must produce a tuple)
|
||
const saved_fbv = self.force_block_value;
|
||
self.force_block_value = true;
|
||
const ref = self.lowerExpr(dd.value);
|
||
self.force_block_value = saved_fbv;
|
||
const ty = self.builder.getRefType(ref);
|
||
|
||
// Get tuple field info
|
||
if (ty.isBuiltin()) return;
|
||
const ti = self.module.types.get(ty);
|
||
if (ti != .tuple) return;
|
||
const tuple = ti.tuple;
|
||
if (dd.names.len > tuple.fields.len) return;
|
||
|
||
// Extract each field and bind to a new variable
|
||
for (dd.names, 0..) |name, i| {
|
||
if (std.mem.eql(u8, name, "_")) continue; // discard
|
||
const field_ty = tuple.fields[i];
|
||
const field_val = self.builder.emit(.{ .tuple_get = .{
|
||
.base = ref,
|
||
.field_index = @intCast(i),
|
||
.base_type = ty,
|
||
} }, field_ty);
|
||
const slot = self.builder.alloca(field_ty);
|
||
self.builder.store(slot, field_val);
|
||
if (self.scope) |scope| {
|
||
scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true });
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Comptime lowering ────────────────────────────────────────────
|
||
|
||
/// Lower a `#run expr` that appears as a top-level constant binding:
|
||
/// NAME :: #run expr;
|
||
/// Creates a comptime function wrapping the expression (for later
|
||
/// interpretation), plus a global constant to hold the result.
|
||
fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void {
|
||
const ret_ty = self.resolveType(type_ann);
|
||
const func_id = self.createComptimeFunction(name, expr, ret_ty);
|
||
|
||
// Add a global constant whose initializer will be filled by the interpreter.
|
||
const name_id = self.module.types.internString(name);
|
||
const gid = self.module.addGlobal(.{
|
||
.name = name_id,
|
||
.ty = ret_ty,
|
||
.init_val = null, // will be filled by interpreter at emit time
|
||
.is_const = true,
|
||
.comptime_func = func_id,
|
||
});
|
||
|
||
// Register for runtime lookup: identifier resolution emits global_get
|
||
self.global_names.put(name, .{ .id = gid, .ty = ret_ty }) catch {};
|
||
}
|
||
|
||
/// Lower a standalone `#run expr;` at the top level (side-effect only).
|
||
/// Creates a comptime function that the interpreter should execute.
|
||
fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void {
|
||
_ = self.createComptimeFunction("__run", expr, .void);
|
||
}
|
||
|
||
/// Lower a `#run expr` that appears inline within an expression.
|
||
/// Creates a comptime function and emits a `call` to it, so the
|
||
/// interpreter can evaluate it and replace with the constant result.
|
||
fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref {
|
||
const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr);
|
||
const func_id = self.createComptimeFunction("__ct", expr, ret_ty);
|
||
// Emit a call to the comptime function. At interpretation time,
|
||
// this will be evaluated and the result inlined as a constant.
|
||
return self.builder.call(func_id, &.{}, ret_ty);
|
||
}
|
||
|
||
/// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get
|
||
/// a string, parses it as sx code, and lowers each statement inline.
|
||
fn lowerInsertExpr(self: *Lowering, expr: *const Node) void {
|
||
_ = self.lowerInsertExprValue(expr);
|
||
}
|
||
|
||
/// Like lowerInsertExpr but returns the value of the last parsed expression.
|
||
fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref {
|
||
// Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal)
|
||
const substituted = if (self.comptime_param_nodes) |cpn|
|
||
self.substituteComptimeNodes(expr, cpn) catch expr
|
||
else
|
||
expr;
|
||
|
||
// Step 2: Evaluate the expression to get a string
|
||
const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void);
|
||
|
||
// Step 3: Parse the string as sx code and lower each statement
|
||
// The last expression's value is captured as the return value
|
||
var p = parser_mod.Parser.init(self.alloc, code_str);
|
||
var last_val: Ref = self.builder.constInt(0, .void);
|
||
while (p.current.tag != .eof) {
|
||
const stmt = p.parseStmt() catch break;
|
||
if (p.current.tag == .eof) {
|
||
// Last statement — try to capture as expression value
|
||
// Note: tryLowerAsExpr internally calls lowerStmt for statement nodes,
|
||
// so we must NOT call lowerStmt again in the else branch.
|
||
if (self.tryLowerAsExpr(stmt)) |val| {
|
||
last_val = val;
|
||
}
|
||
} else {
|
||
self.lowerStmt(stmt);
|
||
}
|
||
}
|
||
return last_val;
|
||
}
|
||
|
||
/// Evaluate an expression at compile time, returning its string value.
|
||
/// Returns null if evaluation fails.
|
||
fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 {
|
||
// Case 1: String literal — return it directly (no need for interpreter)
|
||
if (expr.data == .string_literal) {
|
||
const lit = expr.data.string_literal;
|
||
const str = if (lit.is_raw)
|
||
lit.raw
|
||
else
|
||
unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
|
||
return self.alloc.dupeZ(u8, str) catch null;
|
||
}
|
||
|
||
// Case 2: Evaluate via IR interpreter
|
||
// Build a targeted comptime module with only the needed functions
|
||
var ct_module = Module.init(self.alloc);
|
||
var ct_lowering = Lowering.init(&ct_module);
|
||
ct_lowering.main_file = null; // no main file filtering
|
||
ct_lowering.comptime_param_nodes = self.comptime_param_nodes;
|
||
ct_lowering.fn_ast_map = self.fn_ast_map; // share AST map for lazy resolution
|
||
|
||
// Lower only the functions reachable from this expression.
|
||
// For a call like build_format(fmt), we need build_format's AST.
|
||
if (expr.data == .call) {
|
||
self.lowerComptimeDeps(&ct_lowering, expr);
|
||
}
|
||
|
||
// Create a comptime function that evaluates the expression
|
||
const ct_func_id = ct_lowering.createComptimeFunction("__insert", expr, .string);
|
||
|
||
// Run the interpreter
|
||
var interp = interp_mod.Interpreter.init(&ct_module, self.alloc);
|
||
defer interp.deinit();
|
||
|
||
const result = interp.call(ct_func_id, &.{}) catch return null;
|
||
|
||
// Extract string value
|
||
const str = result.asString(&interp) orelse switch (result) {
|
||
.string => |s| s,
|
||
else => return null,
|
||
};
|
||
|
||
return self.alloc.dupeZ(u8, str) catch null;
|
||
}
|
||
|
||
/// Lower the direct callee of a comptime expression into the ct module.
|
||
/// Transitive dependencies are resolved lazily via the shared fn_ast_map.
|
||
fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void {
|
||
if (expr.data != .call) return;
|
||
if (expr.data.call.callee.data != .identifier) return;
|
||
const name = expr.data.call.callee.data.identifier.name;
|
||
if (resolveBuiltin(name) != null) return;
|
||
if (self.fn_ast_map.get(name)) |fd| {
|
||
if (ct.resolveFuncByName(name) == null) {
|
||
ct.lowerFunction(fd, name, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Substitute comptime parameter identifiers with their actual AST nodes.
|
||
fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node {
|
||
// Direct identifier match
|
||
if (node.data == .identifier) {
|
||
if (cpn.get(node.data.identifier.name)) |replacement| {
|
||
return replacement;
|
||
}
|
||
}
|
||
|
||
// Recurse into call arguments
|
||
if (node.data == .call) {
|
||
var changed = false;
|
||
const new_args = try self.alloc.alloc(*Node, node.data.call.args.len);
|
||
for (node.data.call.args, 0..) |arg, i| {
|
||
const substituted = try self.substituteComptimeNodes(arg, cpn);
|
||
new_args[i] = @constCast(substituted);
|
||
if (substituted != arg) changed = true;
|
||
}
|
||
if (changed) {
|
||
const new_node = try self.alloc.create(Node);
|
||
new_node.* = .{
|
||
.span = node.span,
|
||
.data = .{ .call = .{
|
||
.callee = node.data.call.callee,
|
||
.args = new_args,
|
||
} },
|
||
};
|
||
return new_node;
|
||
}
|
||
}
|
||
|
||
return node;
|
||
}
|
||
|
||
/// Lower a call to a function with comptime params by inlining its body.
|
||
/// Comptime params are substituted, `#insert` expressions are evaluated.
|
||
fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref {
|
||
// Build comptime param substitution map: param_name → call_site AST node
|
||
var cpn = std.StringHashMap(*const Node).init(self.alloc);
|
||
var call_arg_idx: usize = 0;
|
||
|
||
for (fd.params) |param| {
|
||
if (param.is_variadic) {
|
||
// Variadic param: pack remaining call args into []Any slice
|
||
self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx);
|
||
break; // variadic is always the last param
|
||
}
|
||
if (call_arg_idx >= call_node.args.len) break;
|
||
if (param.is_comptime) {
|
||
cpn.put(param.name, call_node.args[call_arg_idx]) catch {};
|
||
call_arg_idx += 1;
|
||
} else {
|
||
const arg_val = self.lowerExpr(call_node.args[call_arg_idx]);
|
||
const pty = self.resolveParamType(¶m);
|
||
const slot = self.builder.alloca(pty);
|
||
self.builder.store(slot, arg_val);
|
||
if (self.scope) |scope| {
|
||
scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
||
}
|
||
call_arg_idx += 1;
|
||
}
|
||
}
|
||
|
||
// Also bind comptime params as local string variables (for `fmt` used in runtime code)
|
||
var cpn_iter = cpn.iterator();
|
||
while (cpn_iter.next()) |entry| {
|
||
const param_name = entry.key_ptr.*;
|
||
const param_node = entry.value_ptr.*;
|
||
if (param_node.data == .string_literal) {
|
||
// Create a local string variable with the literal value
|
||
const str_ref = self.lowerExpr(param_node);
|
||
const slot = self.builder.alloca(.string);
|
||
self.builder.store(slot, str_ref);
|
||
if (self.scope) |scope| {
|
||
scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Install comptime param nodes and lower the function body inline
|
||
const saved_cpn = self.comptime_param_nodes;
|
||
self.comptime_param_nodes = cpn;
|
||
defer self.comptime_param_nodes = saved_cpn;
|
||
|
||
// Lower the body — capture return value for functions with return type
|
||
const ret_ty = self.resolveReturnType(fd);
|
||
if (ret_ty != .void) {
|
||
if (self.lowerBlockValue(fd.body)) |val| {
|
||
return val;
|
||
}
|
||
} else {
|
||
self.lowerBlock(fd.body);
|
||
}
|
||
|
||
return self.builder.constInt(0, .void);
|
||
}
|
||
|
||
/// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value},
|
||
/// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name.
|
||
fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void {
|
||
const any_slice_ty = self.module.types.sliceOf(.any);
|
||
const n = if (call_args.len > start_idx) call_args.len - start_idx else 0;
|
||
|
||
if (n == 0) {
|
||
// Empty slice: {null, 0}
|
||
const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any));
|
||
const zero_len = self.builder.constInt(0, .s64);
|
||
const slice_slot = self.builder.alloca(any_slice_ty);
|
||
// Store ptr (field 0) and len (field 1) into the slice alloca
|
||
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty);
|
||
self.builder.store(ptr_gep, null_ptr);
|
||
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
||
self.builder.store(len_gep, zero_len);
|
||
if (self.scope) |scope| {
|
||
scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Allocate stack array [N x Any]
|
||
const array_ty = self.module.types.arrayOf(.any, @intCast(n));
|
||
const array_slot = self.builder.alloca(array_ty);
|
||
|
||
// Box each arg and store into array
|
||
for (call_args[start_idx..], 0..) |arg, i| {
|
||
var val = self.lowerExpr(arg);
|
||
var source_ty = self.inferExprType(arg);
|
||
// If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that
|
||
if (source_ty == .s64) {
|
||
const ref_ty = self.builder.getRefType(val);
|
||
if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) {
|
||
source_ty = ref_ty;
|
||
} else if (!ref_ty.isBuiltin()) {
|
||
const ri = self.module.types.get(ref_ty);
|
||
if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) {
|
||
source_ty = ref_ty;
|
||
}
|
||
}
|
||
}
|
||
// Auto-unwrap optionals: box inner value if present, else box string "null"
|
||
if (!source_ty.isBuiltin()) {
|
||
const opt_info = self.module.types.get(source_ty);
|
||
if (opt_info == .optional) {
|
||
const child_ty = opt_info.optional.child;
|
||
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool);
|
||
const some_bb = self.freshBlock("opt.some");
|
||
const none_bb = self.freshBlock("opt.none");
|
||
const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any});
|
||
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
||
self.builder.switchToBlock(some_bb);
|
||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
||
const boxed_inner = self.builder.boxAny(unwrapped, child_ty);
|
||
self.builder.br(merge_bb, &.{boxed_inner});
|
||
self.builder.switchToBlock(none_bb);
|
||
const null_str_id = self.module.types.internString("null");
|
||
const null_str = self.builder.constString(null_str_id);
|
||
const boxed_null = self.builder.boxAny(null_str, .string);
|
||
self.builder.br(merge_bb, &.{boxed_null});
|
||
self.builder.switchToBlock(merge_bb);
|
||
val = self.builder.blockParam(merge_bb, 0, TypeId.any);
|
||
source_ty = .any;
|
||
}
|
||
}
|
||
const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty);
|
||
// GEP to array[i] and store
|
||
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
||
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any));
|
||
self.builder.store(elem_ptr, boxed);
|
||
}
|
||
|
||
// Build slice {ptr_to_first_element, len}
|
||
const slice_slot = self.builder.alloca(any_slice_ty);
|
||
// Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any)
|
||
const zero = self.builder.constInt(0, .s64);
|
||
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any));
|
||
const len_ref = self.builder.constInt(@intCast(n), .s64);
|
||
// Store into slice fields
|
||
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty);
|
||
self.builder.store(ptr_gep, data_ptr);
|
||
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
||
self.builder.store(len_gep, len_ref);
|
||
|
||
if (self.scope) |scope| {
|
||
scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
||
}
|
||
}
|
||
|
||
/// Pack variadic args into a slice for regular function calls.
|
||
/// Detects variadic params in the function decl, packs remaining args into a typed slice,
|
||
/// and replaces the args list with [fixed_args..., slice_ref].
|
||
fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void {
|
||
// Find variadic param index
|
||
var variadic_idx: ?usize = null;
|
||
var elem_ty: TypeId = .any;
|
||
for (fd.params, 0..) |p, i| {
|
||
if (p.is_variadic) {
|
||
variadic_idx = i;
|
||
elem_ty = self.resolveTypeWithBindings(p.type_expr);
|
||
break;
|
||
}
|
||
}
|
||
const vi = variadic_idx orelse return; // no variadic param
|
||
|
||
// Number of non-variadic args
|
||
const fixed_count = vi;
|
||
const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0;
|
||
const slice_ty = self.module.types.sliceOf(elem_ty);
|
||
|
||
// Check for spread operator: sum(..arr) — single spread arg becomes the slice directly
|
||
if (variadic_count == 1 and fixed_count < c.args.len) {
|
||
const arg_node = c.args[fixed_count];
|
||
if (arg_node.data == .spread_expr) {
|
||
const spread = arg_node.data.spread_expr;
|
||
const arr_val = self.lowerExpr(spread.operand);
|
||
const arr_ty = self.inferExprType(spread.operand);
|
||
const arr_info = self.module.types.get(arr_ty);
|
||
// Convert array to slice
|
||
const slice_val = switch (arr_info) {
|
||
.array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty),
|
||
.slice => arr_val,
|
||
else => arr_val,
|
||
};
|
||
args.shrinkRetainingCapacity(fixed_count);
|
||
args.append(self.alloc, slice_val) catch unreachable;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (variadic_count == 0) {
|
||
// Empty slice
|
||
const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty));
|
||
const zero_len = self.builder.constInt(0, .s64);
|
||
const slice_slot = self.builder.alloca(slice_ty);
|
||
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty);
|
||
self.builder.store(ptr_gep, null_ptr);
|
||
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty);
|
||
self.builder.store(len_gep, zero_len);
|
||
const slice_val = self.builder.load(slice_slot, slice_ty);
|
||
// Replace args: keep fixed args, append slice
|
||
args.shrinkRetainingCapacity(fixed_count);
|
||
args.append(self.alloc, slice_val) catch unreachable;
|
||
return;
|
||
}
|
||
|
||
// Determine if we need to box as Any (for ..Any params) or use raw type
|
||
const is_any = (elem_ty == .any);
|
||
|
||
// Allocate stack array [N x ElemType]
|
||
const array_elem = if (is_any) TypeId.any else elem_ty;
|
||
const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count));
|
||
const array_slot = self.builder.alloca(array_ty);
|
||
|
||
// Store each variadic arg into array
|
||
for (0..variadic_count) |i| {
|
||
var val = args.items[fixed_count + i];
|
||
if (is_any) {
|
||
var source_ty = self.inferExprType(c.args[fixed_count + i]);
|
||
// If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that
|
||
if (source_ty == .s64) {
|
||
const ref_ty = self.builder.getRefType(val);
|
||
if (ref_ty != .s64 and ref_ty != .void) source_ty = ref_ty;
|
||
}
|
||
// Auto-unwrap optionals: box inner value if present, else box string "null"
|
||
if (!source_ty.isBuiltin()) {
|
||
const opt_info = self.module.types.get(source_ty);
|
||
if (opt_info == .optional) {
|
||
const child_ty = opt_info.optional.child;
|
||
// Branch: has_value? → box inner : box "null"
|
||
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool);
|
||
const some_bb = self.freshBlock("opt.some");
|
||
const none_bb = self.freshBlock("opt.none");
|
||
const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any});
|
||
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
||
// Some: unwrap and box inner value
|
||
self.builder.switchToBlock(some_bb);
|
||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
||
const boxed_inner = self.builder.boxAny(unwrapped, child_ty);
|
||
self.builder.br(merge_bb, &.{boxed_inner});
|
||
// None: box the string "null"
|
||
self.builder.switchToBlock(none_bb);
|
||
const null_str_id = self.module.types.internString("null");
|
||
const null_str = self.builder.constString(null_str_id);
|
||
const boxed_null = self.builder.boxAny(null_str, .string);
|
||
self.builder.br(merge_bb, &.{boxed_null});
|
||
// Merge
|
||
self.builder.switchToBlock(merge_bb);
|
||
val = self.builder.blockParam(merge_bb, 0, TypeId.any);
|
||
source_ty = .any; // already boxed
|
||
}
|
||
}
|
||
if (source_ty != .any) {
|
||
val = self.builder.boxAny(val, source_ty);
|
||
}
|
||
}
|
||
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
||
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem));
|
||
self.builder.store(elem_ptr, val);
|
||
}
|
||
|
||
// Build slice {ptr, len}
|
||
const slice_slot = self.builder.alloca(slice_ty);
|
||
const zero = self.builder.constInt(0, .s64);
|
||
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem));
|
||
const len_ref = self.builder.constInt(@intCast(variadic_count), .s64);
|
||
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty);
|
||
self.builder.store(ptr_gep, data_ptr);
|
||
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty);
|
||
self.builder.store(len_gep, len_ref);
|
||
const slice_val = self.builder.load(slice_slot, slice_ty);
|
||
|
||
// Replace args: keep fixed args, append slice
|
||
args.shrinkRetainingCapacity(fixed_count);
|
||
args.append(self.alloc, slice_val) catch unreachable;
|
||
}
|
||
|
||
// ── Generic monomorphization ──────────────────────────────────
|
||
|
||
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
|
||
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
|
||
// Infer type param bindings from call arguments
|
||
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
||
defer bindings.deinit();
|
||
|
||
// Determine if type args are passed explicitly:
|
||
// If call_node.args.len == fd.params.len, the caller passed type args explicitly
|
||
// (e.g., are_equal(Point, p1, p2)). Otherwise, types are inferred from value args
|
||
// (e.g., are_equal(p1, p2)).
|
||
const types_passed_explicitly = call_node.args.len == fd.params.len;
|
||
|
||
for (fd.type_params) |tp| {
|
||
var found = false;
|
||
|
||
// Strategy 1: Direct type param declaration ($T: Type)
|
||
// The param whose name matches the type param IS the declaration.
|
||
// The call arg at that position is a type expression — resolve it directly.
|
||
// Only applies when type args are passed explicitly in the call.
|
||
if (types_passed_explicitly) {
|
||
for (fd.params, 0..) |param, pi| {
|
||
if (std.mem.eql(u8, param.name, tp.name)) {
|
||
// This param IS the type param declaration
|
||
if (pi < call_node.args.len) {
|
||
const ty = self.resolveTypeArg(call_node.args[pi]);
|
||
bindings.put(tp.name, ty) catch {};
|
||
found = true;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (found) continue;
|
||
|
||
// Strategy 2: Infer from params that USE the type param (e.g., a: $T, b: T, items: []$T)
|
||
// Check ALL params whose type matches the type param name, pick widest type.
|
||
// When types are inferred (not explicit), use a separate arg index that
|
||
// skips type param declarations to correctly map params to call args.
|
||
var inferred_ty: ?TypeId = null;
|
||
var s2_arg_idx: usize = 0;
|
||
for (fd.params) |param| {
|
||
const is_type_decl = isTypeParamDecl(¶m, fd.type_params);
|
||
defer if (!is_type_decl) {
|
||
s2_arg_idx += 1;
|
||
};
|
||
if (is_type_decl) {
|
||
if (types_passed_explicitly) s2_arg_idx += 1;
|
||
continue;
|
||
}
|
||
const matched = self.matchTypeParam(param.type_expr, tp.name);
|
||
if (matched) {
|
||
if (s2_arg_idx < call_node.args.len) {
|
||
const arg_ty = self.inferExprType(call_node.args[s2_arg_idx]);
|
||
const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name);
|
||
if (extracted) |ety| {
|
||
if (inferred_ty) |prev| {
|
||
if (ety == .f64 and prev != .f64) {
|
||
inferred_ty = ety;
|
||
} else if (ety == .f32 and prev != .f64 and prev != .f32) {
|
||
inferred_ty = ety;
|
||
}
|
||
} else {
|
||
inferred_ty = ety;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (inferred_ty) |ty| {
|
||
bindings.put(tp.name, ty) catch {};
|
||
}
|
||
}
|
||
|
||
// Build mangled name: "func_name__Type1_Type2"
|
||
var mangled_buf: [256]u8 = undefined;
|
||
var mangled_len: usize = 0;
|
||
for (base_name) |ch| {
|
||
if (mangled_len < mangled_buf.len) {
|
||
mangled_buf[mangled_len] = ch;
|
||
mangled_len += 1;
|
||
}
|
||
}
|
||
for (fd.type_params) |tp| {
|
||
// Append separator
|
||
for ("__") |ch| {
|
||
if (mangled_len < mangled_buf.len) {
|
||
mangled_buf[mangled_len] = ch;
|
||
mangled_len += 1;
|
||
}
|
||
}
|
||
// Append type name
|
||
const ty = bindings.get(tp.name) orelse .s64;
|
||
const type_name_str = self.mangleTypeName(ty);
|
||
for (type_name_str) |ch| {
|
||
if (mangled_len < mangled_buf.len) {
|
||
mangled_buf[mangled_len] = ch;
|
||
mangled_len += 1;
|
||
}
|
||
}
|
||
}
|
||
const mangled_name = mangled_buf[0..mangled_len];
|
||
|
||
// Check cache
|
||
if (!self.lowered_functions.contains(mangled_name)) {
|
||
// Monomorphize: create a new function with the mangled name and lower with type bindings
|
||
self.monomorphizeFunction(fd, mangled_name, &bindings);
|
||
}
|
||
|
||
// Resolve the monomorphized function and call it (stripping type args)
|
||
if (self.resolveFuncByName(mangled_name)) |fid| {
|
||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||
const ret_ty = func.ret;
|
||
const params = func.params;
|
||
// Build value-only args (skip type param declaration args)
|
||
// Use separate index for lowered_args since type params don't consume call args
|
||
var value_args = std.ArrayList(Ref).empty;
|
||
defer value_args.deinit(self.alloc);
|
||
var arg_idx: usize = 0;
|
||
for (fd.params) |p| {
|
||
if (isTypeParamDecl(&p, fd.type_params)) {
|
||
// Only skip in lowered_args if types were passed explicitly in the call
|
||
if (types_passed_explicitly) arg_idx += 1;
|
||
continue;
|
||
}
|
||
if (arg_idx < lowered_args.len) {
|
||
value_args.append(self.alloc, lowered_args[arg_idx]) catch unreachable;
|
||
}
|
||
arg_idx += 1;
|
||
}
|
||
self.coerceCallArgs(value_args.items, params);
|
||
return self.builder.call(fid, value_args.items, ret_ty);
|
||
}
|
||
|
||
return self.emitError(base_name, call_node.callee.span);
|
||
}
|
||
|
||
/// Create a monomorphized instance of a generic function.
|
||
/// Check if a call has a `cast(runtime_var, val)` argument (runtime type dispatch pattern).
|
||
fn hasCastWithRuntimeType(self: *Lowering, c: *const ast.Call) bool {
|
||
for (c.args) |arg| {
|
||
if (arg.data == .call) {
|
||
if (arg.data.call.callee.data == .identifier) {
|
||
const name = arg.data.call.callee.data.identifier.name;
|
||
if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) {
|
||
const type_arg = arg.data.call.args[0];
|
||
if (type_arg.data == .identifier) {
|
||
// It's a runtime type if it's in scope as a variable
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(type_arg.data.identifier.name) != null) return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Generate runtime dispatch for a generic call inside a type-match arm.
|
||
/// For each type tag in match_tags, monomorphizes the generic function and calls it.
|
||
fn lowerRuntimeDispatchCall(
|
||
self: *Lowering,
|
||
fd: *const ast.FnDecl,
|
||
base_name: []const u8,
|
||
call_node: *const ast.Call,
|
||
match_tags: []const u64,
|
||
) Ref {
|
||
// Find the cast arg: cast(type_var, any_val)
|
||
var cast_arg_idx: usize = 0;
|
||
var type_tag_node: ?*const Node = null;
|
||
var any_val_node: ?*const Node = null;
|
||
for (call_node.args, 0..) |arg, i| {
|
||
if (arg.data == .call and arg.data.call.callee.data == .identifier) {
|
||
const name = arg.data.call.callee.data.identifier.name;
|
||
if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) {
|
||
cast_arg_idx = i;
|
||
type_tag_node = arg.data.call.args[0];
|
||
any_val_node = arg.data.call.args[1];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Lower the type tag (runtime value) and Any value BEFORE the switch
|
||
const type_tag = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span));
|
||
const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span));
|
||
|
||
// Lower non-cast arguments once (before the switch)
|
||
var other_args = std.ArrayList(?Ref).empty;
|
||
defer other_args.deinit(self.alloc);
|
||
for (call_node.args, 0..) |arg, i| {
|
||
if (i == cast_arg_idx) {
|
||
other_args.append(self.alloc, null) catch unreachable; // placeholder
|
||
} else {
|
||
other_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable;
|
||
}
|
||
}
|
||
|
||
// Resolve return type (using first available binding)
|
||
const ret_ty: TypeId = blk: {
|
||
if (fd.return_type) |rt| {
|
||
if (rt.data == .type_expr) {
|
||
if (type_bridge.resolveAstType(rt, &self.module.types) != .s64) {
|
||
break :blk type_bridge.resolveAstType(rt, &self.module.types);
|
||
}
|
||
}
|
||
}
|
||
break :blk .string; // default for to_string functions
|
||
};
|
||
|
||
const merge_bb = self.freshBlock("dispatch.merge");
|
||
const default_bb = self.freshBlock("dispatch.default");
|
||
|
||
// Build switch cases
|
||
var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty;
|
||
defer cases.deinit(self.alloc);
|
||
|
||
// For each type tag, create a case block
|
||
var case_blocks = std.ArrayList(BlockId).empty;
|
||
defer case_blocks.deinit(self.alloc);
|
||
|
||
for (match_tags) |tag| {
|
||
const case_bb = self.freshBlock("dispatch.case");
|
||
case_blocks.append(self.alloc, case_bb) catch unreachable;
|
||
cases.append(self.alloc, .{
|
||
.value = @intCast(tag),
|
||
.target = case_bb,
|
||
.args = &.{},
|
||
}) catch unreachable;
|
||
}
|
||
|
||
// Create a result alloca BEFORE the switch (must be before terminator)
|
||
var result_slot: ?Ref = null;
|
||
if (ret_ty != .void) {
|
||
result_slot = self.builder.alloca(ret_ty);
|
||
}
|
||
|
||
self.builder.switchBr(type_tag, cases.items, default_bb, &.{});
|
||
|
||
for (match_tags, 0..) |tag, ti| {
|
||
self.builder.switchToBlock(case_blocks.items[ti]);
|
||
|
||
const ty_id = TypeId.fromIndex(@intCast(tag));
|
||
|
||
// Unbox the Any value to the concrete type
|
||
const unboxed = self.builder.emit(.{ .unbox_any = .{
|
||
.operand = any_val,
|
||
} }, ty_id);
|
||
|
||
if (fd.type_params.len > 0) {
|
||
// Generic function: build type bindings + monomorphize
|
||
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
||
defer bindings.deinit();
|
||
|
||
// Find which type param the cast arg corresponds to
|
||
if (cast_arg_idx < fd.params.len) {
|
||
const param_te = fd.params[cast_arg_idx].type_expr;
|
||
if (param_te.data == .type_expr) {
|
||
// Direct: `param: $T` → T = ty_id
|
||
const tp_name = param_te.data.type_expr.name;
|
||
for (fd.type_params) |tp| {
|
||
if (std.mem.eql(u8, tp.name, tp_name)) {
|
||
bindings.put(tp.name, ty_id) catch {};
|
||
break;
|
||
}
|
||
}
|
||
} else if (param_te.data == .slice_type_expr) {
|
||
// Compound: `param: []$T` → T = element type of ty_id
|
||
const elem_te = param_te.data.slice_type_expr.element_type;
|
||
if (elem_te.data == .type_expr) {
|
||
const tp_name = elem_te.data.type_expr.name;
|
||
for (fd.type_params) |tp| {
|
||
if (std.mem.eql(u8, tp.name, tp_name)) {
|
||
const elem_ty = self.getElementType(ty_id);
|
||
bindings.put(tp.name, if (elem_ty != .void) elem_ty else ty_id) catch {};
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} else if (param_te.data == .pointer_type_expr) {
|
||
// Compound: `param: *$T` → T = pointee type of ty_id
|
||
const pointee_te = param_te.data.pointer_type_expr.pointee_type;
|
||
if (pointee_te.data == .type_expr) {
|
||
const tp_name = pointee_te.data.type_expr.name;
|
||
for (fd.type_params) |tp| {
|
||
if (std.mem.eql(u8, tp.name, tp_name)) {
|
||
if (!ty_id.isBuiltin()) {
|
||
const pinfo = self.module.types.get(ty_id);
|
||
if (pinfo == .pointer) {
|
||
bindings.put(tp.name, pinfo.pointer.pointee) catch {};
|
||
break;
|
||
}
|
||
}
|
||
bindings.put(tp.name, ty_id) catch {};
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Build mangled name
|
||
var mangled_buf: [256]u8 = undefined;
|
||
var mangled_len: usize = 0;
|
||
for (base_name) |ch| {
|
||
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
||
}
|
||
for (fd.type_params) |tp| {
|
||
for ("__") |ch| {
|
||
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
||
}
|
||
const bound_ty = bindings.get(tp.name) orelse ty_id;
|
||
const type_name_str = self.mangleTypeName(bound_ty);
|
||
for (type_name_str) |ch| {
|
||
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
||
}
|
||
}
|
||
const mangled_name = mangled_buf[0..mangled_len];
|
||
|
||
// Monomorphize if not already done
|
||
if (!self.lowered_functions.contains(mangled_name)) {
|
||
self.monomorphizeFunction(fd, mangled_name, &bindings);
|
||
}
|
||
|
||
// Build call args (replace cast arg with unboxed value, skip type param decl args)
|
||
if (self.resolveFuncByName(mangled_name)) |fid| {
|
||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||
const callee_ret = func.ret;
|
||
const callee_params = func.params;
|
||
var call_args = std.ArrayList(Ref).empty;
|
||
defer call_args.deinit(self.alloc);
|
||
for (fd.params, 0..) |p, pi| {
|
||
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
||
if (pi == cast_arg_idx) {
|
||
call_args.append(self.alloc, unboxed) catch unreachable;
|
||
} else if (pi < other_args.items.len) {
|
||
if (other_args.items[pi]) |ref| {
|
||
call_args.append(self.alloc, ref) catch unreachable;
|
||
}
|
||
}
|
||
}
|
||
self.coerceCallArgs(call_args.items, callee_params);
|
||
const result = self.builder.call(fid, call_args.items, callee_ret);
|
||
if (result_slot) |slot| {
|
||
self.builder.store(slot, result);
|
||
}
|
||
}
|
||
} else {
|
||
// Non-generic function: call directly with per-tag unboxing + coercion
|
||
const resolve_name = base_name;
|
||
if (!self.lowered_functions.contains(resolve_name)) {
|
||
self.lazyLowerFunction(resolve_name);
|
||
}
|
||
if (self.resolveFuncByName(resolve_name)) |fid| {
|
||
const callee_ret = self.module.functions.items[@intFromEnum(fid)].ret;
|
||
const callee_params = self.module.functions.items[@intFromEnum(fid)].params;
|
||
var call_args = std.ArrayList(Ref).empty;
|
||
defer call_args.deinit(self.alloc);
|
||
for (fd.params, 0..) |_, pi| {
|
||
if (pi == cast_arg_idx) {
|
||
// Coerce unboxed value (typed as ty_id) to param type
|
||
var arg = unboxed;
|
||
if (pi < callee_params.len) {
|
||
arg = self.coerceToType(arg, ty_id, callee_params[pi].ty);
|
||
}
|
||
call_args.append(self.alloc, arg) catch unreachable;
|
||
} else if (pi < other_args.items.len) {
|
||
if (other_args.items[pi]) |ref| {
|
||
call_args.append(self.alloc, ref) catch unreachable;
|
||
}
|
||
}
|
||
}
|
||
// Coerce non-cast args (source type unknown, use s64 default)
|
||
for (0..@min(call_args.items.len, callee_params.len)) |ci| {
|
||
if (ci != cast_arg_idx) {
|
||
call_args.items[ci] = self.coerceToType(call_args.items[ci], .s64, callee_params[ci].ty);
|
||
}
|
||
}
|
||
const result = self.builder.call(fid, call_args.items, callee_ret);
|
||
if (result_slot) |slot| {
|
||
self.builder.store(slot, result);
|
||
}
|
||
}
|
||
}
|
||
|
||
self.builder.br(merge_bb, &.{});
|
||
}
|
||
|
||
// Default block: store a default value and branch to merge
|
||
self.builder.switchToBlock(default_bb);
|
||
if (result_slot) |slot| {
|
||
const empty_id = self.module.types.internString("");
|
||
const default_val = if (ret_ty == .string) self.builder.constString(empty_id) else self.zeroValue(ret_ty);
|
||
self.builder.store(slot, default_val);
|
||
}
|
||
self.builder.br(merge_bb, &.{});
|
||
|
||
// Merge block: load result
|
||
self.builder.switchToBlock(merge_bb);
|
||
if (result_slot) |slot| {
|
||
return self.builder.load(slot, ret_ty);
|
||
}
|
||
return self.builder.constInt(0, .void);
|
||
}
|
||
|
||
fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void {
|
||
// Mark as lowered before lowering (prevents infinite recursion)
|
||
// Need to dupe the name since mangled_name may be stack-allocated
|
||
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
||
self.lowered_functions.put(owned_name, {}) catch {};
|
||
|
||
// Save builder state
|
||
const saved_func = self.builder.func;
|
||
const saved_block = self.builder.current_block;
|
||
const saved_counter = self.builder.inst_counter;
|
||
const saved_scope = self.scope;
|
||
const saved_bindings = self.type_bindings;
|
||
const saved_defer_base = self.func_defer_base;
|
||
const saved_block_terminated = self.block_terminated;
|
||
const saved_target = self.target_type;
|
||
self.func_defer_base = self.defer_stack.items.len;
|
||
self.block_terminated = false;
|
||
|
||
// Install type bindings
|
||
self.type_bindings = bindings.*;
|
||
|
||
// Resolve return type with type bindings active. The body's tail
|
||
// expression inherits this as its target_type so bare `.{...}`
|
||
// literals resolve to the monomorphised return type instead of
|
||
// whatever leaked in from the caller (e.g. caller's xx target).
|
||
const ret_ty = self.resolveReturnType(fd);
|
||
self.target_type = ret_ty;
|
||
|
||
// Build param list (substituting type params, skipping type param declarations)
|
||
var params = std.ArrayList(Function.Param).empty;
|
||
for (fd.params) |p| {
|
||
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
||
const pty = self.resolveParamType(&p);
|
||
params.append(self.alloc, .{
|
||
.name = self.module.types.internString(p.name),
|
||
.ty = pty,
|
||
}) catch unreachable;
|
||
}
|
||
|
||
// Create the monomorphized function
|
||
const name_id = self.module.types.internString(owned_name);
|
||
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
||
_ = func_id;
|
||
|
||
// Create entry block
|
||
const entry_name = self.module.types.internString("entry");
|
||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||
self.builder.switchToBlock(entry);
|
||
|
||
// Create scope and bind params
|
||
var scope = Scope.init(self.alloc, null);
|
||
defer scope.deinit();
|
||
self.scope = &scope;
|
||
|
||
{
|
||
var param_idx: u32 = 0;
|
||
for (fd.params) |p| {
|
||
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
||
const pty = self.resolveParamType(&p);
|
||
const slot = self.builder.alloca(pty);
|
||
const param_ref = Ref.fromIndex(param_idx);
|
||
self.builder.store(slot, param_ref);
|
||
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
||
param_idx += 1;
|
||
}
|
||
}
|
||
|
||
// Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32)
|
||
if (fd.body.data == .builtin_expr) {
|
||
// Emit builtin call with param 0, then return
|
||
if (resolveBuiltin(fd.name)) |bid| {
|
||
const param0 = Ref.fromIndex(0);
|
||
const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty);
|
||
self.builder.ret(result, ret_ty);
|
||
} else {
|
||
self.ensureTerminator(ret_ty);
|
||
}
|
||
self.builder.finalize();
|
||
} else {
|
||
// Lower the function body
|
||
if (ret_ty != .void) {
|
||
const body_val = self.lowerBlockValue(fd.body);
|
||
if (!self.currentBlockHasTerminator()) {
|
||
if (body_val) |val| {
|
||
const val_ty = self.builder.getRefType(val);
|
||
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
||
self.builder.ret(coerced, ret_ty);
|
||
} else {
|
||
self.ensureTerminator(ret_ty);
|
||
}
|
||
}
|
||
} else {
|
||
self.lowerBlock(fd.body);
|
||
self.ensureTerminator(ret_ty);
|
||
}
|
||
self.builder.finalize();
|
||
}
|
||
|
||
// Restore builder state
|
||
self.type_bindings = saved_bindings;
|
||
self.scope = saved_scope;
|
||
self.func_defer_base = saved_defer_base;
|
||
self.block_terminated = saved_block_terminated;
|
||
self.target_type = saved_target;
|
||
self.builder.func = saved_func;
|
||
self.builder.current_block = saved_block;
|
||
self.builder.inst_counter = saved_counter;
|
||
}
|
||
|
||
// ── Reflection builtins ────────────────────────────────────────
|
||
|
||
/// Try to lower a call as a reflection builtin (expanded inline during lowering).
|
||
/// Returns null if the call is not a recognized reflection builtin.
|
||
fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref {
|
||
if (std.mem.eql(u8, name, "size_of")) {
|
||
// size_of(T) → const_int(sizeof(T))
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
const size: i64 = @intCast(self.typeSizeBytes(ty));
|
||
return self.builder.constInt(size, .s64);
|
||
}
|
||
if (std.mem.eql(u8, name, "field_count")) {
|
||
// field_count(T) → const_int(N)
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
const info = self.module.types.get(ty);
|
||
const count: i64 = switch (info) {
|
||
.@"struct" => |s| @intCast(s.fields.len),
|
||
.@"union" => |u| @intCast(u.fields.len),
|
||
.tagged_union => |u| @intCast(u.fields.len),
|
||
.@"enum" => |e| @intCast(e.variants.len),
|
||
.array => |a| @intCast(a.length),
|
||
.vector => |v| @intCast(v.length),
|
||
else => 0,
|
||
};
|
||
return self.builder.constInt(count, .s64);
|
||
}
|
||
if (std.mem.eql(u8, name, "type_name")) {
|
||
// type_name(T) → const_string("TypeName")
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
const tn_str = self.formatTypeName(ty);
|
||
const sid = self.module.types.internString(tn_str);
|
||
return self.builder.constString(sid);
|
||
}
|
||
if (std.mem.eql(u8, name, "is_flags")) {
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
if (!ty.isBuiltin()) {
|
||
const info = self.module.types.get(ty);
|
||
if (info == .@"enum") return self.builder.constBool(info.@"enum".is_flags);
|
||
}
|
||
return self.builder.constBool(false);
|
||
}
|
||
if (std.mem.eql(u8, name, "field_name")) {
|
||
// field_name(T, i) → field_name_get instruction
|
||
if (c.args.len < 2) return self.builder.constString(self.module.types.internString(""));
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
const idx = self.lowerExpr(c.args[1]);
|
||
return self.builder.emit(.{ .field_name_get = .{
|
||
.base = .none,
|
||
.index = idx,
|
||
.struct_type = ty,
|
||
} }, .string);
|
||
}
|
||
if (std.mem.eql(u8, name, "field_value")) {
|
||
// field_value(s, i) → field_value_get instruction (structs/unions)
|
||
// → index_get + box_any (slices/arrays)
|
||
if (c.args.len < 2) return self.builder.constInt(0, .any);
|
||
const base = self.lowerExpr(c.args[0]);
|
||
const idx = self.lowerExpr(c.args[1]);
|
||
const struct_ty = self.inferExprType(c.args[0]);
|
||
|
||
// For slices, arrays, and vectors, use index_get to access elements
|
||
if (!struct_ty.isBuiltin()) {
|
||
const ti = self.module.types.get(struct_ty);
|
||
if (ti == .slice or ti == .array or ti == .vector) {
|
||
const elem_ty = self.getElementType(struct_ty);
|
||
const elem = self.builder.emit(.{ .index_get = .{ .lhs = base, .rhs = idx } }, elem_ty);
|
||
return self.builder.boxAny(elem, elem_ty);
|
||
}
|
||
}
|
||
|
||
return self.builder.emit(.{ .field_value_get = .{
|
||
.base = base,
|
||
.index = idx,
|
||
.struct_type = struct_ty,
|
||
} }, .any);
|
||
}
|
||
if (std.mem.eql(u8, name, "type_of")) {
|
||
// type_of(val) — extract Any tag or produce compile-time constant
|
||
if (c.args.len < 1) return self.builder.constInt(0, .s64);
|
||
const arg_ty = self.inferExprType(c.args[0]);
|
||
if (arg_ty == .any) {
|
||
// Runtime: extract tag field (field 0 of Any {tag: s64, value: s64})
|
||
const val = self.lowerExpr(c.args[0]);
|
||
return self.builder.structGet(val, 0, .s64);
|
||
} else {
|
||
// Static: emit type tag as constant
|
||
return self.builder.constInt(@intCast(@intFromEnum(arg_ty)), .s64);
|
||
}
|
||
}
|
||
if (std.mem.eql(u8, name, "field_index")) {
|
||
// field_index(T, val) → extract tag from tagged union
|
||
if (c.args.len < 2) return self.builder.constInt(0, .s64);
|
||
const val = self.lowerExpr(c.args[1]);
|
||
// For tagged unions: extract field 0 (the tag)
|
||
return self.builder.emit(.{ .enum_tag = .{ .operand = val } }, .s64);
|
||
}
|
||
if (std.mem.eql(u8, name, "field_value_int")) {
|
||
// field_value_int(T, i) → lookup enum variant value by index
|
||
if (c.args.len < 2) return self.builder.constInt(0, .s64);
|
||
const ty = self.resolveTypeArg(c.args[0]);
|
||
const idx = self.lowerExpr(c.args[1]);
|
||
// For enums with explicit values, build a global value array and index into it
|
||
if (!ty.isBuiltin()) {
|
||
const ti = self.module.types.get(ty);
|
||
if (ti == .@"enum") {
|
||
if (ti.@"enum".explicit_values) |vals| {
|
||
// Build inline switch: for each index, return the explicit value
|
||
// Simple approach: build an array of constants and use index_get
|
||
var elems = std.ArrayList(Ref).empty;
|
||
defer elems.deinit(self.alloc);
|
||
for (vals) |v| {
|
||
elems.append(self.alloc, self.builder.constInt(v, .s64)) catch unreachable;
|
||
}
|
||
const arr_ty = self.module.types.arrayOf(.s64, @intCast(vals.len));
|
||
const arr = self.builder.structInit(elems.items, arr_ty);
|
||
return self.builder.emit(.{ .index_get = .{ .lhs = arr, .rhs = idx } }, .s64);
|
||
}
|
||
}
|
||
}
|
||
// Default: return the index itself (regular enums)
|
||
return idx;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Resolve a type argument from a call expression. Handles:
|
||
/// - Type param bindings ($T → concrete type via type_bindings)
|
||
/// - Direct type names (Vec4 → lookup in TypeTable)
|
||
/// - type_expr AST nodes
|
||
fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||
switch (node.data) {
|
||
.identifier => |id| {
|
||
// Check type bindings first (from generic monomorphization)
|
||
if (self.type_bindings) |tb| {
|
||
if (tb.get(id.name)) |ty| return ty;
|
||
}
|
||
// Try as a named type by name (resolveAstType doesn't handle .identifier)
|
||
const name_id = self.module.types.internString(id.name);
|
||
return self.module.types.findByName(name_id) orelse .s64;
|
||
},
|
||
.type_expr => |te| {
|
||
if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
|
||
return type_bridge.resolveAstType(node, &self.module.types);
|
||
},
|
||
.call => |cl| {
|
||
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
|
||
return self.resolveTypeCallWithBindings(&cl);
|
||
},
|
||
else => return .s64,
|
||
}
|
||
}
|
||
|
||
/// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64").
|
||
fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 {
|
||
// Builtin types: use their canonical name
|
||
if (ty == .s8) return "s8";
|
||
if (ty == .s16) return "s16";
|
||
if (ty == .s32) return "s32";
|
||
if (ty == .s64) return "s64";
|
||
if (ty == .u8) return "u8";
|
||
if (ty == .u16) return "u16";
|
||
if (ty == .u32) return "u32";
|
||
if (ty == .u64) return "u64";
|
||
if (ty == .f32) return "f32";
|
||
if (ty == .f64) return "f64";
|
||
if (ty == .bool) return "bool";
|
||
if (ty == .void) return "void";
|
||
if (ty == .string) return "string";
|
||
if (ty == .any) return "Any";
|
||
if (ty == .usize) return "usize";
|
||
if (ty == .isize) return "isize";
|
||
|
||
const info = self.module.types.get(ty);
|
||
return switch (info) {
|
||
.@"struct" => |s| self.module.types.getString(s.name),
|
||
.@"union" => |u| self.module.types.getString(u.name),
|
||
.tagged_union => |u| self.module.types.getString(u.name),
|
||
.@"enum" => |e| self.module.types.getString(e.name),
|
||
.pointer => |p| blk: {
|
||
const inner = self.formatTypeName(p.pointee);
|
||
break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer";
|
||
},
|
||
.many_pointer => |p| blk: {
|
||
const inner = self.formatTypeName(p.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer";
|
||
},
|
||
.slice => |s| blk: {
|
||
const inner = self.formatTypeName(s.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice";
|
||
},
|
||
.array => |a| blk: {
|
||
const inner = self.formatTypeName(a.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "[{d}]{s}", .{ a.length, inner }) catch "array";
|
||
},
|
||
.signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed",
|
||
.unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned",
|
||
.optional => |o| blk: {
|
||
const inner = self.formatTypeName(o.child);
|
||
break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional";
|
||
},
|
||
.vector => |v| blk: {
|
||
const inner = self.formatTypeName(v.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector";
|
||
},
|
||
else => @tagName(info),
|
||
};
|
||
}
|
||
|
||
/// Format a function type string like "() -> s32" or "(s32, s32) -> s32".
|
||
fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 {
|
||
var buf: [512]u8 = undefined;
|
||
var pos: usize = 0;
|
||
buf[pos] = '(';
|
||
pos += 1;
|
||
for (fd.params, 0..) |p, i| {
|
||
if (i > 0) {
|
||
@memcpy(buf[pos..][0..2], ", ");
|
||
pos += 2;
|
||
}
|
||
const pty = self.resolveParamType(&p);
|
||
const name = self.formatTypeName(pty);
|
||
@memcpy(buf[pos..][0..name.len], name);
|
||
pos += name.len;
|
||
}
|
||
buf[pos] = ')';
|
||
pos += 1;
|
||
const ret_ty = self.resolveReturnType(fd);
|
||
if (ret_ty != .void) {
|
||
@memcpy(buf[pos..][0..4], " -> ");
|
||
pos += 4;
|
||
const rname = self.formatTypeName(ret_ty);
|
||
@memcpy(buf[pos..][0..rname.len], rname);
|
||
pos += rname.len;
|
||
}
|
||
const result = self.alloc.alloc(u8, pos) catch unreachable;
|
||
@memcpy(result, buf[0..pos]);
|
||
return result;
|
||
}
|
||
|
||
/// Format a type name for function name mangling (identifier-safe).
|
||
/// E.g. *Point → "ptr_Point", []s32 → "slice_s32", [3]f64 → "array_3_f64".
|
||
/// Check if a param type expression references a type param name (possibly nested).
|
||
fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool {
|
||
return switch (type_node.data) {
|
||
.type_expr => |te| std.mem.eql(u8, te.name, tp_name),
|
||
.identifier => |id| std.mem.eql(u8, id.name, tp_name),
|
||
.slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name),
|
||
.pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name),
|
||
.many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name),
|
||
.optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name),
|
||
.array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name),
|
||
else => false,
|
||
};
|
||
}
|
||
|
||
fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool {
|
||
return switch (type_node.data) {
|
||
.type_expr => |te| std.mem.eql(u8, te.name, tp_name),
|
||
.identifier => |id| std.mem.eql(u8, id.name, tp_name),
|
||
.slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name),
|
||
.pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name),
|
||
.many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name),
|
||
.optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name),
|
||
.array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name),
|
||
else => false,
|
||
};
|
||
}
|
||
|
||
/// Extract the concrete type that corresponds to a type param from an arg type.
|
||
/// E.g., param type []$T with arg type []s64 → T = s64.
|
||
fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId {
|
||
return switch (type_node.data) {
|
||
.type_expr => |te| if (std.mem.eql(u8, te.name, tp_name)) arg_ty else null,
|
||
.identifier => |id| if (std.mem.eql(u8, id.name, tp_name)) arg_ty else null,
|
||
.slice_type_expr => |st| blk: {
|
||
// arg_ty should be a slice → extract element type
|
||
if (arg_ty.isBuiltin()) break :blk null;
|
||
const info = self.module.types.get(arg_ty);
|
||
break :blk switch (info) {
|
||
.slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name),
|
||
else => null,
|
||
};
|
||
},
|
||
.pointer_type_expr => |pt| blk: {
|
||
if (arg_ty.isBuiltin()) break :blk null;
|
||
const info = self.module.types.get(arg_ty);
|
||
break :blk switch (info) {
|
||
.pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name),
|
||
else => null,
|
||
};
|
||
},
|
||
.many_pointer_type_expr => |mp| blk: {
|
||
if (arg_ty.isBuiltin()) break :blk null;
|
||
const info = self.module.types.get(arg_ty);
|
||
break :blk switch (info) {
|
||
.many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name),
|
||
else => null,
|
||
};
|
||
},
|
||
.optional_type_expr => |ot| blk: {
|
||
if (arg_ty.isBuiltin()) break :blk null;
|
||
const info = self.module.types.get(arg_ty);
|
||
break :blk switch (info) {
|
||
.optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name),
|
||
else => null,
|
||
};
|
||
},
|
||
.array_type_expr => |at| blk: {
|
||
if (arg_ty.isBuiltin()) break :blk null;
|
||
const info = self.module.types.get(arg_ty);
|
||
break :blk switch (info) {
|
||
.array => |a| self.extractTypeParam(at.element_type, a.element, tp_name),
|
||
else => null,
|
||
};
|
||
},
|
||
else => null,
|
||
};
|
||
}
|
||
|
||
fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 {
|
||
// Builtin types
|
||
if (ty == .s8) return "s8";
|
||
if (ty == .s16) return "s16";
|
||
if (ty == .s32) return "s32";
|
||
if (ty == .s64) return "s64";
|
||
if (ty == .u8) return "u8";
|
||
if (ty == .u16) return "u16";
|
||
if (ty == .u32) return "u32";
|
||
if (ty == .u64) return "u64";
|
||
if (ty == .f32) return "f32";
|
||
if (ty == .f64) return "f64";
|
||
if (ty == .bool) return "bool";
|
||
if (ty == .void) return "void";
|
||
if (ty == .string) return "string";
|
||
if (ty == .any) return "Any";
|
||
if (ty == .usize) return "usize";
|
||
if (ty == .isize) return "isize";
|
||
|
||
const info = self.module.types.get(ty);
|
||
return switch (info) {
|
||
.@"struct" => |s| self.module.types.getString(s.name),
|
||
.@"union" => |u| self.module.types.getString(u.name),
|
||
.tagged_union => |u| self.module.types.getString(u.name),
|
||
.@"enum" => |e| self.module.types.getString(e.name),
|
||
.pointer => |p| blk: {
|
||
const inner = self.mangleTypeName(p.pointee);
|
||
break :blk std.fmt.allocPrint(self.alloc, "ptr_{s}", .{inner}) catch "pointer";
|
||
},
|
||
.many_pointer => |p| blk: {
|
||
const inner = self.mangleTypeName(p.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "mptr_{s}", .{inner}) catch "many_pointer";
|
||
},
|
||
.slice => |s| blk: {
|
||
const inner = self.mangleTypeName(s.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "SL_{s}", .{inner}) catch "slice";
|
||
},
|
||
.array => |a| blk: {
|
||
const inner = self.mangleTypeName(a.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "AR_{d}_{s}", .{ a.length, inner }) catch "array";
|
||
},
|
||
.signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed",
|
||
.unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned",
|
||
.optional => |o| blk: {
|
||
const inner = self.mangleTypeName(o.child);
|
||
break :blk std.fmt.allocPrint(self.alloc, "opt_{s}", .{inner}) catch "optional";
|
||
},
|
||
.vector => |v| blk: {
|
||
const inner = self.mangleTypeName(v.element);
|
||
break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
|
||
},
|
||
.closure => |c| self.mangleParamList("cl", c.params, c.ret),
|
||
.function => |f| self.mangleParamList("fn", f.params, f.ret),
|
||
.tuple => |t| blk: {
|
||
var buf = std.ArrayList(u8).empty;
|
||
buf.appendSlice(self.alloc, "tu") catch break :blk "tuple";
|
||
for (t.fields) |fid| {
|
||
buf.append(self.alloc, '_') catch break :blk "tuple";
|
||
buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple";
|
||
}
|
||
break :blk buf.items;
|
||
},
|
||
else => @tagName(info),
|
||
};
|
||
}
|
||
|
||
/// Collect impl entries visible from `current_source_file` — defined in
|
||
/// the current file or in any module the current file transitively
|
||
/// imports. Falls open (returns all entries) when the source-file
|
||
/// context or import graph isn't wired (e.g. comptime callers).
|
||
fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void {
|
||
const here = self.current_source_file orelse {
|
||
out.appendSlice(self.alloc, entries) catch {};
|
||
return;
|
||
};
|
||
const graph = self.import_graph orelse {
|
||
out.appendSlice(self.alloc, entries) catch {};
|
||
return;
|
||
};
|
||
|
||
// BFS over the import graph to compute the visible set.
|
||
var visible = std.StringHashMap(void).init(self.alloc);
|
||
defer visible.deinit();
|
||
visible.put(here, {}) catch {};
|
||
var queue = std.ArrayList([]const u8).empty;
|
||
defer queue.deinit(self.alloc);
|
||
queue.append(self.alloc, here) catch {};
|
||
var head: usize = 0;
|
||
while (head < queue.items.len) : (head += 1) {
|
||
const node = queue.items[head];
|
||
const direct = graph.get(node) orelse continue;
|
||
var it = direct.iterator();
|
||
while (it.next()) |kv| {
|
||
const next = kv.key_ptr.*;
|
||
if (visible.contains(next)) continue;
|
||
visible.put(next, {}) catch {};
|
||
queue.append(self.alloc, next) catch {};
|
||
}
|
||
}
|
||
|
||
for (entries) |e| {
|
||
if (visible.contains(e.defining_module)) {
|
||
out.append(self.alloc, e) catch {};
|
||
}
|
||
}
|
||
}
|
||
|
||
fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 {
|
||
var buf = std.ArrayList(u8).empty;
|
||
buf.appendSlice(self.alloc, prefix) catch return prefix;
|
||
for (params) |p| {
|
||
buf.append(self.alloc, '_') catch return prefix;
|
||
buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix;
|
||
}
|
||
buf.appendSlice(self.alloc, "__") catch return prefix;
|
||
buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix;
|
||
return buf.items;
|
||
}
|
||
|
||
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
|
||
/// Returns a list of TypeId index values that match the category.
|
||
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
|
||
var tags = std.ArrayList(u64).empty;
|
||
|
||
// Fixed builtin categories
|
||
if (std.mem.eql(u8, name, "int")) {
|
||
tags.append(self.alloc, TypeId.s8.index()) catch {};
|
||
tags.append(self.alloc, TypeId.s16.index()) catch {};
|
||
tags.append(self.alloc, TypeId.s32.index()) catch {};
|
||
tags.append(self.alloc, TypeId.s64.index()) catch {};
|
||
tags.append(self.alloc, TypeId.u8.index()) catch {};
|
||
tags.append(self.alloc, TypeId.u16.index()) catch {};
|
||
tags.append(self.alloc, TypeId.u32.index()) catch {};
|
||
tags.append(self.alloc, TypeId.u64.index()) catch {};
|
||
tags.append(self.alloc, TypeId.usize.index()) catch {};
|
||
tags.append(self.alloc, TypeId.isize.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
if (std.mem.eql(u8, name, "float")) {
|
||
tags.append(self.alloc, TypeId.f32.index()) catch {};
|
||
tags.append(self.alloc, TypeId.f64.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
if (std.mem.eql(u8, name, "bool")) {
|
||
tags.append(self.alloc, TypeId.bool.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
if (std.mem.eql(u8, name, "string")) {
|
||
tags.append(self.alloc, TypeId.string.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
if (std.mem.eql(u8, name, "void")) {
|
||
tags.append(self.alloc, TypeId.void.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) {
|
||
tags.append(self.alloc, TypeId.any.index()) catch {};
|
||
return tags.items;
|
||
}
|
||
|
||
// Dynamic categories: scan TypeTable for matching types
|
||
const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector };
|
||
const cat: ?Category = if (std.mem.eql(u8, name, "struct"))
|
||
.@"struct"
|
||
else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union"))
|
||
.@"enum"
|
||
else if (std.mem.eql(u8, name, "slice"))
|
||
.slice
|
||
else if (std.mem.eql(u8, name, "array"))
|
||
.array
|
||
else if (std.mem.eql(u8, name, "pointer"))
|
||
.pointer
|
||
else if (std.mem.eql(u8, name, "vector"))
|
||
.vector
|
||
else
|
||
null;
|
||
|
||
if (cat) |c| {
|
||
for (self.module.types.infos.items, 0..) |info, idx| {
|
||
const matches = switch (c) {
|
||
.@"struct" => info == .@"struct",
|
||
.@"enum" => info == .@"enum" or info == .tagged_union,
|
||
.@"union" => info == .@"union" or info == .tagged_union,
|
||
.slice => info == .slice,
|
||
.array => info == .array,
|
||
.pointer => info == .pointer or info == .many_pointer,
|
||
.vector => info == .vector,
|
||
};
|
||
if (matches) {
|
||
tags.append(self.alloc, @intCast(idx)) catch {};
|
||
}
|
||
}
|
||
}
|
||
|
||
// Specific type name (e.g., Point, Color) — look up in type registry
|
||
if (tags.items.len == 0) {
|
||
const name_id = self.module.types.internString(name);
|
||
if (self.module.types.findByName(name_id)) |tid| {
|
||
tags.append(self.alloc, tid.index()) catch {};
|
||
}
|
||
}
|
||
|
||
return tags.items;
|
||
}
|
||
|
||
/// Check if a match expression is a type-category match (patterns are type/category names).
|
||
fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId {
|
||
// Infer result type from the first non-null arm body.
|
||
// If we skip null_literal arms and find a concrete type T, and there
|
||
// were null arms, the result is ?T (optional).
|
||
var has_null = false;
|
||
for (me.arms) |arm| {
|
||
const last_node = if (arm.body.data == .block) blk: {
|
||
if (arm.body.data.block.stmts.len > 0) {
|
||
break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1];
|
||
}
|
||
break :blk arm.body;
|
||
} else arm.body;
|
||
|
||
if (last_node.data == .null_literal) {
|
||
has_null = true;
|
||
continue;
|
||
}
|
||
|
||
// First non-null arm determines the type (same as old behavior)
|
||
const arm_ty = self.inferExprType(last_node);
|
||
if (has_null and arm_ty != .void) {
|
||
return self.module.types.optionalOf(arm_ty);
|
||
}
|
||
return arm_ty;
|
||
}
|
||
return .void;
|
||
}
|
||
|
||
fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool {
|
||
for (me.arms) |arm| {
|
||
if (arm.pattern) |pat| {
|
||
const name = switch (pat.data) {
|
||
.identifier => |id| id.name,
|
||
.type_expr => |te| te.name,
|
||
else => continue,
|
||
};
|
||
const categories = [_][]const u8{
|
||
"int", "float", "bool", "string", "void", "type", "Type",
|
||
"struct", "enum", "union", "slice", "array", "pointer", "vector",
|
||
};
|
||
for (categories) |cat| {
|
||
if (std.mem.eql(u8, name, cat)) return true;
|
||
}
|
||
// Also match specific struct/enum type names (e.g., case Point:)
|
||
if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Resolve parameter types for a call expression (for target_type context).
|
||
/// Returns empty slice if the function can't be resolved.
|
||
fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call) []const TypeId {
|
||
// Method calls: obj.method(args) — resolve param types from the method signature,
|
||
// skipping the first param (self) since it's prepended later.
|
||
if (c.callee.data == .field_access) {
|
||
const fa = c.callee.data.field_access;
|
||
const obj_ty = self.inferExprType(fa.object);
|
||
// Protocol-typed receiver: look up the method on the protocol decl. The
|
||
// protocol's ProtocolMethodInfo.param_types already excludes self.
|
||
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
||
for (proto_info.methods) |m| {
|
||
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
|
||
}
|
||
}
|
||
// Optional-protocol receiver (`?GPU`): same as above but the
|
||
// protocol type sits inside the optional's payload.
|
||
if (!obj_ty.isBuiltin()) {
|
||
const opt_info = self.module.types.get(obj_ty);
|
||
if (opt_info == .optional) {
|
||
if (self.getProtocolInfo(opt_info.optional.child)) |proto_info| {
|
||
for (proto_info.methods) |m| {
|
||
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Closure-typed struct field: `c.on(args)` lowers to call_closure on
|
||
// the field value. Pick up the callee's param types from the closure
|
||
// type so each arg gets the right target_type during lowering.
|
||
if (!obj_ty.isBuiltin()) {
|
||
const field_name_id = self.module.types.internString(fa.field);
|
||
const struct_fields = self.getStructFields(obj_ty);
|
||
for (struct_fields) |f| {
|
||
if (f.name == field_name_id and !f.ty.isBuiltin()) {
|
||
const fti = self.module.types.get(f.ty);
|
||
if (fti == .closure) return fti.closure.params;
|
||
if (fti == .function) return fti.function.params;
|
||
}
|
||
}
|
||
}
|
||
if (self.getStructTypeName(obj_ty)) |sname| {
|
||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{};
|
||
// Try already-lowered functions first
|
||
if (self.resolveFuncByName(qualified)) |fid| {
|
||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||
if (func.params.len > 0) {
|
||
// Skip self param — caller args don't include self
|
||
var types_list = std.ArrayList(TypeId).empty;
|
||
for (func.params[1..]) |p| {
|
||
types_list.append(self.alloc, p.ty) catch unreachable;
|
||
}
|
||
return types_list.items;
|
||
}
|
||
}
|
||
// Try AST map (not yet lowered)
|
||
if (self.fn_ast_map.get(qualified)) |fd| {
|
||
if (fd.params.len > 0) {
|
||
var types_list = std.ArrayList(TypeId).empty;
|
||
for (fd.params[1..]) |p| {
|
||
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
|
||
}
|
||
return types_list.items;
|
||
}
|
||
}
|
||
// Try generic struct template method: List__Container.append → List.append
|
||
// with type bindings from the struct instantiation
|
||
if (self.struct_instance_template.get(sname)) |tmpl_name| {
|
||
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{};
|
||
if (self.fn_ast_map.get(tmpl_qualified)) |fd| {
|
||
if (fd.params.len > 0) {
|
||
// Temporarily set type_bindings so resolveParamType can substitute T → concrete type
|
||
const saved_bindings = self.type_bindings;
|
||
if (self.struct_instance_bindings.getPtr(sname)) |bindings| {
|
||
self.type_bindings = bindings.*;
|
||
}
|
||
var types_list = std.ArrayList(TypeId).empty;
|
||
for (fd.params[1..]) |p| {
|
||
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
|
||
}
|
||
self.type_bindings = saved_bindings;
|
||
return types_list.items;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return &.{};
|
||
}
|
||
if (c.callee.data != .identifier) return &.{};
|
||
const bare_name = c.callee.data.identifier.name;
|
||
const name = blk: {
|
||
const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
||
if (self.ufcs_alias_map.get(bare_name)) |target| {
|
||
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
||
}
|
||
break :blk scoped;
|
||
};
|
||
|
||
// Check declared functions
|
||
if (self.resolveFuncByName(name)) |fid| {
|
||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||
// Return param types (allocated as slice of TypeId)
|
||
var types_list = std.ArrayList(TypeId).empty;
|
||
for (func.params) |p| {
|
||
types_list.append(self.alloc, p.ty) catch unreachable;
|
||
}
|
||
return types_list.items;
|
||
}
|
||
|
||
// Check AST map for function signatures
|
||
if (self.fn_ast_map.get(name)) |fd| {
|
||
var types_list = std.ArrayList(TypeId).empty;
|
||
for (fd.params) |p| {
|
||
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
|
||
}
|
||
return types_list.items;
|
||
}
|
||
|
||
// Check global function pointer variables
|
||
if (self.global_names.get(bare_name)) |gi| {
|
||
if (!gi.ty.isBuiltin()) {
|
||
const ti = self.module.types.get(gi.ty);
|
||
if (ti == .function) {
|
||
return ti.function.params;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check local scope for function pointer variables
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(bare_name)) |binding| {
|
||
if (!binding.ty.isBuiltin()) {
|
||
const ti = self.module.types.get(binding.ty);
|
||
if (ti == .function) {
|
||
return ti.function.params;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return &.{};
|
||
}
|
||
|
||
/// Check if a param is a type param declaration ($T: Type).
|
||
/// A type param declaration has param.name == one of the type_params names.
|
||
fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool {
|
||
for (type_params) |tp| {
|
||
if (std.mem.eql(u8, param.name, tp.name)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Check if a function has comptime (non-Type) value parameters.
|
||
fn hasComptimeParams(fd: *const ast.FnDecl) bool {
|
||
for (fd.params) |p| {
|
||
if (p.is_comptime) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Creates a temporary function marked `is_comptime = true` that wraps
|
||
/// the given expression as its return value. Returns the FuncId.
|
||
pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId {
|
||
var buf: [64]u8 = undefined;
|
||
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
|
||
self.comptime_counter += 1;
|
||
|
||
// Save current builder state
|
||
const saved_func = self.builder.func;
|
||
const saved_block = self.builder.current_block;
|
||
const saved_counter = self.builder.inst_counter;
|
||
const saved_scope = self.scope;
|
||
|
||
// Create the comptime function (no params, returns ret_ty)
|
||
const name_id = self.module.types.internString(name);
|
||
const func_id = self.builder.beginFunction(name_id, &.{}, ret_ty);
|
||
|
||
// Mark as comptime
|
||
self.module.getFunctionMut(func_id).is_comptime = true;
|
||
|
||
// Create entry block
|
||
const entry_name = self.module.types.internString("entry");
|
||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||
self.builder.switchToBlock(entry);
|
||
|
||
// Create a scope that chains to the enclosing scope (so the
|
||
// expression can reference names visible at the #run site).
|
||
var ct_scope = Scope.init(self.alloc, saved_scope);
|
||
self.scope = &ct_scope;
|
||
|
||
// Lower the expression and return it
|
||
const result = self.lowerExpr(expr);
|
||
if (ret_ty == .void) {
|
||
self.builder.retVoid();
|
||
} else {
|
||
self.builder.ret(result, ret_ty);
|
||
}
|
||
|
||
self.builder.finalize();
|
||
|
||
// Restore builder state
|
||
self.scope = saved_scope;
|
||
ct_scope.deinit();
|
||
self.builder.func = saved_func;
|
||
self.builder.current_block = saved_block;
|
||
self.builder.inst_counter = saved_counter;
|
||
|
||
return func_id;
|
||
}
|
||
|
||
// ── Block helpers ───────────────────────────────────────────────
|
||
|
||
fn freshBlock(self: *Lowering, prefix: []const u8) BlockId {
|
||
return self.freshBlockWithParams(prefix, &.{});
|
||
}
|
||
|
||
fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId {
|
||
var buf: [64]u8 = undefined;
|
||
const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix;
|
||
self.block_counter += 1;
|
||
const name_id = self.module.types.internString(name);
|
||
return self.builder.appendBlock(name_id, params);
|
||
}
|
||
|
||
fn currentBlockHasTerminator(self: *Lowering) bool {
|
||
const func = self.builder.module.getFunctionMut(self.builder.func.?);
|
||
const block_idx = self.builder.current_block orelse return true;
|
||
const block = &func.blocks.items[block_idx.index()];
|
||
if (block.insts.items.len > 0) {
|
||
const last_op = block.insts.items[block.insts.items.len - 1].op;
|
||
return switch (last_op) {
|
||
.ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true,
|
||
else => false,
|
||
};
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ── Type resolution ─────────────────────────────────────────────
|
||
// Delegates to type_bridge for full AST type node resolution.
|
||
|
||
fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId {
|
||
if (fd.return_type) |rt| {
|
||
return self.resolveTypeWithBindings(rt);
|
||
}
|
||
// Arrow functions without explicit return type: infer from body expression
|
||
if (fd.is_arrow) {
|
||
return self.inferExprType(fd.body);
|
||
}
|
||
// No annotation, not arrow: an explicit `return <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);
|
||
}
|
||
}
|
||
};
|