Files
sx/src/ir/emit_llvm.zig
agra c32d694d57 ERR/E3.0 (slice 2): emit DWARF line-info
Attach LLVM debug metadata so a captured return-address PC resolves to
file:line:col (the runtime half E3.3 needs) and sx binaries become
debuggable in lldb/gdb.

- llvm_api.zig: bind llvm-c/DebugInfo.h (DIBuilder C API was unbound).
- emit_llvm.zig: DIBuilder + one DICompileUnit/DIFile on the main file,
  a DISubprogram per function (LLVMSetSubprogram), and a DILocation per
  instruction from Inst.span (errors.SourceLoc.compute, scoped to the
  subprogram). Plus the "Debug Info Version"/"Dwarf Version" module
  flags and LLVMDIBuilderFinalize.
- Gated on opt none/less + a wired source map (setDebugContext from
  core.zig), mirroring lower.zig's tracesEnabled; release strips it.

Verified: sx ir/sx asm --opt none show correct DILocations + .loc
directives; the 290-example JIT suite (-O0 -> debug on) verifies and
runs unchanged. +2 DWARF unit tests.
2026-06-01 13:14:00 +03:00

5262 lines
276 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const llvm = @import("../llvm_api.zig");
const c = llvm.c;
const target_mod = @import("../target.zig");
const TargetConfig = target_mod.TargetConfig;
const ir_types = @import("types.zig");
const TypeId = ir_types.TypeId;
const TypeInfo = ir_types.TypeInfo;
const TypeTable = ir_types.TypeTable;
const StringId = ir_types.StringId;
const errors = @import("../errors.zig");
const ir_inst = @import("inst.zig");
const Ref = ir_inst.Ref;
const Span = ir_inst.Span;
const BlockId = ir_inst.BlockId;
const FuncId = ir_inst.FuncId;
const GlobalId = ir_inst.GlobalId;
const Inst = ir_inst.Inst;
const Op = ir_inst.Op;
const Block = ir_inst.Block;
const Function = ir_inst.Function;
const Global = ir_inst.Global;
const ir_module = @import("module.zig");
const Module = ir_module.Module;
const interp_mod = @import("interp.zig");
const Interpreter = interp_mod.Interpreter;
const Value = interp_mod.Value;
fn isIdentByte(b: u8) bool {
return (b >= 'a' and b <= 'z') or (b >= 'A' and b <= 'Z') or (b >= '0' and b <= '9') or b == '_';
}
/// JNI vtable slot offsets — indices into the `JNINativeInterface`
/// function-pointer array reachable via `*env`. Stable per the JNI
/// spec across versions; locked to the documented order in
/// `<jni.h>`. Slot numbers here MUST match the order of fields in
/// the C `JNINativeInterface_` struct.
const Jni = struct {
const FindClass: u32 = 6;
const NewGlobalRef: u32 = 21;
const NewObject: u32 = 28;
const GetObjectClass: u32 = 31;
const GetMethodID: u32 = 33;
// Call<Type>Method (instance, varargs variant). Each numeric type
// has its own slot — distinct ABI per return type, so the JNI
// runtime dispatches the right arg-shuffle for each.
const CallObjectMethod: u32 = 34;
const CallBooleanMethod: u32 = 37;
const CallIntMethod: u32 = 49;
const CallLongMethod: u32 = 52;
const CallFloatMethod: u32 = 55;
const CallDoubleMethod: u32 = 58;
const CallVoidMethod: u32 = 61;
// CallNonvirtual<T>Method (instance, super-dispatch variant). Used by
// `super.method(args)` from inside a `#jni_main` Activity method body:
// dispatch is bound to a specific class rather than going through the
// vtable, so subclass overrides don't intercept the call. Signature:
// `(JNIEnv*, jobject obj, jclass clazz, jmethodID, args...)`.
const CallNonvirtualObjectMethod: u32 = 64;
const CallNonvirtualBooleanMethod: u32 = 67;
const CallNonvirtualIntMethod: u32 = 79;
const CallNonvirtualLongMethod: u32 = 82;
const CallNonvirtualFloatMethod: u32 = 85;
const CallNonvirtualDoubleMethod: u32 = 88;
const CallNonvirtualVoidMethod: u32 = 91;
// Static-dispatch siblings — `target` IS already a `jclass`, so
// no `GetObjectClass` step. `GetStaticMethodID` returns a
// method-ID that's bound to a class+method+sig like the instance
// variant; `CallStatic<Type>Method` dispatches without a `this`.
const GetStaticMethodID: u32 = 113;
const CallStaticObjectMethod: u32 = 114;
const CallStaticBooleanMethod: u32 = 117;
const CallStaticIntMethod: u32 = 129;
const CallStaticLongMethod: u32 = 132;
const CallStaticFloatMethod: u32 = 135;
const CallStaticDoubleMethod: u32 = 138;
const CallStaticVoidMethod: u32 = 141;
};
// ── LLVMEmitter ─────────────────────────────────────────────────────────
// Emits LLVM IR from an IR Module. This is the Phase 3 replacement for
// the AST-based codegen.
pub const LLVMEmitter = struct {
// LLVM handles
context: c.LLVMContextRef,
llvm_module: c.LLVMModuleRef,
builder: c.LLVMBuilderRef,
target_machine: ?c.LLVMTargetMachineRef,
// IR Module being emitted
ir_mod: *const Module,
// Allocator for temporary bookkeeping
alloc: Allocator,
// Maps IR Ref → LLVM Value for the current function
ref_map: std.AutoHashMap(u32, c.LLVMValueRef),
// Maps IR FuncId → LLVM function value
func_map: std.AutoHashMap(u32, c.LLVMValueRef),
// Maps IR GlobalId → LLVM global value
global_map: std.AutoHashMap(u32, c.LLVMValueRef),
// Maps (func_idx, block_idx) → LLVM BasicBlock
block_map: std.AutoHashMap(u64, c.LLVMBasicBlockRef),
// Cached LLVM types
cached_i1: c.LLVMTypeRef,
cached_i8: c.LLVMTypeRef,
cached_i16: c.LLVMTypeRef,
cached_i32: c.LLVMTypeRef,
cached_i64: c.LLVMTypeRef,
cached_f32: c.LLVMTypeRef,
cached_f64: c.LLVMTypeRef,
cached_ptr: c.LLVMTypeRef,
cached_void: c.LLVMTypeRef,
// Current ref counter — tracks which Ref index we're emitting within a function
ref_counter: u32 = 0,
// Pending PHI nodes to fixup after all blocks in a function are emitted
pending_phis: std.ArrayList(PendingPhi),
// Whether the current function being emitted is "main" (needs i32 return for JIT)
current_func_is_main: bool = false,
current_func_idx: u32 = 0,
// Cached composite types
string_struct_type: ?c.LLVMTypeRef,
any_struct_type: ?c.LLVMTypeRef,
closure_struct_type: ?c.LLVMTypeRef,
// The shared `@objc_msgSend` function value. Lazily declared on
// first `objc_msg_send` instruction; all `#objc_call` sites
// dispatch through it with their own LLVMBuildCall2 function type
// (opaque pointers — the function value is just a `ptr`).
objc_msg_send_value: ?c.LLVMValueRef,
// `(name, sig)` → `{cls_slot, mid_slot}` cache for `#jni_call`
// interning (step 1.17). Two call sites with the same literal
// name + signature share one pair of static slots, populated
// lazily on the first call.
jni_slots: std.StringHashMap(JniSlotPair),
// Cached field name arrays for reflection (TypeId → LLVM global)
field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef),
// The always-linked tag-name table (global tag id → name); built once.
tag_name_array: ?c.LLVMValueRef = null,
// Lazy global `[N x string]` indexed by TypeId.index(), holding
// each type's display name. Built on the first dynamic
// `type_name(t)` call site; reused thereafter.
type_name_array: ?c.LLVMValueRef = null,
type_name_array_len: u32 = 0,
// Target configuration (stored for ABI decisions during emission)
target_config: TargetConfig,
// Build configuration accumulated from #run blocks
build_config: interp_mod.BuildConfig,
// ── DWARF debug info (ERR E3.0) ──────────────────────────────────
// Emitted only when the build keeps error traces (opt_level
// none/less, matching lower.zig's `tracesEnabled`) AND a source map
// is wired in via `setDebugContext`. One `DICompileUnit` (on the
// main file) + a `DIFile` per source file + a `DISubprogram` per
// emitted function + a `DILocation` per instruction (resolved from
// `Inst.span`). Lets a captured return-address PC resolve to
// file:line:col for E3.3's runtime trace formatting, and makes sx
// binaries debuggable in lldb/gdb as a bonus.
di_builder: c.LLVMDIBuilderRef = null,
di_cu: c.LLVMMetadataRef = null,
di_files: std.StringHashMap(c.LLVMMetadataRef),
// The current function's DISubprogram — the scope for its
// DILocations. Null between functions (and in functions we don't
// describe, e.g. the synthetic Obj-C init constructors).
di_scope: c.LLVMMetadataRef = null,
// Source file of the function currently being emitted (span → line).
current_func_file: []const u8 = "",
// File path → source text (the diagnostics' `import_sources` map).
// Null in unit tests, so no debug info is emitted there.
import_sources: ?*const std.StringHashMap([:0]const u8) = null,
// Main file path — the compile unit's file and the span-resolution
// fallback for functions with no recorded source file.
main_file: []const u8 = "",
const PendingPhi = struct {
phi: c.LLVMValueRef,
block_id: BlockId, // the block this phi belongs to
param_index: u32,
};
const JniSlotPair = struct {
cls_slot: c.LLVMValueRef, // @SX_JNI_CLS_<key>: ptr (GlobalRef to jclass)
mid_slot: c.LLVMValueRef, // @SX_JNI_MID_<key>: ptr (jmethodID)
};
pub fn init(alloc: Allocator, ir_mod: *const Module, module_name: [*:0]const u8, target_config: TargetConfig) LLVMEmitter {
// Initialize LLVM targets
if (target_config.triple == null) {
llvm.initNativeTarget();
} else {
llvm.initAllTargets();
}
const ctx = c.LLVMContextCreate();
const llvm_module = c.LLVMModuleCreateWithNameInContext(module_name, ctx);
const builder = c.LLVMCreateBuilderInContext(ctx);
// Set target triple
const triple_owned = target_config.triple == null;
const triple = target_config.triple orelse c.LLVMGetDefaultTargetTriple();
defer if (triple_owned) c.LLVMDisposeMessage(@constCast(triple));
c.LLVMSetTarget(llvm_module, triple);
// Create target machine and set data layout
var target: c.LLVMTargetRef = null;
var err_msg: [*c]u8 = null;
var tm: c.LLVMTargetMachineRef = null;
if (c.LLVMGetTargetFromTriple(triple, &target, &err_msg) == 0) {
tm = c.LLVMCreateTargetMachine(
target,
triple,
target_config.getCpu(),
target_config.getFeatures(),
target_config.opt_level.toLLVM(),
c.LLVMRelocPIC,
c.LLVMCodeModelDefault,
);
const dl = c.LLVMCreateTargetDataLayout(tm);
c.LLVMSetModuleDataLayout(llvm_module, dl);
c.LLVMDisposeTargetData(dl);
} else {
if (err_msg != null) c.LLVMDisposeMessage(err_msg);
}
return .{
.context = ctx,
.llvm_module = llvm_module,
.builder = builder,
.target_machine = tm,
.ir_mod = ir_mod,
.alloc = alloc,
.ref_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.func_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.global_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.block_map = std.AutoHashMap(u64, c.LLVMBasicBlockRef).init(alloc),
.pending_phis = std.ArrayList(PendingPhi).empty,
.cached_i1 = c.LLVMInt1TypeInContext(ctx),
.cached_i8 = c.LLVMInt8TypeInContext(ctx),
.cached_i16 = c.LLVMInt16TypeInContext(ctx),
.cached_i32 = c.LLVMInt32TypeInContext(ctx),
.cached_i64 = c.LLVMInt64TypeInContext(ctx),
.cached_f32 = c.LLVMFloatTypeInContext(ctx),
.cached_f64 = c.LLVMDoubleTypeInContext(ctx),
.cached_ptr = c.LLVMPointerTypeInContext(ctx, 0),
.cached_void = c.LLVMVoidTypeInContext(ctx),
.string_struct_type = null,
.any_struct_type = null,
.closure_struct_type = null,
.objc_msg_send_value = null,
.jni_slots = std.StringHashMap(JniSlotPair).init(alloc),
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.target_config = target_config,
.build_config = .{},
.di_files = std.StringHashMap(c.LLVMMetadataRef).init(alloc),
};
}
pub fn deinit(self: *LLVMEmitter) void {
self.build_config.deinit(self.alloc);
self.ref_map.deinit();
self.func_map.deinit();
self.field_name_arrays.deinit();
var jni_it = self.jni_slots.keyIterator();
while (jni_it.next()) |k| self.alloc.free(k.*);
self.jni_slots.deinit();
self.global_map.deinit();
self.block_map.deinit();
self.di_files.deinit();
if (self.di_builder != null) c.LLVMDisposeDIBuilder(self.di_builder);
if (self.target_machine) |tm| c.LLVMDisposeTargetMachine(tm);
c.LLVMDisposeBuilder(self.builder);
c.LLVMDisposeModule(self.llvm_module);
c.LLVMContextDispose(self.context);
}
// ── Top-level emit ──────────────────────────────────────────────
pub fn emit(self: *LLVMEmitter) void {
// Pass -1: Set up DWARF debug info (compile unit + module flags).
// Must precede any DISubprogram (created per function below).
self.initDebugInfo();
// Pass 0: Declare and initialize globals
self.emitGlobals();
// Pass 0.5: Run comptime side-effect functions (#run expr; at top level)
self.runComptimeSideEffects();
// Pass 1: Declare all functions (so calls can reference them)
for (self.ir_mod.functions.items, 0..) |func, i| {
self.declareFunction(&func, @intCast(i));
}
// Pass 1.5: Initialize vtable globals (needs function declarations from Pass 1)
self.initVtableGlobals();
// Pass 2: Emit function bodies
for (self.ir_mod.functions.items, 0..) |func, i| {
if (func.is_extern or func.blocks.items.len == 0) continue;
self.emitFunction(&func, @intCast(i));
}
// Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5).
self.emitObjcSelectorInit();
// Pass 2.5b: Emit Obj-C class-pair registration constructor for
// sx-defined classes (M1.2 A.4+). Runs BEFORE the foreign
// class-cache populator (2.5c) so a sx-defined class is already
// registered with the Obj-C runtime by the time
// `objc_getClass(\"SxFoo\")` runs to populate the Phase 3.1
// class-object cache — otherwise the cache slot would store
// null and `SxFoo.method()` dispatches against null.
self.emitObjcDefinedClassInit();
// Pass 2.5c: Emit Obj-C class-object init constructor (Phase 3.1).
// Same shape as the selector init — populates the per-module
// cached `Class*` slots via `objc_getClass` at module-init time.
self.emitObjcClassInit();
// Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at
// startup so relative asset paths work when Finder/`open`
// launches the binary with CWD=/. Non-bundled binaries no-op.
self.emitMacosBundleChdir();
// Pass 3: Verify typeSizeBytes matches LLVM's ABI sizes
self.verifySizes();
// Pass 4: Resolve DWARF temporary metadata. Must come after all
// DISubprograms / DILocations are created and before the module
// is verified or emitted.
self.finalizeDebugInfo();
}
// ── DWARF debug info (ERR E3.0) ──────────────────────────────────
/// Wire the source map + main file so spans can resolve to
/// file:line:col. Called by the driver after `init`; absent in unit
/// tests, which keeps debug-info emission off there.
pub fn setDebugContext(self: *LLVMEmitter, import_sources: *const std.StringHashMap([:0]const u8), main_file: []const u8) void {
self.import_sources = import_sources;
self.main_file = main_file;
}
/// Debug info is emitted only when error traces are kept (opt_level
/// none/less, matching `tracesEnabled` in lower.zig) and a source
/// map is available. Release builds (default/aggressive) skip it, so
/// the DWARF is strippable cost-free.
fn debugEnabled(self: *const LLVMEmitter) bool {
if (self.import_sources == null) return false;
return self.target_config.opt_level == .none or self.target_config.opt_level == .less;
}
/// Source text for `file` via the diagnostics' file→source map (the
/// same map `#caller_location` uses). Empty when unavailable —
/// line:col then degrades to 1:1 rather than crash.
fn sourceForFile(self: *LLVMEmitter, file: []const u8) []const u8 {
const is = self.import_sources orelse return "";
if (is.get(file)) |s| return s;
if (self.main_file.len > 0) {
if (is.get(self.main_file)) |s| return s;
}
return "";
}
/// The `DIFile` for `path`, created once and cached. Splits the path
/// into basename + directory as DWARF expects.
fn diFileFor(self: *LLVMEmitter, path: []const u8) c.LLVMMetadataRef {
if (self.di_files.get(path)) |f| return f;
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
const dir = if (slash) |s| path[0..s] else "";
const base = if (slash) |s| path[s + 1 ..] else path;
const f = c.LLVMDIBuilderCreateFile(self.di_builder, base.ptr, base.len, dir.ptr, dir.len);
self.di_files.put(path, f) catch {};
return f;
}
/// Create the DIBuilder, the module flags ("Debug Info Version" /
/// "Dwarf Version"), and the single compile unit on the main file.
fn initDebugInfo(self: *LLVMEmitter) void {
if (!self.debugEnabled()) return;
self.di_builder = c.LLVMCreateDIBuilder(self.llvm_module);
c.LLVMAddModuleFlag(
self.llvm_module,
c.LLVMModuleFlagBehaviorWarning,
"Debug Info Version",
"Debug Info Version".len,
c.LLVMValueAsMetadata(c.LLVMConstInt(self.cached_i32, c.LLVMDebugMetadataVersion(), 0)),
);
c.LLVMAddModuleFlag(
self.llvm_module,
c.LLVMModuleFlagBehaviorWarning,
"Dwarf Version",
"Dwarf Version".len,
c.LLVMValueAsMetadata(c.LLVMConstInt(self.cached_i32, 4, 0)),
);
const cu_file = self.diFileFor(if (self.main_file.len > 0) self.main_file else "sx");
self.di_cu = c.LLVMDIBuilderCreateCompileUnit(
self.di_builder,
c.LLVMDWARFSourceLanguageC,
cu_file,
"sx",
"sx".len,
0, // isOptimized
"",
0, // flags
0, // runtime version
"",
0, // split name
c.LLVMDWARFEmissionFull,
0, // DWOId
0, // split debug inlining
0, // debug info for profiling
"",
0, // sysroot
"",
0, // sdk
);
}
/// Create a `DISubprogram` for `func` and attach it to `llvm_func`,
/// making it the scope (`di_scope`) for the function's instruction
/// locations. Clears any stale builder location first so synthetic
/// functions emitted between sx functions carry none.
fn beginFunctionDebug(self: *LLVMEmitter, func: *const Function, llvm_func: c.LLVMValueRef, name: []const u8) void {
self.di_scope = null;
c.LLVMSetCurrentDebugLocation2(self.builder, null);
if (self.di_builder == null) return;
const file = func.source_file orelse self.main_file;
self.current_func_file = file;
const di_file = self.diFileFor(file);
const subroutine_ty = c.LLVMDIBuilderCreateSubroutineType(self.di_builder, di_file, null, 0, c.LLVMDIFlagZero);
// Line = the first instruction's line (the function body's start),
// else 1 when the body is empty / span-less.
var line: c_uint = 1;
if (func.blocks.items.len > 0 and func.blocks.items[0].insts.items.len > 0) {
const sp = func.blocks.items[0].insts.items[0].span;
const src = self.sourceForFile(file);
line = errors.SourceLoc.compute(src, sp.start).line;
}
const is_local: c.LLVMBool = if (func.linkage == .external) 0 else 1;
const subprogram = c.LLVMDIBuilderCreateFunction(
self.di_builder,
di_file, // scope
name.ptr,
name.len,
name.ptr,
name.len, // linkage name
di_file,
line,
subroutine_ty,
is_local,
1, // is definition
line, // scope line
c.LLVMDIFlagZero,
0, // isOptimized
);
c.LLVMSetSubprogram(llvm_func, subprogram);
self.di_scope = subprogram;
}
/// End the current function's debug scope and clear the builder's
/// location, so the next (possibly synthetic) function doesn't
/// inherit a DILocation pointing into this function's subprogram.
fn endFunctionDebug(self: *LLVMEmitter) void {
self.di_scope = null;
c.LLVMSetCurrentDebugLocation2(self.builder, null);
}
/// Set the builder's current debug location from an instruction span,
/// scoped to the current function's subprogram. No-op when debug info
/// is off (`di_scope == null`).
fn setInstDebugLocation(self: *LLVMEmitter, span: Span) void {
const scope = self.di_scope orelse return;
const src = self.sourceForFile(self.current_func_file);
const loc = errors.SourceLoc.compute(src, span.start);
const di_loc = c.LLVMDIBuilderCreateDebugLocation(self.context, loc.line, loc.col, scope, null);
c.LLVMSetCurrentDebugLocation2(self.builder, di_loc);
}
fn finalizeDebugInfo(self: *LLVMEmitter) void {
if (self.di_builder == null) return;
c.LLVMDIBuilderFinalize(self.di_builder);
}
/// Synthesize a module constructor that populates each interned
/// Obj-C selector slot via `sel_registerName`, once at module
/// load. Registered in `@llvm.global_ctors` so dyld / ld.so / the
/// LLVM ORC JIT all run it before main. Per `#objc_call` site
/// collapses to a single load from the slot.
///
/// We tried clang's section-based shape (`__DATA,__objc_selrefs` +
/// `externally_initialized` linkage, no constructor — dyld
/// resolves at load time) and it works for fully-linked binaries
/// via the system loader, BUT LLVM's ORC JIT (the engine behind
/// `sx run`) doesn't process Mach-O Obj-C metadata sections —
/// the slot stays at its initial value (the method-name string
/// pointer) and `objc_msgSend` dispatches with a bogus SEL.
/// `@llvm.global_ctors` is a portable choice that works both
/// in-JIT and as a linked binary, at the cost of a tiny
/// startup pass (one sel_registerName + store per unique
/// selector).
fn emitObjcSelectorInit(self: *LLVMEmitter) void {
if (self.ir_mod.objc_selector_cache.items.len == 0) return;
// Lazy-declare sel_registerName for the constructor body —
// lower.zig only declares it when a non-literal selector
// appears, which the constructor doesn't depend on.
const sel_reg_name = "sel_registerName";
const sel_reg_z = self.alloc.dupeZ(u8, sel_reg_name) catch unreachable;
defer self.alloc.free(sel_reg_z);
var sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr);
var sel_reg_ty: c.LLVMTypeRef = undefined;
if (sel_reg_fn == null) {
var params: [1]c.LLVMTypeRef = .{self.cached_ptr};
sel_reg_ty = c.LLVMFunctionType(self.cached_ptr, &params, 1, 0);
sel_reg_fn = c.LLVMAddFunction(self.llvm_module, sel_reg_z.ptr, sel_reg_ty);
c.LLVMSetLinkage(sel_reg_fn, c.LLVMExternalLinkage);
} else {
sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn);
}
// Constructor: void __sx_objc_selector_init().
var no_params: [0]c.LLVMTypeRef = .{};
const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0);
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_selector_init", ctor_ty);
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
c.LLVMPositionBuilderAtEnd(self.builder, entry);
for (self.ir_mod.objc_selector_cache.items) |entry_kv| {
const sel_str = entry_kv.sel;
const slot_gid = entry_kv.slot;
const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue;
// Method-name C-string — names match clang's convention
// so debuggers / nm / dyld see the same symbols, even
// though the surrounding section tagging isn't load-
// bearing in our JIT path.
const meth_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue;
defer self.alloc.free(meth_str_z);
@memcpy(meth_str_z[0..sel_str.len], sel_str);
const str_const = c.LLVMConstStringInContext(self.context, meth_str_z.ptr, @intCast(sel_str.len), 0);
const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_METH_VAR_NAME_");
c.LLVMSetInitializer(str_global, str_const);
c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage);
c.LLVMSetGlobalConstant(str_global, 1);
c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr);
var sel_args: [1]c.LLVMValueRef = .{str_global};
const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel");
_ = c.LLVMBuildStore(self.builder, sel_val, slot_global);
}
_ = c.LLVMBuildRetVoid(self.builder);
// Register the constructor in @llvm.global_ctors. dyld picks
// this up for a fully-linked binary at load time.
const i32_ty = self.cached_i32;
const ptr_ty = self.cached_ptr;
var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty };
const ctor_struct_ty = c.LLVMStructTypeInContext(self.context, &ctor_field_types, 3, 0);
var ctor_fields: [3]c.LLVMValueRef = .{
c.LLVMConstInt(i32_ty, 65535, 0),
ctor,
c.LLVMConstNull(ptr_ty),
};
const ctor_entry = c.LLVMConstNamedStruct(ctor_struct_ty, &ctor_fields, 3);
const ctors_arr_ty = c.LLVMArrayType2(ctor_struct_ty, 1);
var ctor_entries: [1]c.LLVMValueRef = .{ctor_entry};
const ctors_init = c.LLVMConstArray2(ctor_struct_ty, &ctor_entries, 1);
const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors");
c.LLVMSetInitializer(ctors_global, ctors_init);
c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage);
// BUT — LLVM's ORC JIT (the engine for `sx run`) doesn't
// automatically run `@llvm.global_ctors`. Inject a direct
// call from `main`'s entry block as well; idempotent under
// dyld (sel_registerName returns the same SEL on second call).
const main_z = "main";
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
if (main_fn != null) {
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
const first_inst = c.LLVMGetFirstInstruction(entry_bb);
if (first_inst != null) {
c.LLVMPositionBuilderBefore(self.builder, first_inst);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
}
/// Phase 3.1 companion to `emitObjcSelectorInit`. Walks
/// `module.objc_class_cache` and synthesizes a constructor that
/// populates each cached `Class*` slot via `objc_getClass(name)`
/// exactly once at module-init. Registered in `@llvm.global_ctors`
/// AND injected at the top of `main()` for the ORC JIT path.
fn emitObjcClassInit(self: *LLVMEmitter) void {
if (self.ir_mod.objc_class_cache.items.len == 0) return;
// Lazy-declare objc_getClass(name: *u8) -> *void.
const get_class_name = "objc_getClass";
const get_class_z = self.alloc.dupeZ(u8, get_class_name) catch unreachable;
defer self.alloc.free(get_class_z);
var get_class_fn = c.LLVMGetNamedFunction(self.llvm_module, get_class_z.ptr);
var get_class_ty: c.LLVMTypeRef = undefined;
if (get_class_fn == null) {
var params: [1]c.LLVMTypeRef = .{self.cached_ptr};
get_class_ty = c.LLVMFunctionType(self.cached_ptr, &params, 1, 0);
get_class_fn = c.LLVMAddFunction(self.llvm_module, get_class_z.ptr, get_class_ty);
c.LLVMSetLinkage(get_class_fn, c.LLVMExternalLinkage);
} else {
get_class_ty = c.LLVMGlobalGetValueType(get_class_fn);
}
// Constructor: void __sx_objc_class_init().
var no_params: [0]c.LLVMTypeRef = .{};
const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0);
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_class_init", ctor_ty);
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
c.LLVMPositionBuilderAtEnd(self.builder, entry);
for (self.ir_mod.objc_class_cache.items) |entry_kv| {
const class_name = entry_kv.name;
const slot_gid = entry_kv.slot;
const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue;
// Class-name C-string.
const name_z = self.alloc.allocSentinel(u8, class_name.len, 0) catch continue;
defer self.alloc.free(name_z);
@memcpy(name_z[0..class_name.len], class_name);
const str_const = c.LLVMConstStringInContext(self.context, name_z.ptr, @intCast(class_name.len), 0);
const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_CLASS_NAME_");
c.LLVMSetInitializer(str_global, str_const);
c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage);
c.LLVMSetGlobalConstant(str_global, 1);
c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr);
var call_args: [1]c.LLVMValueRef = .{str_global};
const class_val = c.LLVMBuildCall2(self.builder, get_class_ty, get_class_fn, &call_args, 1, "cls");
_ = c.LLVMBuildStore(self.builder, class_val, slot_global);
}
_ = c.LLVMBuildRetVoid(self.builder);
// Register in @llvm.global_ctors for AOT + inject into main for ORC JIT.
const i32_ty = self.cached_i32;
const ptr_ty = self.cached_ptr;
var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty };
const ctor_struct_ty = c.LLVMStructTypeInContext(self.context, &ctor_field_types, 3, 0);
var ctor_fields: [3]c.LLVMValueRef = .{
c.LLVMConstInt(i32_ty, 65535, 0),
ctor,
c.LLVMConstNull(ptr_ty),
};
const ctor_entry = c.LLVMConstNamedStruct(ctor_struct_ty, &ctor_fields, 3);
// Append-vs-replace the existing global_ctors. Selector init may
// have created `@llvm.global_ctors` already — extend its array
// rather than overwriting.
const existing_z = "llvm.global_ctors";
const existing = c.LLVMGetNamedGlobal(self.llvm_module, existing_z);
if (existing != null) {
const existing_init = c.LLVMGetInitializer(existing);
const existing_arr_ty = c.LLVMGlobalGetValueType(existing);
const old_count = c.LLVMGetArrayLength(existing_arr_ty);
const new_count: c_uint = old_count + 1;
var new_entries = std.ArrayList(c.LLVMValueRef).empty;
defer new_entries.deinit(self.alloc);
var i: c_uint = 0;
while (i < old_count) : (i += 1) {
new_entries.append(self.alloc, c.LLVMGetAggregateElement(existing_init, i)) catch unreachable;
}
new_entries.append(self.alloc, ctor_entry) catch unreachable;
const new_arr_ty = c.LLVMArrayType2(ctor_struct_ty, new_count);
const new_init = c.LLVMConstArray2(ctor_struct_ty, new_entries.items.ptr, new_count);
const new_global = c.LLVMAddGlobal(self.llvm_module, new_arr_ty, "llvm.global_ctors.new");
c.LLVMSetInitializer(new_global, new_init);
c.LLVMSetLinkage(new_global, c.LLVMAppendingLinkage);
c.LLVMSetValueName2(existing, "llvm.global_ctors.old", "llvm.global_ctors.old".len);
c.LLVMSetValueName2(new_global, "llvm.global_ctors", "llvm.global_ctors".len);
c.LLVMDeleteGlobal(existing);
} else {
const ctors_arr_ty = c.LLVMArrayType2(ctor_struct_ty, 1);
var ctor_entries: [1]c.LLVMValueRef = .{ctor_entry};
const ctors_init = c.LLVMConstArray2(ctor_struct_ty, &ctor_entries, 1);
const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors");
c.LLVMSetInitializer(ctors_global, ctors_init);
c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage);
}
// ORC JIT injection: same trick as emitObjcSelectorInit. Inject a
// direct call from main's entry so the JIT path populates the
// slots too. Must run AFTER the selector init's main injection
// (selectors are needed independently of class objects), so we
// place this call AFTER the first instruction (which is the
// selector-init call, if present) rather than at the very top.
const main_z = "main";
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
if (main_fn != null) {
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
// Walk past any existing init calls (selector init etc.) so
// class init runs after them. The order within main's prelude
// doesn't matter functionally (the two caches are independent),
// but stable ordering keeps IR snapshots deterministic.
var insert_before = c.LLVMGetFirstInstruction(entry_bb);
while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) {
if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break;
}
if (insert_before != null) {
c.LLVMPositionBuilderBefore(self.builder, insert_before);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
}
/// M1.2 A.4 — emit class-pair registration constructor for every
/// sx-defined `#objc_class` declaration. Same shape as the Phase
/// 3.1 `emitObjcClassInit` companion: a `@llvm.global_ctors`-
/// registered constructor that runs at module load AND gets
/// injected at the top of `main` for the ORC JIT path (which
/// doesn't honor `@llvm.global_ctors`).
///
/// For each entry in `objc_defined_class_cache`:
/// super_cls = objc_getClass("<ParentName>") // default NSObject
/// cls = objc_allocateClassPair(super_cls, "<ClassName>", 0)
/// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i
/// objc_registerClassPair(cls)
/// g_<ClassName>_state_ivar = class_getInstanceVariable(cls, "__sx_state")
///
/// Method IMPs (`class_addMethod`) and the `+alloc` / `-dealloc`
/// overrides come in A.4b.ii / A.5 / A.6.
fn emitObjcDefinedClassInit(self: *LLVMEmitter) void {
if (self.ir_mod.objc_defined_class_cache.items.len == 0) return;
const ptr_ty = self.cached_ptr;
const i32_ty = self.cached_i32;
const i64_ty = self.cached_i64;
const i8_ty = c.LLVMInt8TypeInContext(self.context);
// Lazy-declare the Obj-C runtime APIs the constructor calls.
// objc_getClass(name: *u8) -> *void.
const get_class_fn, const get_class_ty = self.lazyDeclareCRuntime("objc_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
// objc_allocateClassPair(super: *void, name: *u8, extra: usize) -> *void.
const alloc_pair_fn, const alloc_pair_ty = self.lazyDeclareCRuntime("objc_allocateClassPair", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, i64_ty }, ptr_ty, 0);
// class_addIvar(cls: *void, name: *u8, size: u64, log2align: u8, type: *u8) -> bool.
const add_ivar_fn, const add_ivar_ty = self.lazyDeclareCRuntime("class_addIvar", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, i64_ty, i8_ty, ptr_ty }, i8_ty, 0);
// sel_registerName(name: *u8) -> *void.
const sel_reg_fn, const sel_reg_ty = self.lazyDeclareCRuntime("sel_registerName", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
// class_addMethod(cls: *void, sel: *void, imp: *void, types: *u8) -> bool.
const add_method_fn, const add_method_ty = self.lazyDeclareCRuntime("class_addMethod", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, ptr_ty, ptr_ty }, i8_ty, 0);
// objc_registerClassPair(cls: *void) -> void.
const register_fn, const register_ty = self.lazyDeclareCRuntime("objc_registerClassPair", &[_]c.LLVMTypeRef{ptr_ty}, self.cached_void, 0);
// class_getInstanceVariable(cls: *void, name: *u8) -> *Ivar.
const get_iv_fn, const get_iv_ty = self.lazyDeclareCRuntime("class_getInstanceVariable", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, ptr_ty, 0);
// Constructor: void __sx_objc_defined_class_init().
var no_params: [0]c.LLVMTypeRef = .{};
const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0);
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_defined_class_init", ctor_ty);
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
c.LLVMPositionBuilderAtEnd(self.builder, entry);
// Reusable C-string globals for ivar metadata (same across classes).
const sx_state_name_global = self.emitPrivateCString("__sx_state", "OBJC_IVAR_NAME_");
const sx_state_enc_global = self.emitPrivateCString("^v", "OBJC_IVAR_TYPE_");
for (self.ir_mod.objc_defined_class_cache.items) |entry_kv| {
const fcd = entry_kv.decl;
const class_name = fcd.name;
// Parent class — pre-resolved Obj-C runtime name from
// lower.zig (M2.3 resolveObjcParentName). Stored on the
// cache entry so emit_llvm doesn't re-walk
// foreign_class_map here.
const parent_name = entry_kv.parent_objc_name;
const parent_str_global = self.emitPrivateCString(parent_name, "OBJC_CLASS_NAME_");
const class_str_global = self.emitPrivateCString(class_name, "OBJC_CLASS_NAME_");
// super_cls = objc_getClass("ParentName")
var get_args: [1]c.LLVMValueRef = .{parent_str_global};
const super_val = c.LLVMBuildCall2(self.builder, get_class_ty, get_class_fn, &get_args, 1, "super_cls");
// cls = objc_allocateClassPair(super_cls, "ClassName", 0)
var alloc_args: [3]c.LLVMValueRef = .{ super_val, class_str_global, c.LLVMConstInt(i64_ty, 0, 0) };
const cls_val = c.LLVMBuildCall2(self.builder, alloc_pair_ty, alloc_pair_fn, &alloc_args, 3, "cls");
// class_addIvar(cls, "__sx_state", 8, 3, "^v")
// size = 8 (pointer) — sizeof(*void) on 64-bit
// log2align = 3 — alignof(*void) = 8 = 2^3
// type = "^v" (encoded *void)
var ivar_args: [5]c.LLVMValueRef = .{
cls_val,
sx_state_name_global,
c.LLVMConstInt(i64_ty, 8, 0),
c.LLVMConstInt(i8_ty, 3, 0),
sx_state_enc_global,
};
_ = c.LLVMBuildCall2(self.builder, add_ivar_ty, add_ivar_fn, &ivar_args, 5, "");
// Class-method registration (M2.1(b)) and the +alloc IMP
// (M1.2 A.5) both target the metaclass. Compute it once
// up-front so all metaclass-bound class_addMethod calls
// can reference the same LLVM value.
//
// metaclass = object_getClass(cls). (object_getClass on a
// Class returns the metaclass — a Class IS an instance of
// its metaclass. Distinct from objc_getClass(name).)
const obj_get_class_fn, const obj_get_class_ty = self.lazyDeclareCRuntime("object_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
var ogc_args: [1]c.LLVMValueRef = .{cls_val};
const metaclass_val = c.LLVMBuildCall2(self.builder, obj_get_class_ty, obj_get_class_fn, &ogc_args, 1, "metacls");
// class_addMethod(target, sel_registerName(sel), imp, encoding)
// — register each method's IMP trampoline (M1.2 A.4b.iii
// + M2.1(b)). Instance methods register on `cls`; class
// methods (`is_class`) on the metaclass. Must run BEFORE
// objc_registerClassPair; the runtime locks the method
// list at registration time on some SDK versions.
for (entry_kv.methods) |method| {
const sel_str_global = self.emitPrivateCString(method.sel, "OBJC_METH_VAR_NAME_");
const enc_str_global = self.emitPrivateCString(method.encoding, "OBJC_METH_VAR_TYPE_");
var sel_args: [1]c.LLVMValueRef = .{sel_str_global};
const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel");
const imp_z = self.alloc.dupeZ(u8, method.imp_name) catch continue;
defer self.alloc.free(imp_z);
const imp_fn = c.LLVMGetNamedFunction(self.llvm_module, imp_z.ptr);
if (imp_fn == null) continue;
const target_cls = if (method.is_class) metaclass_val else cls_val;
var add_args: [4]c.LLVMValueRef = .{ target_cls, sel_val, imp_fn, enc_str_global };
_ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, "");
}
// M2.3 / M3.2 — register `#implements` protocol conformances
// BEFORE objc_registerClassPair. iOS checks
// `class_conformsToProtocol` when instantiating scene
// delegates and other protocol-typed callbacks; without
// these the runtime silently rejects the class.
//
// The protocol may not be present on every SDK / runtime
// (dead-strip pruning, version skew), so `objc_getProtocol`
// returning null is non-fatal — skip the addProtocol call.
const get_proto_fn, const get_proto_ty = self.lazyDeclareCRuntime("objc_getProtocol", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
const add_proto_fn, const add_proto_ty = self.lazyDeclareCRuntime("class_addProtocol", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, i8_ty, 0);
for (fcd.members) |m| switch (m) {
.implements => |proto_alias| {
const proto_str_global = self.emitPrivateCString(proto_alias, "OBJC_PROTOCOL_NAME_");
var gp_args: [1]c.LLVMValueRef = .{proto_str_global};
const proto_val = c.LLVMBuildCall2(self.builder, get_proto_ty, get_proto_fn, &gp_args, 1, "proto");
var ap_args: [2]c.LLVMValueRef = .{ cls_val, proto_val };
_ = c.LLVMBuildCall2(self.builder, add_proto_ty, add_proto_fn, &ap_args, 2, "");
},
else => {},
};
// objc_registerClassPair(cls)
var reg_args: [1]c.LLVMValueRef = .{cls_val};
_ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, &reg_args, 1, "");
// Cache the class pointer in `__<Cls>_class` global so the
// synthesized -dealloc trampoline (M1.2 A.6) can use it for
// [super dealloc] dispatch via objc_msgSendSuper2.
const class_global_name = std.fmt.allocPrint(self.alloc, "__{s}_class", .{class_name}) catch continue;
defer self.alloc.free(class_global_name);
const class_global_z = self.alloc.dupeZ(u8, class_global_name) catch continue;
defer self.alloc.free(class_global_z);
const class_global = c.LLVMGetNamedGlobal(self.llvm_module, class_global_z.ptr);
if (class_global != null) {
_ = c.LLVMBuildStore(self.builder, cls_val, class_global);
}
// M1.2 A.6 — register the synthesized `-dealloc` IMP on the
// class itself (instance method). The runtime fires it at
// refcount-zero; the IMP frees __sx_state and chains to
// [super dealloc].
const dealloc_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_dealloc_imp", .{class_name}) catch continue;
defer self.alloc.free(dealloc_imp_name);
const dealloc_imp_z = self.alloc.dupeZ(u8, dealloc_imp_name) catch continue;
defer self.alloc.free(dealloc_imp_z);
const dealloc_imp_fn = c.LLVMGetNamedFunction(self.llvm_module, dealloc_imp_z.ptr);
if (dealloc_imp_fn != null) {
const dealloc_sel_global = self.emitPrivateCString("dealloc", "OBJC_METH_VAR_NAME_");
const dealloc_enc_global = self.emitPrivateCString("v@:", "OBJC_METH_VAR_TYPE_");
var sel_args: [1]c.LLVMValueRef = .{dealloc_sel_global};
const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_dealloc");
var add_args: [4]c.LLVMValueRef = .{ cls_val, sel_val, dealloc_imp_fn, dealloc_enc_global };
_ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, "");
}
// M1.2 A.5 — register the synthesized `+alloc` IMP on the
// metaclass. Class methods live on the metaclass (every
// Class object's `isa` points to the metaclass), so we
// resolve it via `object_getClass(cls)` and `class_addMethod`
// the IMP there. Encoding `@@:` = returns id, takes Class,
// then SEL — Apple's standard `+alloc` shape. This override
// wins over NSObject's default +alloc; runtime instantiations
// (UIKit, Info.plist, NSCoder) go through our IMP and get the
// __sx_state ivar bound.
const alloc_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_alloc_imp", .{class_name}) catch continue;
defer self.alloc.free(alloc_imp_name);
const alloc_imp_z = self.alloc.dupeZ(u8, alloc_imp_name) catch continue;
defer self.alloc.free(alloc_imp_z);
const alloc_imp_fn = c.LLVMGetNamedFunction(self.llvm_module, alloc_imp_z.ptr);
if (alloc_imp_fn != null) {
// metaclass_val was computed up-front above (shared
// with class-method registration). +alloc is a class
// method registered on the metaclass.
const alloc_sel_global = self.emitPrivateCString("alloc", "OBJC_METH_VAR_NAME_");
const alloc_enc_global = self.emitPrivateCString("@@:", "OBJC_METH_VAR_TYPE_");
var sel_args: [1]c.LLVMValueRef = .{alloc_sel_global};
const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_alloc");
var add_args: [4]c.LLVMValueRef = .{ metaclass_val, sel_val, alloc_imp_fn, alloc_enc_global };
_ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, "");
}
// Cache the ivar handle in the per-class global so trampolines
// can read the __sx_state ivar without re-looking-it-up. The
// global is declared by lower.zig (M1.2 A.4b.i) and starts as
// null; the constructor fills it in here.
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{class_name}) catch continue;
defer self.alloc.free(ivar_global_name);
const ivar_global_z = self.alloc.dupeZ(u8, ivar_global_name) catch continue;
defer self.alloc.free(ivar_global_z);
const ivar_global = c.LLVMGetNamedGlobal(self.llvm_module, ivar_global_z.ptr);
if (ivar_global != null) {
var iv_args: [2]c.LLVMValueRef = .{ cls_val, sx_state_name_global };
const iv_val = c.LLVMBuildCall2(self.builder, get_iv_ty, get_iv_fn, &iv_args, 2, "iv");
_ = c.LLVMBuildStore(self.builder, iv_val, ivar_global);
}
}
_ = c.LLVMBuildRetVoid(self.builder);
// Inject the call into main's entry block ONLY — skip
// @llvm.global_ctors. Apple's frameworks (UIKit on iOS,
// AppKit on macOS) register their Obj-C classes during
// dyld's image-init phase, which overlaps global_ctors. If
// we ran there too, `objc_getClass(\"UIResponder\")` would
// return null and `objc_allocateClassPair(null, ...)` would
// crash inside objc_registerClassPair. main's entry runs
// AFTER dyld's framework init is complete but BEFORE user
// code (UIApplicationMain), so the runtime sees the parent
// class properly.
self.injectCtorIntoMain(ctor, ctor_ty);
_ = i32_ty;
}
/// Lazy-declare an extern C runtime function. Returns (fn-value, fn-type).
fn lazyDeclareCRuntime(self: *LLVMEmitter, name: []const u8, params: []const c.LLVMTypeRef, ret_ty: c.LLVMTypeRef, is_var_arg: c_int) struct { c.LLVMValueRef, c.LLVMTypeRef } {
const name_z = self.alloc.dupeZ(u8, name) catch unreachable;
defer self.alloc.free(name_z);
var fn_value = c.LLVMGetNamedFunction(self.llvm_module, name_z.ptr);
var fn_ty: c.LLVMTypeRef = undefined;
if (fn_value == null) {
fn_ty = c.LLVMFunctionType(ret_ty, @constCast(params.ptr), @intCast(params.len), is_var_arg);
fn_value = c.LLVMAddFunction(self.llvm_module, name_z.ptr, fn_ty);
c.LLVMSetLinkage(fn_value, c.LLVMExternalLinkage);
} else {
fn_ty = c.LLVMGlobalGetValueType(fn_value);
}
return .{ fn_value, fn_ty };
}
/// Emit a private constant C string global. Used for class names,
/// selector names, etc. consumed by the Obj-C runtime.
fn emitPrivateCString(self: *LLVMEmitter, s: []const u8, name_hint: []const u8) c.LLVMValueRef {
const s_z = self.alloc.allocSentinel(u8, s.len, 0) catch unreachable;
defer self.alloc.free(s_z);
@memcpy(s_z[0..s.len], s);
const str_const = c.LLVMConstStringInContext(self.context, s_z.ptr, @intCast(s.len), 0);
const name_z = self.alloc.dupeZ(u8, name_hint) catch unreachable;
defer self.alloc.free(name_z);
const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), name_z.ptr);
c.LLVMSetInitializer(str_global, str_const);
c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage);
c.LLVMSetGlobalConstant(str_global, 1);
c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr);
return str_global;
}
/// Append a constructor entry to `@llvm.global_ctors` (creating the
/// global if not present, extending the array if so) AND inject a
/// direct call from `main`'s entry block so the ORC JIT path runs
/// the constructor too.
/// Inject a call to `ctor()` at the start of `main`'s entry block
/// (past any existing init calls). Used by class-pair init etc.
/// that need to run BEFORE user code but AFTER dyld's framework
/// load — global_ctors is too early because Apple frameworks
/// (UIKit etc.) register their Obj-C classes during their own
/// init phase that overlaps ours.
fn injectCtorIntoMain(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void {
const main_z = "main";
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
if (main_fn == null) return;
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
var insert_before = c.LLVMGetFirstInstruction(entry_bb);
while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) {
if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break;
}
if (insert_before != null) {
c.LLVMPositionBuilderBefore(self.builder, insert_before);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
fn appendModuleCtor(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void {
const i32_ty = self.cached_i32;
const ptr_ty = self.cached_ptr;
var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty };
const ctor_struct_ty = c.LLVMStructTypeInContext(self.context, &ctor_field_types, 3, 0);
var ctor_fields: [3]c.LLVMValueRef = .{
c.LLVMConstInt(i32_ty, 65535, 0),
ctor,
c.LLVMConstNull(ptr_ty),
};
const ctor_entry = c.LLVMConstNamedStruct(ctor_struct_ty, &ctor_fields, 3);
const existing_z = "llvm.global_ctors";
const existing = c.LLVMGetNamedGlobal(self.llvm_module, existing_z);
if (existing != null) {
const existing_init = c.LLVMGetInitializer(existing);
const existing_arr_ty = c.LLVMGlobalGetValueType(existing);
const old_count = c.LLVMGetArrayLength(existing_arr_ty);
const new_count: c_uint = old_count + 1;
var new_entries = std.ArrayList(c.LLVMValueRef).empty;
defer new_entries.deinit(self.alloc);
var i: c_uint = 0;
while (i < old_count) : (i += 1) {
new_entries.append(self.alloc, c.LLVMGetAggregateElement(existing_init, i)) catch unreachable;
}
new_entries.append(self.alloc, ctor_entry) catch unreachable;
const new_arr_ty = c.LLVMArrayType2(ctor_struct_ty, new_count);
const new_init = c.LLVMConstArray2(ctor_struct_ty, new_entries.items.ptr, new_count);
const new_global = c.LLVMAddGlobal(self.llvm_module, new_arr_ty, "llvm.global_ctors.new");
c.LLVMSetInitializer(new_global, new_init);
c.LLVMSetLinkage(new_global, c.LLVMAppendingLinkage);
c.LLVMSetValueName2(existing, "llvm.global_ctors.old", "llvm.global_ctors.old".len);
c.LLVMSetValueName2(new_global, "llvm.global_ctors", "llvm.global_ctors".len);
c.LLVMDeleteGlobal(existing);
} else {
const ctors_arr_ty = c.LLVMArrayType2(ctor_struct_ty, 1);
var ctor_entries: [1]c.LLVMValueRef = .{ctor_entry};
const ctors_init = c.LLVMConstArray2(ctor_struct_ty, &ctor_entries, 1);
const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors");
c.LLVMSetInitializer(ctors_global, ctors_init);
c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage);
}
// ORC JIT: inject a direct call at the end of main's prelude
// (past any existing init calls).
const main_z = "main";
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
if (main_fn != null) {
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
var insert_before = c.LLVMGetFirstInstruction(entry_bb);
while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) {
if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break;
}
if (insert_before != null) {
c.LLVMPositionBuilderBefore(self.builder, insert_before);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
}
/// On macOS, emit a startup helper that chdir's to the .app bundle's
/// `Contents/Resources` directory when the executable lives inside a
/// `.app/Contents/MacOS/` path. Lets relative asset paths like
/// `assets/foo.png` resolve correctly when Finder/`open` launches the
/// binary with CWD=/.
///
/// Bundled binary: strstr finds the marker, chdir succeeds.
/// CLI binary / `sx run`: strstr returns null, the function no-ops.
///
/// The call is injected at the very start of `main()` (matching the
/// pattern used for the Obj-C selector init) rather than registered
/// via `@llvm.global_ctors`, so the ORC JIT path runs it too without
/// special handling.
fn emitMacosBundleChdir(self: *LLVMEmitter) void {
if (!self.target_config.is_aot) return;
if (!self.target_config.isMacOS()) return;
const ptr_ty = self.cached_ptr;
const i32_ty = self.cached_i32;
const i8_ty = self.cached_i8;
const void_ty = self.cached_void;
// Declare libc externs (re-use if already declared).
var ns_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty };
const ns_ty = c.LLVMFunctionType(i32_ty, &ns_params, 2, 0);
var ns_fn = c.LLVMGetNamedFunction(self.llvm_module, "_NSGetExecutablePath");
if (ns_fn == null) ns_fn = c.LLVMAddFunction(self.llvm_module, "_NSGetExecutablePath", ns_ty);
var chdir_params: [1]c.LLVMTypeRef = .{ptr_ty};
const chdir_ty = c.LLVMFunctionType(i32_ty, &chdir_params, 1, 0);
var chdir_fn = c.LLVMGetNamedFunction(self.llvm_module, "chdir");
if (chdir_fn == null) chdir_fn = c.LLVMAddFunction(self.llvm_module, "chdir", chdir_ty);
var ss_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty };
const ss_ty = c.LLVMFunctionType(ptr_ty, &ss_params, 2, 0);
var ss_fn = c.LLVMGetNamedFunction(self.llvm_module, "strstr");
if (ss_fn == null) ss_fn = c.LLVMAddFunction(self.llvm_module, "strstr", ss_ty);
var sc_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty };
const sc_ty = c.LLVMFunctionType(ptr_ty, &sc_params, 2, 0);
var sc_fn = c.LLVMGetNamedFunction(self.llvm_module, "strcpy");
if (sc_fn == null) sc_fn = c.LLVMAddFunction(self.llvm_module, "strcpy", sc_ty);
var no_params: [0]c.LLVMTypeRef = .{};
const ctor_ty = c.LLVMFunctionType(void_ty, &no_params, 0, 0);
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_macos_bundle_chdir", ctor_ty);
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
const found_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "found");
const done_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "done");
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
const buf_ty = c.LLVMArrayType2(i8_ty, 1024);
const buf = c.LLVMBuildAlloca(self.builder, buf_ty, "buf");
const bufsize = c.LLVMBuildAlloca(self.builder, i32_ty, "bufsize");
_ = c.LLVMBuildStore(self.builder, c.LLVMConstInt(i32_ty, 1024, 0), bufsize);
var ns_args: [2]c.LLVMValueRef = .{ buf, bufsize };
_ = c.LLVMBuildCall2(self.builder, ns_ty, ns_fn, &ns_args, 2, "");
const needle = self.emitCStringGlobal("/Contents/MacOS/", "__sx_macos_chdir_needle");
const replacement = self.emitCStringGlobal("/Contents/Resources", "__sx_macos_chdir_replacement");
var ss_args: [2]c.LLVMValueRef = .{ buf, needle };
const p = c.LLVMBuildCall2(self.builder, ss_ty, ss_fn, &ss_args, 2, "p");
const is_null = c.LLVMBuildIsNull(self.builder, p, "is_null");
_ = c.LLVMBuildCondBr(self.builder, is_null, done_bb, found_bb);
c.LLVMPositionBuilderAtEnd(self.builder, found_bb);
var sc_args: [2]c.LLVMValueRef = .{ p, replacement };
_ = c.LLVMBuildCall2(self.builder, sc_ty, sc_fn, &sc_args, 2, "");
var chdir_args: [1]c.LLVMValueRef = .{buf};
_ = c.LLVMBuildCall2(self.builder, chdir_ty, chdir_fn, &chdir_args, 1, "");
_ = c.LLVMBuildBr(self.builder, done_bb);
c.LLVMPositionBuilderAtEnd(self.builder, done_bb);
_ = c.LLVMBuildRetVoid(self.builder);
// Inject a call at the very start of main(). Matches the
// emitObjcSelectorInit pattern so the ORC JIT path picks it up
// without needing `@llvm.global_ctors` plumbing.
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, "main");
if (main_fn != null) {
const main_entry = c.LLVMGetEntryBasicBlock(main_fn);
const first_inst = c.LLVMGetFirstInstruction(main_entry);
if (first_inst != null) {
c.LLVMPositionBuilderBefore(self.builder, first_inst);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, main_entry);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
}
/// Return `{cls_slot, mid_slot}` global pair for the
/// `(name, sig)` literal — created on first lookup, shared across
/// later `#jni_call` sites with the same literal pair. Both
/// slots are zero-initialized `ptr`; the call-site lowering does
/// lazy population on first dispatch.
fn getOrCreateJniSlots(self: *LLVMEmitter, name: []const u8, sig: []const u8) JniSlotPair {
// Compose the key from name + a separator + sig. The separator
// is a byte that can't appear in a JNI method name or signature
// (NUL), so the same key never collides across distinct pairs.
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ name, sig }) catch unreachable;
if (self.jni_slots.get(key)) |existing| {
self.alloc.free(key);
return existing;
}
const mangled = self.mangleJniKey(name, sig);
defer self.alloc.free(mangled);
const cls_name = std.fmt.allocPrintSentinel(self.alloc, "SX_JNI_CLS_{s}", .{mangled}, 0) catch unreachable;
defer self.alloc.free(cls_name);
const mid_name = std.fmt.allocPrintSentinel(self.alloc, "SX_JNI_MID_{s}", .{mangled}, 0) catch unreachable;
defer self.alloc.free(mid_name);
const cls_slot = c.LLVMAddGlobal(self.llvm_module, self.cached_ptr, cls_name.ptr);
c.LLVMSetLinkage(cls_slot, c.LLVMInternalLinkage);
c.LLVMSetInitializer(cls_slot, c.LLVMConstNull(self.cached_ptr));
const mid_slot = c.LLVMAddGlobal(self.llvm_module, self.cached_ptr, mid_name.ptr);
c.LLVMSetLinkage(mid_slot, c.LLVMInternalLinkage);
c.LLVMSetInitializer(mid_slot, c.LLVMConstNull(self.cached_ptr));
const pair = JniSlotPair{ .cls_slot = cls_slot, .mid_slot = mid_slot };
self.jni_slots.put(key, pair) catch unreachable;
return pair;
}
/// Build an LLVM-friendly identifier suffix from a JNI
/// `(method_name, signature)` pair. Non-identifier characters are
/// rewritten to `_`; the resulting string is unique per pair (the
/// caller guarantees uniqueness on `(name, sig)`, which we
/// preserve through the separator between mangled name and sig).
fn mangleJniKey(self: *LLVMEmitter, name: []const u8, sig: []const u8) []u8 {
var buf = std.ArrayList(u8).empty;
for (name) |b| buf.append(self.alloc, if (isIdentByte(b)) b else '_') catch unreachable;
buf.appendSlice(self.alloc, "__") catch unreachable;
for (sig) |b| buf.append(self.alloc, if (isIdentByte(b)) b else '_') catch unreachable;
return buf.toOwnedSlice(self.alloc) catch unreachable;
}
/// If `val` is a `{ptr, i64}` slice struct, extract field 0
/// (the ptr); otherwise return it unchanged. Used by JNI dispatch
/// to feed string-literal method names + signatures to
/// `GetMethodID`, which expects raw C strings.
fn extractSlicePtr(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef {
const val_ty = c.LLVMTypeOf(val);
if (c.LLVMGetTypeKind(val_ty) != c.LLVMStructTypeKind) return val;
if (c.LLVMCountStructElementTypes(val_ty) != 2) return val;
const f0 = c.LLVMStructGetTypeAtIndex(val_ty, 0);
if (c.LLVMGetTypeKind(f0) != c.LLVMPointerTypeKind) return val;
return c.LLVMBuildExtractValue(self.builder, val, 0, "jni.str.ptr");
}
/// Load a JNI vtable function pointer at the given offset. `ifs`
/// is the `JNINativeInterface*` loaded from `JNIEnv*`. Treats the
/// vtable as an array of opaque `ptr`s and indexes into it.
fn loadJniFn(self: *LLVMEmitter, ifs: c.LLVMValueRef, offset: u32, name: [*:0]const u8) c.LLVMValueRef {
const offset_val = c.LLVMConstInt(self.cached_i32, offset, 0);
var idx = [_]c.LLVMValueRef{offset_val};
const slot = c.LLVMBuildInBoundsGEP2(self.builder, self.cached_ptr, ifs, &idx, 1, "");
return c.LLVMBuildLoad2(self.builder, self.cached_ptr, slot, name);
}
/// Lazily look up / declare the shared `@objc_msgSend` function.
/// Cached on the emitter; all `objc_msg_send` instructions hand
/// LLVMBuildCall2 their own per-call-site function type — the
/// underlying function value is just an opaque `ptr` symbol.
fn getObjcMsgSendValue(self: *LLVMEmitter) c.LLVMValueRef {
if (self.objc_msg_send_value) |v| return v;
const name_z = "objc_msgSend";
if (c.LLVMGetNamedFunction(self.llvm_module, name_z)) |existing| {
self.objc_msg_send_value = existing;
return existing;
}
// Seed with a `(ptr, ptr) -> ptr` shape; opaque pointers mean
// each call site can override.
var params: [2]c.LLVMTypeRef = .{ self.cached_ptr, self.cached_ptr };
const fn_ty = c.LLVMFunctionType(self.cached_ptr, &params, 2, 0);
const fn_val = c.LLVMAddFunction(self.llvm_module, name_z, fn_ty);
c.LLVMSetLinkage(fn_val, c.LLVMExternalLinkage);
self.objc_msg_send_value = fn_val;
return fn_val;
}
/// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types.
fn verifySizes(self: *LLVMEmitter) void {
// Skip for wasm32: 4-byte pointers vs IR's assumed 8-byte,
// so struct sizes will differ. LLVM handles emission correctly.
if (self.target_config.isWasm32()) return;
const dl = c.LLVMGetModuleDataLayout(self.llvm_module);
if (dl == null) return;
const type_count = self.ir_mod.types.infos.items.len;
for (TypeId.first_user..type_count) |idx| {
const ty = TypeId.fromIndex(@intCast(idx));
const info = self.ir_mod.types.get(ty);
// Only verify aggregate types where sizing is non-trivial
switch (info) {
.@"struct", .@"union", .tagged_union, .tuple => {},
else => continue,
}
const llvm_ty = self.toLLVMType(ty);
const llvm_size = c.LLVMABISizeOfType(dl, llvm_ty);
const ir_size = self.ir_mod.types.typeSizeBytes(ty);
std.debug.assert(llvm_size == ir_size);
}
}
/// Run comptime side-effect functions (e.g., `#run main();` at top level).
/// These are functions marked `is_comptime = true` with void return that
/// aren't associated with any global. They produce compile-time output.
fn runComptimeSideEffects(self: *LLVMEmitter) void {
for (self.ir_mod.functions.items, 0..) |func, i| {
if (!func.is_comptime or func.ret != .void) continue;
// Skip functions that are global initializers (already run by emitGlobals)
var is_global_init = false;
for (self.ir_mod.globals.items) |global| {
if (global.comptime_func) |gf| {
if (@intFromEnum(gf) == i) {
is_global_init = true;
break;
}
}
}
if (is_global_init) continue;
// Run the side-effect function via interpreter
const func_id = ir_inst.FuncId.fromIndex(@intCast(i));
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
_ = interp_inst.call(func_id, &.{}) catch {};
// Route #run `print` output to fd 1 so it joins the
// JIT-executed runtime's stream. Same call site shape as
// `core.flushInterpOutput` — see issue-0047.
if (interp_inst.output.items.len > 0) {
_ = std.c.write(1, interp_inst.output.items.ptr, interp_inst.output.items.len);
}
interp_inst.deinit();
}
}
fn emitGlobals(self: *LLVMEmitter) void {
for (self.ir_mod.globals.items, 0..) |global, i| {
const name = self.ir_mod.types.getString(global.name);
const llvm_ty = self.toLLVMType(global.ty);
const name_z = self.alloc.dupeZ(u8, name) catch continue;
defer self.alloc.free(name_z);
const llvm_global = c.LLVMAddGlobal(self.llvm_module, llvm_ty, name_z.ptr);
// Extern globals (`<name> : <type> #foreign;`) resolve at link time
// to a libSystem / framework symbol — no initializer, default linkage.
if (global.is_extern) {
c.LLVMSetLinkage(llvm_global, c.LLVMExternalLinkage);
self.global_map.put(@intCast(i), llvm_global) catch {};
continue;
}
c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage);
if (global.is_thread_local) {
c.LLVMSetThreadLocal(llvm_global, 1);
}
// Evaluate comptime initializer if present
if (global.comptime_func) |func_id| {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
Interpreter.last_bail_op = null;
Interpreter.last_bail_builtin = null;
Interpreter.last_bail_detail = null;
const result = interp_inst.call(func_id, &.{}) catch |err| blk: {
// Surface the bail loudly instead of silently filling
// the const with zero. Stale state from a previous
// comptime function would otherwise hide the error.
const op = Interpreter.last_bail_op orelse "<unknown>";
const detail = Interpreter.last_bail_detail orelse "";
const sep: []const u8 = if (detail.len > 0) ": " else "";
const gname = self.ir_mod.types.getString(global.name);
std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail });
break :blk .void_val;
};
const init_val = self.valueToLLVMConst(result, global.ty, &interp_inst, self.ir_mod.types.getString(global.name));
c.LLVMSetInitializer(llvm_global, init_val);
} else if (global.init_val) |iv| {
const init_val = switch (iv) {
.int => |v| c.LLVMConstInt(llvm_ty, @bitCast(v), 1),
.float => |v| c.LLVMConstReal(llvm_ty, v),
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |agg| self.emitConstAggregate(agg, llvm_ty),
.vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations
else => c.LLVMConstNull(llvm_ty),
};
c.LLVMSetInitializer(llvm_global, init_val);
} else {
c.LLVMSetInitializer(llvm_global, c.LLVMConstNull(llvm_ty));
}
self.global_map.put(@intCast(i), llvm_global) catch {};
}
}
/// Initialize vtable + aggregate-with-func_ref globals with function
/// pointer constants. Must run after Pass 1 (function declarations) so
/// func_map is populated — that's why these globals get a placeholder
/// initializer in `emitGlobals` and we fix them up here.
fn initVtableGlobals(self: *LLVMEmitter) void {
for (self.ir_mod.globals.items, 0..) |global, i| {
const iv = global.init_val orelse continue;
const llvm_global = self.global_map.get(@intCast(i)) orelse continue;
const llvm_ty = self.toLLVMType(global.ty);
switch (iv) {
.vtable => |func_ids| {
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (func_ids) |fid| {
const llvm_func = self.func_map.get(fid.index()) orelse {
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
continue;
};
field_vals.append(self.alloc, llvm_func) catch unreachable;
}
const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
c.LLVMSetInitializer(llvm_global, init_val);
c.LLVMSetGlobalConstant(llvm_global, 1);
},
.aggregate => |agg| {
// Re-emit. The first pass in `emitGlobals` already ran,
// but func_ref leaves resolved to null then (func_map
// wasn't populated yet). Now they resolve properly.
const init_val = self.emitConstAggregate(agg, llvm_ty);
c.LLVMSetInitializer(llvm_global, init_val);
},
else => continue,
}
}
}
/// Read `len` bytes from `addr` in the current process. Used to lift
/// comptime-evaluated heap data into a static binary constant — the
/// interp ran in this process, so any libc-malloc'd buffer it
/// produced is still mapped and readable. Returns `null` on a
/// null/zero address (callers handle empty-slice as a special case
/// before calling this).
fn readHostBytes(addr: usize, len: usize) ?[]const u8 {
if (addr == 0) return null;
const ptr: [*]const u8 = @ptrFromInt(addr);
return ptr[0..len];
}
/// Serialize an interp `Value` to an LLVM constant for use as a static
/// global initializer. `ty` is the IR-level type of the destination;
/// the LLVM type is derived from it. `interp` gives access to the
/// interpreter's heap so heap_ptr values can be walked. `global_name`
/// is included in any diagnostic the path produces so the user can
/// locate the offending `#run` site.
///
/// Returns `LLVMGetUndef` on bail — the build continues so adjacent
/// constants can still emit, but the diagnostic makes the problem clear.
fn valueToLLVMConst(
self: *LLVMEmitter,
val: Value,
ty: TypeId,
interp: *const Interpreter,
global_name: []const u8,
) c.LLVMValueRef {
const llvm_ty = self.toLLVMType(ty);
return switch (val) {
.int => |v| blk: {
// Host-pointer-as-int trap: the interp marshals raw pointers
// (libc-malloc'd buffers, etc.) into a .int that holds the
// host address. When that address is meant for a `ptr` slot
// in the destination type, emitting `LLVMConstInt` against
// the ptr type silently produces a malformed `i0 0`. The
// string/slice paths above handle this case by reading the
// pointed-to bytes; anything else with an int landing in a
// ptr slot is a Phase-1.4a heap-walk case we don't yet
// know how to serialize.
const kind = c.LLVMGetTypeKind(llvm_ty);
if (kind == c.LLVMPointerTypeKind) {
std.debug.print(
"error: comptime init of '{s}' produced a raw integer for a pointer field — needs IR-typed heap-walk serialization (Phase 1.4a heap-walk follow-up)\n",
.{global_name},
);
break :blk c.LLVMGetUndef(llvm_ty);
}
break :blk c.LLVMConstInt(llvm_ty, @bitCast(v), 1);
},
.float => |v| c.LLVMConstReal(llvm_ty, v),
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
.null_val => c.LLVMConstNull(llvm_ty),
.void_val, .undef => c.LLVMGetUndef(llvm_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(llvm_ty),
.string => |s| self.emitConstStringGlobal(s),
.aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name),
// The remaining Value variants cannot become static binary
// constants outside of a fat-pointer aggregate. Bail loudly.
// (`heap_ptr` / `byte_ptr` / `int → ptr` are handled inside
// `serializeAggregateValue` when they appear in a string or
// slice fat-pointer's data field.)
.heap_ptr, .byte_ptr, .slot_ptr, .closure, .type_tag => blk: {
std.debug.print(
"error: comptime init of '{s}' produced a {s} value, which cannot be serialized as a static constant\n",
.{ global_name, @tagName(val) },
);
break :blk c.LLVMGetUndef(llvm_ty);
},
};
}
/// Helper for `valueToLLVMConst` — serialize an aggregate value
/// against an IR TypeId. Splits on the type:
///
/// - `string` / `slice` — fat pointer `{ data, len }`. The data
/// field can be a heap_ptr (interp-managed memory), byte_ptr
/// (raw host address), int (same), or string literal. The len
/// field is consulted to know how many bytes to capture from
/// the data. Bytes are emitted as a private global byte array
/// and the aggregate constant points at it.
/// - `struct` — walk the IR field types in lockstep with the
/// value fields; recurse per field with its declared TypeId.
/// - `array` — walk elements with the array's element TypeId.
fn serializeAggregateValue(
self: *LLVMEmitter,
fields: []const Value,
ty: TypeId,
interp: *const Interpreter,
global_name: []const u8,
) c.LLVMValueRef {
const llvm_ty = self.toLLVMType(ty);
// Fat-pointer types: extract len, then read bytes from the data
// field's address (whatever flavour the interp produced for it).
const is_string = (ty == .string);
const is_slice = !ty.isBuiltin() and self.ir_mod.types.get(ty) == .slice;
if ((is_string or is_slice) and fields.len == 2) {
const data = fields[0];
const len_i = fields[1].asInt() orelse {
std.debug.print(
"error: comptime init of '{s}' produced a fat-pointer aggregate whose len field is not an integer\n",
.{global_name},
);
return c.LLVMGetUndef(llvm_ty);
};
const len: usize = @intCast(len_i);
const bytes_opt: ?[]const u8 = switch (data) {
.heap_ptr => |hp| blk: {
const mem = interp.heapSlice(hp) orelse break :blk null;
break :blk if (len <= mem.len) mem[0..len] else null;
},
.byte_ptr => |addr| readHostBytes(addr, len),
.int => |v| blk: {
if (v == 0 and len == 0) break :blk &.{}; // empty slice
if (v == 0) break :blk null;
break :blk readHostBytes(@as(usize, @bitCast(v)), len);
},
.string => |s| if (len <= s.len) s[0..len] else null,
else => null,
};
const bytes = bytes_opt orelse {
std.debug.print(
"error: comptime init of '{s}' produced a fat-pointer aggregate whose data field ({s}) cannot be resolved to {} bytes — needs Phase 1.4a heap-walk for this shape\n",
.{ global_name, @tagName(data), len },
);
return c.LLVMGetUndef(llvm_ty);
};
return self.emitConstStringGlobal(bytes);
}
// Generic struct: walk IR fields by their declared TypeIds.
if (!ty.isBuiltin()) {
const info = self.ir_mod.types.get(ty);
if (info == .@"struct") {
const ir_fields = info.@"struct".fields;
if (ir_fields.len != fields.len) {
std.debug.print(
"error: comptime init of '{s}' produced aggregate with {} fields but struct '{s}' expects {}\n",
.{ global_name, fields.len, self.ir_mod.types.getString(info.@"struct".name), ir_fields.len },
);
return c.LLVMGetUndef(llvm_ty);
}
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (ir_fields, fields) |ir_field, fv| {
field_vals.append(self.alloc, self.valueToLLVMConst(fv, ir_field.ty, interp, global_name)) catch unreachable;
}
return c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
}
if (info == .array) {
const elem_ty = info.array.element;
const llvm_elem_ty = self.toLLVMType(elem_ty);
var elem_vals = std.ArrayList(c.LLVMValueRef).empty;
defer elem_vals.deinit(self.alloc);
for (fields) |fv| {
elem_vals.append(self.alloc, self.valueToLLVMConst(fv, elem_ty, interp, global_name)) catch unreachable;
}
return c.LLVMConstArray2(llvm_elem_ty, elem_vals.items.ptr, @intCast(elem_vals.items.len));
}
}
std.debug.print(
"error: comptime init of '{s}' produced an aggregate but the destination type ({s}) is neither struct, array, string, nor slice\n",
.{ global_name, self.ir_mod.types.typeName(ty) },
);
return c.LLVMGetUndef(llvm_ty);
}
// ── Function declaration ────────────────────────────────────────
fn declareFunction(self: *LLVMEmitter, func: *const Function, func_idx: u32) void {
const name = self.ir_mod.types.getString(func.name);
// Skip builtins that are declared via getOrDeclare* with correct C-compatible types.
// The IR lowering creates extern stubs with IR types (e.g. memset → void return),
// but the C ABI may differ (memset returns ptr). Let getOrDeclare* handle these.
if (func.is_extern and isBuiltinLibcName(name)) {
// Still register in func_map so call resolution works
const builtin_fn = self.getOrDeclareBuiltinByName(name);
if (builtin_fn) |bf| {
self.func_map.put(func_idx, bf) catch unreachable;
return;
}
}
const is_main = std.mem.eql(u8, name, "main");
// main always returns i32 at the LLVM level (JIT expects it)
const raw_ret_ty = self.toLLVMType(func.ret);
const needs_c_abi = func.is_extern or func.call_conv == .c;
// sret return: C-ABI functions returning a >16 B non-HFA struct
// use the indirect-return convention (caller allocates space,
// passes its pointer as a hidden first arg with `sret(<T>)`,
// function writes through and returns void). Distinct from
// small-struct register coercion (i64 / [2 x i64]) and HFA.
const uses_sret = needs_c_abi and !is_main and self.needsByval(func.ret, raw_ret_ty);
const ret_ty = if (is_main) self.cached_i32
else if (uses_sret) self.cached_void
else if (needs_c_abi) self.abiCoerceParamTypeEx(func.ret, raw_ret_ty, func.is_extern)
else raw_ret_ty;
// Build parameter types — apply C ABI coercion for foreign/callconv(.c) functions.
// When uses_sret, prepend the sret pointer at index 0.
const sret_offset: usize = if (uses_sret) 1 else 0;
const param_count: c_uint = @intCast(func.params.len + sret_offset);
const param_types = self.alloc.alloc(c.LLVMTypeRef, func.params.len + sret_offset) catch unreachable;
defer self.alloc.free(param_types);
if (uses_sret) param_types[0] = self.cached_ptr;
for (func.params, 0..) |param, j| {
const llvm_ty = self.toLLVMType(param.ty);
param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamTypeEx(param.ty, llvm_ty, func.is_extern) else llvm_ty;
}
const is_var_arg: c_int = if (func.is_variadic) 1 else 0;
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, is_var_arg);
const name_z = self.alloc.dupeZ(u8, name) catch unreachable;
defer self.alloc.free(name_z);
const llvm_func = c.LLVMAddFunction(self.llvm_module, name_z.ptr, fn_type);
// sret(<RetType>) attribute on the prepended pointer param.
// LLVMAttributeIndex 1 = first parameter (0 = return value).
if (uses_sret) {
const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4);
const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, raw_ret_ty);
const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1));
c.LLVMAddAttributeAtIndex(llvm_func, param1_idx, sret_attr);
}
// Set linkage
switch (func.linkage) {
.external => c.LLVMSetLinkage(llvm_func, c.LLVMExternalLinkage),
.internal => c.LLVMSetLinkage(llvm_func, c.LLVMInternalLinkage),
.private => c.LLVMSetLinkage(llvm_func, c.LLVMPrivateLinkage),
}
// Set calling convention
if (func.call_conv == .c) {
c.LLVMSetFunctionCallConv(llvm_func, c.LLVMCCallConv);
}
// Add frame-pointer and nounwind attributes for correct ARM64 codegen
{
const fp_kind = "frame-pointer";
const fp_val = "all";
const fp_attr = c.LLVMCreateStringAttribute(
self.context,
fp_kind.ptr,
@intCast(fp_kind.len),
fp_val.ptr,
@intCast(fp_val.len),
);
const func_idx_attr: c.LLVMAttributeIndex = @bitCast(@as(i32, -1));
c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, fp_attr);
// Add nounwind
const nounwind_id = c.LLVMGetEnumAttributeKindForName("nounwind", 8);
const nounwind_attr = c.LLVMCreateEnumAttribute(self.context, nounwind_id, 0);
c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, nounwind_attr);
}
// Apple ARM64 ABI for >16B non-HFA composites: pass by reference
// via a pointer in the next int register (NOT via LLVM's `byval`
// attribute, which lowers the struct on the stack — incompatible
// with what `clang` emits and what foreign C callees expect).
// abiCoerceParamType returned `ptr` for these slots, so the formal
// param IS a plain pointer; the prologue loads the struct back.
self.func_map.put(func_idx, llvm_func) catch unreachable;
}
// ── Function body emission ──────────────────────────────────────
fn emitFunction(self: *LLVMEmitter, func: *const Function, func_idx: u32) void {
const llvm_func = self.func_map.get(func_idx) orelse unreachable;
const name = self.ir_mod.types.getString(func.name);
self.current_func_is_main = std.mem.eql(u8, name, "main");
self.current_func_idx = func_idx;
// DWARF: describe this function and make it the scope for the
// per-instruction locations set in emitInst (no-op if off).
self.beginFunctionDebug(func, llvm_func, name);
// Clear ref_map and pre-map parameter refs
self.ref_map.clearRetainingCapacity();
self.ref_counter = 0;
// Refs 0..N-1 are function parameters (matching the IR convention)
for (0..func.params.len) |pi| {
const param_val = c.LLVMGetParam(llvm_func, @intCast(pi));
self.mapRef(param_val);
}
// Create all basic blocks first (so branches can reference them)
for (func.blocks.items, 0..) |block, bi| {
const block_name = self.ir_mod.types.getString(block.name);
const block_name_z = self.alloc.dupeZ(u8, block_name) catch unreachable;
defer self.alloc.free(block_name_z);
const bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, block_name_z.ptr);
const block_key = makeBlockKey(func_idx, @intCast(bi));
self.block_map.put(block_key, bb) catch unreachable;
}
// byval params arrive as `ptr` in LLVM but the IR body expects struct values.
// At entry, load each byval param into a struct SSA value and re-map its ref.
const needs_c_abi = func.is_extern or func.call_conv == .c;
if (needs_c_abi and func.blocks.items.len > 0) {
const entry_key = makeBlockKey(func_idx, 0);
const entry_bb = self.block_map.get(entry_key) orelse unreachable;
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
for (func.params, 0..) |param, pi| {
const raw_llvm_ty = self.toLLVMType(param.ty);
if (self.needsByval(param.ty, raw_llvm_ty)) {
const ptr_val = c.LLVMGetParam(llvm_func, @intCast(pi));
const loaded = c.LLVMBuildLoad2(self.builder, raw_llvm_ty, ptr_val, "byval.load");
self.ref_map.put(@intCast(pi), loaded) catch unreachable;
}
}
}
// Clear pending phis for this function
self.pending_phis.clearRetainingCapacity();
// Emit instructions for each block — use first_ref to sync ref numbering
for (func.blocks.items, 0..) |block, bi| {
const block_key = makeBlockKey(func_idx, @intCast(bi));
const bb = self.block_map.get(block_key) orelse unreachable;
c.LLVMPositionBuilderAtEnd(self.builder, bb);
// Reset ref_counter to this block's actual starting ref
// (blocks may not be in emission order due to nested control flow)
self.ref_counter = block.first_ref;
for (block.insts.items, 0..) |instruction, inst_i| {
_ = inst_i;
self.emitInst(&instruction, func_idx);
}
}
// Fixup PHI nodes: scan all blocks for branches that pass args
self.fixupPhiNodes(func, func_idx);
// DWARF: leave no stale location for the next function.
self.endFunctionDebug();
}
/// After emitting all blocks, fill in PHI incoming values from branch args.
fn fixupPhiNodes(self: *LLVMEmitter, func: *const Function, func_idx: u32) void {
if (self.pending_phis.items.len == 0) return;
for (func.blocks.items, 0..) |block, bi| {
const src_key = makeBlockKey(func_idx, @intCast(bi));
const src_bb = self.block_map.get(src_key) orelse continue;
for (block.insts.items) |instruction| {
switch (instruction.op) {
.br => |branch| {
self.addPhiIncoming(branch.target, branch.args, src_bb);
},
.cond_br => |cb| {
self.addPhiIncoming(cb.then_target, cb.then_args, src_bb);
self.addPhiIncoming(cb.else_target, cb.else_args, src_bb);
},
.switch_br => |sw| {
for (sw.cases) |case| {
self.addPhiIncoming(case.target, case.args, src_bb);
}
self.addPhiIncoming(sw.default, sw.default_args, src_bb);
},
else => {},
}
}
}
}
fn addPhiIncoming(self: *LLVMEmitter, target: BlockId, args: []const Ref, src_bb: c.LLVMBasicBlockRef) void {
for (args, 0..) |arg, pi| {
const val = self.resolveRef(arg) orelse continue;
// Find the matching pending phi
for (self.pending_phis.items) |pp| {
if (pp.block_id.index() == target.index() and pp.param_index == pi) {
var incoming_vals = [1]c.LLVMValueRef{val};
var incoming_bbs = [1]c.LLVMBasicBlockRef{src_bb};
c.LLVMAddIncoming(pp.phi, &incoming_vals, &incoming_bbs, 1);
break;
}
}
}
}
// ── Instruction emission ────────────────────────────────────────
fn emitInst(self: *LLVMEmitter, instruction: *const Inst, func_idx: u32) void {
// DWARF: stamp every LLVM instruction this op emits with the sx
// source location (no-op when debug info is off).
self.setInstDebugLocation(instruction.span);
switch (instruction.op) {
// ── Constants ───────────────────────────────────────────
.const_int => |val| {
const ty = self.toLLVMType(instruction.ty);
const kind = c.LLVMGetTypeKind(ty);
const llvm_val = if (kind == c.LLVMIntegerTypeKind)
c.LLVMConstInt(ty, @bitCast(val), 1)
else if (kind == c.LLVMPointerTypeKind)
c.LLVMConstNull(ty)
else
// void or other non-integer type: emit i64 0 as unused placeholder
c.LLVMConstInt(c.LLVMInt64TypeInContext(self.context), 0, 0);
self.mapRef(llvm_val);
},
.const_float => |val| {
const ty = self.toLLVMType(instruction.ty);
const llvm_val = c.LLVMConstReal(ty, val);
self.mapRef(llvm_val);
},
.const_bool => |val| {
const llvm_val = c.LLVMConstInt(self.cached_i1, @intFromBool(val), 0);
self.mapRef(llvm_val);
},
.is_comptime => {
// Compiled code is never the comptime interpreter → constant
// `false`. A `if is_comptime() { … }` branch becomes dead.
self.mapRef(c.LLVMConstInt(self.cached_i1, 0, 0));
},
.interp_print_frames => {
// No interpreter stack in compiled code; this only ever sits in
// a dead `is_comptime()` branch. Emit nothing.
self.advanceRefCounter();
},
.const_string => |str_id| {
const str = self.ir_mod.types.getString(str_id);
const llvm_val = self.emitStringConstant(str);
self.mapRef(llvm_val);
},
.const_null => {
const ty = if (instruction.ty == .void) self.cached_ptr else self.toLLVMType(instruction.ty);
const llvm_val = c.LLVMConstNull(ty);
self.mapRef(llvm_val);
},
.const_undef => {
if (instruction.ty == .void) {
// void has no value — map to undef i64 as placeholder
self.mapRef(c.LLVMGetUndef(self.cached_i64));
} else {
const ty = self.toLLVMType(instruction.ty);
const llvm_val = c.LLVMGetUndef(ty);
self.mapRef(llvm_val);
}
},
.const_type => |tid| {
// Type values are Any-shaped pairs:
// { tag = .any.index() (the meta-marker),
// value = tid.index() }
// Lets storage in Any slots, struct fields,
// `Type`-typed vars, and slice elements all round-
// trip through the standard Any infrastructure.
// `case type:` in `any_to_string` matches on
// tag == `.any.index()`. Runtime `type_name(t)`
// extracts the value field and indexes into the
// type-name lookup table.
const any_ty = self.getAnyStructType();
const tag = c.LLVMConstInt(self.cached_i64, TypeId.any.index(), 0);
const val = c.LLVMConstInt(self.cached_i64, tid.index(), 0);
var result = c.LLVMGetUndef(any_ty);
result = c.LLVMBuildInsertValue(self.builder, result, tag, 0, "ct.tag");
result = c.LLVMBuildInsertValue(self.builder, result, val, 1, "ct.val");
self.mapRef(result);
},
// ── Arithmetic ─────────────────────────────────────────
.add => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFAdd(self.builder, lhs, rhs, "fadd")
else
c.LLVMBuildAdd(self.builder, lhs, rhs, "add");
self.mapRef(result);
},
.sub => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFSub(self.builder, lhs, rhs, "fsub")
else
c.LLVMBuildSub(self.builder, lhs, rhs, "sub");
self.mapRef(result);
},
.mul => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFMul(self.builder, lhs, rhs, "fmul")
else
c.LLVMBuildMul(self.builder, lhs, rhs, "mul");
self.mapRef(result);
},
.div => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFDiv(self.builder, lhs, rhs, "fdiv")
else if (isSignedType(instruction.ty))
c.LLVMBuildSDiv(self.builder, lhs, rhs, "sdiv")
else
c.LLVMBuildUDiv(self.builder, lhs, rhs, "udiv");
self.mapRef(result);
},
.mod => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFRem(self.builder, lhs, rhs, "fmod")
else if (isSignedType(instruction.ty))
c.LLVMBuildSRem(self.builder, lhs, rhs, "srem")
else
c.LLVMBuildURem(self.builder, lhs, rhs, "urem");
self.mapRef(result);
},
.neg => |un| {
const operand = self.resolveRef(un.operand);
const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types);
const result = if (is_float)
c.LLVMBuildFNeg(self.builder, operand, "fneg")
else
c.LLVMBuildNeg(self.builder, operand, "neg");
self.mapRef(result);
},
// ── Bitwise ────────────────────────────────────────────
.bit_and => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildAnd(self.builder, lhs, rhs, "and"));
},
.bit_or => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildOr(self.builder, lhs, rhs, "or"));
},
.bit_xor => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildXor(self.builder, lhs, rhs, "xor"));
},
.bit_not => |un| {
const operand = self.resolveRef(un.operand);
self.mapRef(c.LLVMBuildNot(self.builder, operand, "not"));
},
.shl => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildShl(self.builder, lhs, rhs, "shl"));
},
.shr => |bin| {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
// Use arithmetic shift right for signed, logical for unsigned
const result = if (isSignedType(instruction.ty))
c.LLVMBuildAShr(self.builder, lhs, rhs, "ashr")
else
c.LLVMBuildLShr(self.builder, lhs, rhs, "lshr");
self.mapRef(result);
},
// ── Comparisons ───────────────────────────────────────
.cmp_eq => |bin| self.emitCmp(bin, instruction.ty, c.LLVMIntEQ, c.LLVMRealOEQ),
.cmp_ne => |bin| self.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealONE),
.cmp_lt => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSLT, c.LLVMIntULT, c.LLVMRealOLT),
.cmp_le => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSLE, c.LLVMIntULE, c.LLVMRealOLE),
.cmp_gt => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSGT, c.LLVMIntUGT, c.LLVMRealOGT),
.cmp_ge => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSGE, c.LLVMIntUGE, c.LLVMRealOGE),
.str_eq => |bin| self.emitStrCmp(bin, true),
.str_ne => |bin| self.emitStrCmp(bin, false),
// ── Logical ───────────────────────────────────────────
.bool_and => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
self.mapRef(c.LLVMBuildAnd(self.builder, lhs, rhs, "land"));
},
.bool_or => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
self.mapRef(c.LLVMBuildOr(self.builder, lhs, rhs, "lor"));
},
.bool_not => |un| {
const operand = self.resolveRef(un.operand);
self.mapRef(c.LLVMBuildNot(self.builder, operand, "lnot"));
},
// ── Memory ────────────────────────────────────────────
.alloca => |elem_ty| {
const llvm_ty = self.toLLVMType(elem_ty);
const result = c.LLVMBuildAlloca(self.builder, llvm_ty, "alloca");
self.mapRef(result);
},
.load => |un| {
const ptr = self.resolveRef(un.operand);
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) {
const llvm_ty = self.toLLVMType(instruction.ty);
const result = c.LLVMBuildLoad2(self.builder, llvm_ty, ptr, "load");
self.mapRef(result);
} else {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(if (instruction.ty == .void) .s64 else instruction.ty)));
}
},
.store => |st| {
const ptr = self.resolveRef(st.ptr);
var val = self.resolveRef(st.val);
// Guard: don't store void types or store to non-pointer
const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr));
const val_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(val));
if (ptr_kind == c.LLVMPointerTypeKind and val_kind != c.LLVMVoidTypeKind) {
// Coerce value to match the IR-declared pointer target type.
// E.g. storing i64 to *i8 (from index_gep on string) needs truncation.
//
// Only unwrap .pointer (from index_gep/alloca: *element → element).
// Never unwrap .many_pointer — it only appears as struct_gep field
// value types (e.g., [*]BigNode), where unwrapping to the element
// type gives a wrong store size (stores BigNode-sized instead of ptr).
if (self.getRefIRType(st.ptr)) |ptr_ir_ty| {
const pointee_info = self.ir_mod.types.get(ptr_ir_ty);
const target_ty: ?c.LLVMTypeRef = switch (pointee_info) {
.pointer => |p| self.toLLVMType(p.pointee),
else => null,
};
if (target_ty) |tt| {
val = self.coerceArg(val, tt);
}
}
_ = c.LLVMBuildStore(self.builder, val, ptr);
}
self.advanceRefCounter();
},
// ── Globals ───────────────────────────────────────────
.global_get => |gid| {
const llvm_global = self.global_map.get(gid.index()) orelse {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
};
const llvm_ty = self.toLLVMType(instruction.ty);
self.mapRef(c.LLVMBuildLoad2(self.builder, llvm_ty, llvm_global, "gload"));
},
.global_addr => |gid| {
const llvm_global = self.global_map.get(gid.index()) orelse {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
return;
};
// Return the global's address directly (no load)
self.mapRef(llvm_global);
},
.func_ref => |fid| {
// Produce a reference to the function as a function pointer value
if (self.func_map.get(@intFromEnum(fid))) |llvm_func| {
self.mapRef(llvm_func);
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
},
.global_set => |gs| {
const llvm_global = self.global_map.get(gs.global.index()) orelse {
self.advanceRefCounter();
return;
};
const val = self.resolveRef(gs.value);
_ = c.LLVMBuildStore(self.builder, val, llvm_global);
self.advanceRefCounter();
},
// ── Conversions ───────────────────────────────────────
.widen => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
const result = self.emitConversion(operand, conv.from, conv.to, to_ty);
self.mapRef(result);
},
.narrow => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
const result = self.emitConversion(operand, conv.from, conv.to, to_ty);
self.mapRef(result);
},
.bitcast => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
// LLVMBuildBitCast doesn't accept ptr↔int on modern
// LLVM. Dispatch to PtrToInt / IntToPtr when needed —
// lower.zig emits a `bitcast` IR op for both shapes.
const from_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(operand));
const to_kind = c.LLVMGetTypeKind(to_ty);
if (from_kind == c.LLVMPointerTypeKind and to_kind == c.LLVMIntegerTypeKind) {
const i64_val = c.LLVMBuildPtrToInt(self.builder, operand, self.cached_i64, "pti");
const w = c.LLVMGetIntTypeWidth(to_ty);
if (w == 64) {
self.mapRef(i64_val);
} else if (w < 64) {
self.mapRef(c.LLVMBuildTrunc(self.builder, i64_val, to_ty, "pti.tr"));
} else {
self.mapRef(c.LLVMBuildZExt(self.builder, i64_val, to_ty, "pti.ext"));
}
} else if (from_kind == c.LLVMIntegerTypeKind and to_kind == c.LLVMPointerTypeKind) {
self.mapRef(c.LLVMBuildIntToPtr(self.builder, operand, to_ty, "itp"));
} else {
self.mapRef(c.LLVMBuildBitCast(self.builder, operand, to_ty, "bitcast"));
}
},
.int_to_float => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
const result = if (isSignedType(conv.from))
c.LLVMBuildSIToFP(self.builder, operand, to_ty, "sitofp")
else
c.LLVMBuildUIToFP(self.builder, operand, to_ty, "uitofp");
self.mapRef(result);
},
.float_to_int => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
const result = if (isSignedType(conv.to))
c.LLVMBuildFPToSI(self.builder, operand, to_ty, "fptosi")
else
c.LLVMBuildFPToUI(self.builder, operand, to_ty, "fptoui");
self.mapRef(result);
},
// ── Pointer ops ───────────────────────────────────────
.addr_of => |un| {
// addr_of returns the pointer directly (the operand is already a ptr from alloca)
self.mapRef(self.resolveRef(un.operand));
},
.deref => |un| {
const ptr = self.resolveRef(un.operand);
const llvm_ty = self.toLLVMType(instruction.ty);
self.mapRef(c.LLVMBuildLoad2(self.builder, llvm_ty, ptr, "deref"));
},
// ── Calls ─────────────────────────────────────────────
.objc_msg_send => |msg| {
const msg_send = self.getObjcMsgSendValue();
// Detect the sret case: >16 B non-HFA struct return.
// Same predicate as the plain-foreign-call path so the
// two arms stay in lockstep.
const raw_ret_ty = self.toLLVMType(instruction.ty);
const uses_sret = self.needsByval(instruction.ty, raw_ret_ty);
const ret_ty = if (uses_sret) self.cached_void else raw_ret_ty;
// Slot layout:
// uses_sret = false → [recv, sel, args...]
// uses_sret = true → [sret_slot, recv, sel, args...]
const sret_off: usize = if (uses_sret) 1 else 0;
const total_params: usize = 2 + msg.args.len + sret_off;
const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable;
defer self.alloc.free(param_types);
const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable;
defer self.alloc.free(call_args);
var sret_slot: c.LLVMValueRef = null;
if (uses_sret) {
sret_slot = c.LLVMBuildAlloca(self.builder, raw_ret_ty, "objc.sret");
param_types[0] = self.cached_ptr;
call_args[0] = sret_slot;
}
// recv (typed *void from the IR)
param_types[sret_off] = self.cached_ptr;
call_args[sret_off] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
// sel (loaded SEL — opaque ptr)
param_types[sret_off + 1] = self.cached_ptr;
call_args[sret_off + 1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr);
// additional args take their IR types, with ABI
// coercion applied so structs / strings decay the
// same way they do for any C foreign call.
for (msg.args, 0..) |arg_ref, i| {
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
const raw_llvm = self.toLLVMType(raw_ty);
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
param_types[i + 2 + sret_off] = coerced_ty;
call_args[i + 2 + sret_off] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0);
const call_label: [*:0]const u8 = if (instruction.ty == .void or uses_sret) "" else "objc.msg";
var result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label);
if (uses_sret) {
// Tag the call's arg 0 (sret slot) with the sret
// attribute so the AArch64 / SysV backends route
// through the x8 / hidden-pointer convention.
const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4);
const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, raw_ret_ty);
const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1));
c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr);
result = c.LLVMBuildLoad2(self.builder, raw_ret_ty, sret_slot, "objc.sret.load");
}
// Always mapRef — the IR Ref counter for this
// instruction advances regardless of return type,
// so skipping it would misalign every subsequent
// ref lookup in this function.
self.mapRef(result);
},
.jni_msg_send => |msg| {
// JNI vtable indirection:
// ifs = *env // JNINativeInterface*
// instance: cls = ifs[GetObjectClass](env, target)
// mid = ifs[GetMethodID](env, cls, name, sig)
// ifs[Call<T>Method](env, target, mid, args...)
// static: target IS the jclass — skip GetObjectClass
// mid = ifs[GetStaticMethodID](env, target, name, sig)
// ifs[CallStatic<T>Method](env, target, mid, args...)
// ctor: cls = ifs[FindClass](env, parent_class_path)
// mid = ifs[GetMethodID](env, cls, "<init>", sig)
// ifs[NewObject](env, cls, mid, args...) → jobject
// nonvirt: handled below via FindClass + GetMethodID +
// CallNonvirtual<T>Method.
// The cached path (msg.cache_key != null) still shares one
// (jclass GlobalRef, jmethodID) pair per literal (name, sig).
if (msg.is_constructor) {
self.emitJniConstructor(msg, instruction.ty);
return;
}
const ret_ty_id = instruction.ty;
const is_pointer_ret = switch (self.ir_mod.types.get(ret_ty_id)) {
.pointer, .many_pointer => true,
else => false,
};
const call_method_offset: u32 = if (msg.is_static) blk: {
if (is_pointer_ret) break :blk Jni.CallStaticObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallStaticVoidMethod,
.s32 => Jni.CallStaticIntMethod,
.s64 => Jni.CallStaticLongMethod,
.f32 => Jni.CallStaticFloatMethod,
.f64 => Jni.CallStaticDoubleMethod,
.bool => Jni.CallStaticBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
} else if (msg.is_nonvirtual) blk: {
if (is_pointer_ret) break :blk Jni.CallNonvirtualObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallNonvirtualVoidMethod,
.s32 => Jni.CallNonvirtualIntMethod,
.s64 => Jni.CallNonvirtualLongMethod,
.f32 => Jni.CallNonvirtualFloatMethod,
.f64 => Jni.CallNonvirtualDoubleMethod,
.bool => Jni.CallNonvirtualBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
} else blk: {
if (is_pointer_ret) break :blk Jni.CallObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallVoidMethod,
.s32 => Jni.CallIntMethod,
.s64 => Jni.CallLongMethod,
.f32 => Jni.CallFloatMethod,
.f64 => Jni.CallDoubleMethod,
.bool => Jni.CallBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
};
const get_mid_offset: u32 = if (msg.is_static) Jni.GetStaticMethodID else Jni.GetMethodID;
const env = self.resolveRef(msg.env);
const target = self.resolveRef(msg.target);
// String literals lower as `{ptr, i64}` slices in sx IR;
// JNI's `GetMethodID` expects raw C strings, so extract
// field 0 when the source is a slice.
const name_ptr = self.extractSlicePtr(self.resolveRef(msg.name));
const sig_ptr = self.extractSlicePtr(self.resolveRef(msg.sig));
const ifs = c.LLVMBuildLoad2(self.builder, self.cached_ptr, env, "jni.ifs");
// Method-ID resolution. When `name` and `sig` are both
// string literals the call site participates in
// `(name, sig)` slot interning (step 1.17): a shared
// pair of static globals holds the `jclass` GlobalRef
// and the `jmethodID`, populated lazily on the first
// call to any matching site. Non-literal sites fall
// back to the per-call `GetObjectClass + GetMethodID`
// sequence (1.15 shape).
const mid = if (msg.cache_key) |ck| blk: {
const pair = self.getOrCreateJniSlots(ck.name_str, ck.sig_str);
const cached_mid = c.LLVMBuildLoad2(self.builder, self.cached_ptr, pair.mid_slot, "jni.cached.mid");
const is_cached = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cached_mid, c.LLVMConstNull(self.cached_ptr), "jni.is.cached");
const cur_fn = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const miss_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "jni.miss");
const cont_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "jni.cont");
const before_bb = c.LLVMGetInsertBlock(self.builder);
_ = c.LLVMBuildCondBr(self.builder, is_cached, cont_bb, miss_bb);
// Miss path:
// instance: GetObjectClass → NewGlobalRef → GetMethodID
// static: target IS class → NewGlobalRef(target) → GetStaticMethodID
c.LLVMPositionBuilderAtEnd(self.builder, miss_bb);
const local_cls = if (msg.is_static) target else inst_cls: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
};
const new_global_ref = self.loadJniFn(ifs, Jni.NewGlobalRef, "jni.NewGlobalRef");
var ngref_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const ngref_ty = c.LLVMFunctionType(self.cached_ptr, &ngref_params, 2, 0);
var ngref_args = [_]c.LLVMValueRef{ env, local_cls };
const global_cls = c.LLVMBuildCall2(self.builder, ngref_ty, new_global_ref, &ngref_args, 2, "jni.global.cls");
_ = c.LLVMBuildStore(self.builder, global_cls, pair.cls_slot);
const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID");
var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr };
const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0);
var gmid_args = [_]c.LLVMValueRef{ env, global_cls, name_ptr, sig_ptr };
const fresh_mid = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.fresh.mid");
_ = c.LLVMBuildStore(self.builder, fresh_mid, pair.mid_slot);
const miss_end_bb = c.LLVMGetInsertBlock(self.builder);
_ = c.LLVMBuildBr(self.builder, cont_bb);
// Cont: phi the cached vs fresh mid.
c.LLVMPositionBuilderAtEnd(self.builder, cont_bb);
const phi = c.LLVMBuildPhi(self.builder, self.cached_ptr, "jni.mid");
var phi_vals = [_]c.LLVMValueRef{ cached_mid, fresh_mid };
var phi_blocks = [_]c.LLVMBasicBlockRef{ before_bb, miss_end_bb };
c.LLVMAddIncoming(phi, &phi_vals, &phi_blocks, 2);
break :blk phi;
} else blk: {
const cls = if (msg.is_static) target else if (msg.is_nonvirtual) nonvirt_cls: {
// `super.method(args)`: dispatch is bound to a
// specific class (the parent), not subclass-override.
// Resolve via FindClass(parent_path). No caching yet —
// per-call lookup. The parent path is a NUL-terminated
// C string emitted as a private LLVM global.
const path = msg.parent_class_path orelse "";
const path_global = self.emitCStringGlobal(path, "jni.parent.path");
const find_class = self.loadJniFn(ifs, Jni.FindClass, "jni.FindClass");
var fc_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const fc_ty = c.LLVMFunctionType(self.cached_ptr, &fc_params, 2, 0);
var fc_args = [_]c.LLVMValueRef{ env, path_global };
break :nonvirt_cls c.LLVMBuildCall2(self.builder, fc_ty, find_class, &fc_args, 2, "jni.parent.cls");
} else inst_cls: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
};
const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID");
var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr };
const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0);
var gmid_args = [_]c.LLVMValueRef{ env, cls, name_ptr, sig_ptr };
const mid_val = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.mid");
if (msg.is_nonvirtual) {
// Stash cls in a dummy slot so the call site below
// can pick it up. Easiest path: do the call right
// here and return Ref.none, but we need to keep the
// outer phi shape. Instead, return both via tuple
// through an auxiliary local — simplest is to attach
// `cls` to a per-invocation slot. Use a stack alloca.
const cls_slot = c.LLVMBuildAlloca(self.builder, self.cached_ptr, "jni.parent.cls.slot");
_ = c.LLVMBuildStore(self.builder, cls, cls_slot);
// Tag the slot pointer onto the phi result via the
// generated metadata: we'll re-extract by re-running
// FindClass — actually simpler: lower nonvirtual on
// the spot below. Drop the implicit `break` here:
const call_fn = self.loadJniFn(ifs, call_method_offset, "jni.callfn.nonvirtual");
const raw_ret = self.toLLVMType(ret_ty_id);
const total_call_params_nv: usize = 4 + msg.args.len;
const call_param_types_nv = self.alloc.alloc(c.LLVMTypeRef, total_call_params_nv) catch unreachable;
defer self.alloc.free(call_param_types_nv);
const call_args_nv = self.alloc.alloc(c.LLVMValueRef, total_call_params_nv) catch unreachable;
defer self.alloc.free(call_args_nv);
call_param_types_nv[0] = self.cached_ptr;
call_param_types_nv[1] = self.cached_ptr;
call_param_types_nv[2] = self.cached_ptr;
call_param_types_nv[3] = self.cached_ptr;
call_args_nv[0] = env;
call_args_nv[1] = target;
call_args_nv[2] = cls;
call_args_nv[3] = mid_val;
for (msg.args, 0..) |arg_ref, i| {
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
const raw_llvm = self.toLLVMType(raw_ty);
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
call_param_types_nv[i + 4] = coerced_ty;
call_args_nv[i + 4] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const call_fn_ty_nv = c.LLVMFunctionType(raw_ret, call_param_types_nv.ptr, @intCast(total_call_params_nv), 0);
const label_nv: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.nonvirtual.ret";
const result_nv = c.LLVMBuildCall2(self.builder, call_fn_ty_nv, call_fn, call_args_nv.ptr, @intCast(total_call_params_nv), label_nv);
self.mapRef(result_nv);
return;
}
break :blk mid_val;
};
// Call<Type>Method: (JNIEnv*, jobject, jmethodID, args...) -> RetTy
const call_fn = self.loadJniFn(ifs, call_method_offset, "jni.callfn");
const raw_ret = self.toLLVMType(ret_ty_id);
const total_call_params: usize = 3 + msg.args.len;
const call_param_types = self.alloc.alloc(c.LLVMTypeRef, total_call_params) catch unreachable;
defer self.alloc.free(call_param_types);
const call_args = self.alloc.alloc(c.LLVMValueRef, total_call_params) catch unreachable;
defer self.alloc.free(call_args);
call_param_types[0] = self.cached_ptr;
call_param_types[1] = self.cached_ptr;
call_param_types[2] = self.cached_ptr;
call_args[0] = env;
call_args[1] = target;
call_args[2] = mid;
for (msg.args, 0..) |arg_ref, i| {
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
const raw_llvm = self.toLLVMType(raw_ty);
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
call_param_types[i + 3] = coerced_ty;
call_args[i + 3] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const call_fn_ty = c.LLVMFunctionType(raw_ret, call_param_types.ptr, @intCast(total_call_params), 0);
const label: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.ret";
const result = c.LLVMBuildCall2(self.builder, call_fn_ty, call_fn, call_args.ptr, @intCast(total_call_params), label);
self.mapRef(result);
},
.call => |call_op| {
// Evaluate comptime functions at compile time
const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
if (callee_func.is_comptime and call_op.args.len == 0) {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
defer interp_inst.deinit();
if (interp_inst.call(call_op.callee, &.{})) |result| {
if (result.asInt()) |v| {
self.mapRef(c.LLVMConstInt(self.toLLVMType(instruction.ty), @bitCast(v), 0));
return;
} else if (result.asFloat()) |v| {
self.mapRef(c.LLVMConstReal(self.toLLVMType(instruction.ty), v));
return;
} else if (result.asBool()) |v| {
self.mapRef(c.LLVMConstInt(self.toLLVMType(instruction.ty), @intFromBool(v), 0));
return;
} else if (result == .string) {
self.mapRef(self.emitStringConstant(result.string));
return;
}
} else |_| {}
}
const callee = self.func_map.get(call_op.callee.index()) orelse {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
};
const callee_needs_c_abi = callee_func.is_extern or callee_func.call_conv == .c;
const callee_raw_ret = self.toLLVMType(callee_func.ret);
const callee_uses_sret = callee_needs_c_abi and self.needsByval(callee_func.ret, callee_raw_ret);
// When the callee uses sret, prepend an alloca for the result.
// Index alignment: actual_args[0] = sret_slot; actual_args[i+1] = sx arg i.
const sret_off: usize = if (callee_uses_sret) 1 else 0;
const total_args = call_op.args.len + sret_off;
const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable;
defer self.alloc.free(args);
var sret_slot: c.LLVMValueRef = null;
if (callee_uses_sret) {
sret_slot = c.LLVMBuildAlloca(self.builder, callee_raw_ret, "sret.slot");
args[0] = sret_slot;
}
for (call_op.args, 0..) |arg_ref, j| {
args[j + sret_off] = self.resolveRef(arg_ref);
}
const arg_count: c_uint = @intCast(total_args);
// Get the function type from LLVM and coerce arguments
const fn_ty = c.LLVMGlobalGetValueType(callee);
const param_count = c.LLVMCountParamTypes(fn_ty);
if (param_count > 0) {
const param_types = self.alloc.alloc(c.LLVMTypeRef, param_count) catch unreachable;
defer self.alloc.free(param_types);
c.LLVMGetParamTypes(fn_ty, param_types.ptr);
for (0..@min(args.len, param_count)) |j| {
// The sret slot is already a properly-typed pointer; skip coercion.
if (callee_uses_sret and j == 0) continue;
const fn_param_idx = j - sret_off;
// Materialize byval args before coercion so we pass a ptr instead of the struct value.
if (callee_needs_c_abi and fn_param_idx < callee_func.params.len) {
const ir_ty = callee_func.params[fn_param_idx].ty;
const raw_struct = self.toLLVMType(ir_ty);
if (self.needsByval(ir_ty, raw_struct)) {
args[j] = self.materializeByvalArg(args[j], raw_struct);
continue;
}
}
args[j] = self.coerceArg(args[j], param_types[j]);
}
}
// A `void`/`noreturn` call has no value, so it must stay
// unnamed (LLVM rejects a named void result).
const call_is_void_like = instruction.ty == .void or instruction.ty == .noreturn;
const call_label: [*:0]const u8 = if (call_is_void_like or callee_uses_sret) "" else "call";
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, call_label);
if (callee_uses_sret) {
// Mirror the function-decl `sret(<T>)` attribute on the call site so the
// LLVM backend lowers arg 0 via x8 (AAPCS64) / hidden ptr (SysV AMD64).
const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4);
const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, callee_raw_ret);
const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1));
c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr);
// Load the actual struct value the callee wrote into the slot.
result = c.LLVMBuildLoad2(self.builder, callee_raw_ret, sret_slot, "sret.load");
} else if (!call_is_void_like and callee_func.is_extern) {
// Coerce ABI return value (e.g. i64 / [2 x i64]) back to IR struct type if needed
const expected_ty = self.toLLVMType(instruction.ty);
result = self.coerceArg(result, expected_ty);
}
self.mapRef(result);
},
.call_indirect => |call_op| {
const callee = self.resolveRef(call_op.callee);
const arg_count: c_uint = @intCast(call_op.args.len);
const args = self.alloc.alloc(c.LLVMValueRef, call_op.args.len) catch unreachable;
defer self.alloc.free(args);
for (call_op.args, 0..) |arg_ref, j| {
args[j] = self.resolveRef(arg_ref);
}
// Get callee's IR type to resolve parameter types accurately
const callee_ir_ty = self.getRefIRType(call_op.callee);
const fn_params: ?[]const @import("types.zig").TypeId = if (callee_ir_ty) |cty| blk: {
if (!cty.isBuiltin()) {
const ci = self.ir_mod.types.get(cty);
switch (ci) {
.function => |f| break :blk f.params,
.closure => |cl| break :blk cl.params,
else => {},
}
}
break :blk null;
} else null;
// Read the fn-pointer type's calling convention. Only `.c` opts
// into the C-ABI byval coercion for >16B aggregate params.
const fp_is_c_abi: bool = if (callee_ir_ty) |cty| blk: {
if (!cty.isBuiltin()) {
const ci = self.ir_mod.types.get(cty);
if (ci == .function and ci.function.call_conv == .c) break :blk true;
}
break :blk false;
} else false;
// Default-conv fn-pointers under implicit-ctx carry a hidden
// `*void` (the implicit __sx_ctx) at LLVM slot 0. The IR fn
// type does not include it, so shift fn_params lookups by 1.
const fp_ctx_slots: usize = if (callee_ir_ty) |cty| blk: {
if (!self.ir_mod.has_implicit_ctx) break :blk 0;
if (cty.isBuiltin()) break :blk 0;
const ci = self.ir_mod.types.get(cty);
switch (ci) {
.function => |f| break :blk if (f.call_conv == .c) @as(usize, 0) else 1,
else => break :blk 0,
}
} else 0;
const ret_ty = if (callee_ir_ty) |cty| blk: {
if (!cty.isBuiltin()) {
const ci = self.ir_mod.types.get(cty);
switch (ci) {
.function => |f| break :blk self.toLLVMType(f.ret),
.closure => |cl| break :blk self.toLLVMType(cl.ret),
else => {},
}
}
break :blk self.toLLVMType(instruction.ty);
} else self.toLLVMType(instruction.ty);
const param_tys = self.alloc.alloc(c.LLVMTypeRef, call_op.args.len) catch unreachable;
defer self.alloc.free(param_tys);
if (fn_params) |fp| {
for (0..call_op.args.len) |j| {
// Slots 0..fp_ctx_slots are the implicit __sx_ctx
// (passed as opaque ptr; not in fp).
if (j < fp_ctx_slots) {
param_tys[j] = self.cached_ptr;
args[j] = self.coerceArg(args[j], self.cached_ptr);
continue;
}
const fp_idx = j - fp_ctx_slots;
if (fp_idx < fp.len) {
const raw_struct = self.toLLVMType(fp[fp_idx]);
if (fp_is_c_abi and self.needsByval(fp[fp_idx], raw_struct)) {
args[j] = self.materializeByvalArg(args[j], raw_struct);
param_tys[j] = self.cached_ptr;
continue;
}
var llvm_pty = raw_struct;
// Array params in fn-ptr calls decay to pointers (C ABI)
if (c.LLVMGetTypeKind(llvm_pty) == c.LLVMArrayTypeKind) {
llvm_pty = self.cached_ptr;
}
param_tys[j] = llvm_pty;
args[j] = self.coerceArg(args[j], llvm_pty);
} else {
param_tys[j] = c.LLVMTypeOf(args[j]);
}
}
} else {
for (args, 0..) |arg, j| {
param_tys[j] = c.LLVMTypeOf(arg);
}
}
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, arg_count, 0);
const icall_void_like = instruction.ty == .void or instruction.ty == .noreturn;
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (icall_void_like) "" else "icall");
// Coerce call result to instruction's expected type
const expected_ty = self.toLLVMType(instruction.ty);
if (!icall_void_like and c.LLVMTypeOf(result) != expected_ty) {
result = self.coerceArg(result, expected_ty);
}
self.mapRef(result);
},
// ── Terminators ────────────────────────────────────────
.ret => |un| {
var val = self.resolveRef(un.operand);
const func = &self.ir_mod.functions.items[self.current_func_idx];
// Failable main: wrap the return in the entry-point reporter
// (ERR E4.2) — exit 0 (or the value) on success, else print the
// trace + tag to stderr and exit 1 — instead of returning the
// tag/tuple as the raw exit code. Two shapes:
// `-> !` → `val` is the bare u32 error tag.
// `-> (int, !)` → `val` is a `{value, tag}` tuple; extract both.
if (self.current_func_is_main) {
const rinfo = self.ir_mod.types.get(func.ret);
if (rinfo == .error_set) {
self.emitFailableMainRet(null, val);
self.advanceRefCounter();
return;
}
if (rinfo == .tuple and rinfo.tuple.fields.len == 2 and
self.ir_mod.types.get(rinfo.tuple.fields[1]) == .error_set)
{
const value = c.LLVMBuildExtractValue(self.builder, val, 0, "main.ret.val");
const tag = c.LLVMBuildExtractValue(self.builder, val, 1, "main.ret.tag");
self.emitFailableMainRet(value, tag);
self.advanceRefCounter();
return;
}
}
// sret-shaped function: declared return-type-in-IR is
// the struct, but the LLVM signature is void with a
// prepended ptr sret param. Store the value through
// the sret slot and emit ret void.
const needs_c_abi = func.is_extern or func.call_conv == .c;
const raw_ret = self.toLLVMType(func.ret);
if (needs_c_abi and self.needsByval(func.ret, raw_ret)) {
const llvm_func2 = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const sret_ptr = c.LLVMGetParam(llvm_func2, 0);
_ = c.LLVMBuildStore(self.builder, val, sret_ptr);
_ = c.LLVMBuildRetVoid(self.builder);
self.advanceRefCounter();
return;
}
// Coerce return value to match the function's LLVM return type
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const fn_ty = c.LLVMGlobalGetValueType(llvm_func);
const expected_ret = c.LLVMGetReturnType(fn_ty);
val = self.coerceArg(val, expected_ret);
// If coercion didn't fix the type (e.g. dead comptime function),
// emit undef of the correct type to avoid LLVM verification error
if (c.LLVMTypeOf(val) != expected_ret) {
val = c.LLVMGetUndef(expected_ret);
}
_ = c.LLVMBuildRet(self.builder, val);
self.advanceRefCounter();
},
.ret_void => {
if (self.current_func_is_main) {
// main must return i32 0 for JIT
_ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 0, 0));
} else {
_ = c.LLVMBuildRetVoid(self.builder);
}
self.advanceRefCounter();
},
.@"unreachable" => {
_ = c.LLVMBuildUnreachable(self.builder);
self.advanceRefCounter();
},
.br => |branch| {
const target = self.getBlock(func_idx, branch.target);
_ = c.LLVMBuildBr(self.builder, target);
self.advanceRefCounter();
},
.cond_br => |cbr| {
var cond = self.resolveRef(cbr.cond);
const then_bb = self.getBlock(func_idx, cbr.then_target);
const else_bb = self.getBlock(func_idx, cbr.else_target);
// Coerce condition to i1 if needed (e.g., loaded bool stored as i64)
const cond_ty = c.LLVMTypeOf(cond);
const cond_kind = c.LLVMGetTypeKind(cond_ty);
if (cond_ty != self.cached_i1) {
if (cond_kind == c.LLVMPointerTypeKind) {
cond = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cond, c.LLVMConstNull(cond_ty), "tobool");
} else if (cond_kind == c.LLVMIntegerTypeKind) {
cond = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cond, c.LLVMConstInt(cond_ty, 0, 0), "tobool");
} else if (cond_kind == c.LLVMStructTypeKind) {
// Struct values are always truthy
cond = c.LLVMConstInt(self.cached_i1, 1, 0);
} else {
cond = c.LLVMConstInt(self.cached_i1, 1, 0); // default truthy
}
}
_ = c.LLVMBuildCondBr(self.builder, cond, then_bb, else_bb);
self.advanceRefCounter();
},
// ── Struct ops ────────────────────────────────────────────
.struct_init => |agg| {
const struct_ty = self.toLLVMType(instruction.ty);
const type_kind = c.LLVMGetTypeKind(struct_ty);
// For vector types, use InsertElement instead of InsertValue
const is_vector = type_kind == c.LLVMVectorTypeKind or type_kind == c.LLVMScalableVectorTypeKind;
// For array types, get expected element type for coercion
const is_array = type_kind == c.LLVMArrayTypeKind;
const elem_llvm_ty = if (is_array) c.LLVMGetElementType(struct_ty) else null;
var result = c.LLVMGetUndef(struct_ty);
for (agg.fields, 0..) |field_ref, i| {
var field_val = self.resolveRef(field_ref);
if (is_vector) {
// Coerce element to match vector element type
const vec_elem_ty = c.LLVMGetElementType(struct_ty);
const val_ty = c.LLVMTypeOf(field_val);
if (val_ty != vec_elem_ty) {
field_val = self.coerceArg(field_val, vec_elem_ty);
}
const idx = c.LLVMConstInt(self.cached_i32, @intCast(i), 0);
result = c.LLVMBuildInsertElement(self.builder, result, field_val, idx, "vi");
} else {
// Coerce element to match array element type if needed
if (elem_llvm_ty) |elt| {
const val_ty = c.LLVMTypeOf(field_val);
if (val_ty != elt) {
const val_kind = c.LLVMGetTypeKind(val_ty);
const elt_kind = c.LLVMGetTypeKind(elt);
if (val_kind == c.LLVMIntegerTypeKind and elt_kind == c.LLVMIntegerTypeKind) {
const val_w = c.LLVMGetIntTypeWidth(val_ty);
const elt_w = c.LLVMGetIntTypeWidth(elt);
if (val_w > elt_w) {
field_val = c.LLVMBuildTrunc(self.builder, field_val, elt, "atrunc");
} else if (val_w < elt_w) {
field_val = c.LLVMBuildZExt(self.builder, field_val, elt, "aext");
}
}
}
} else if (type_kind == c.LLVMStructTypeKind) {
// Coerce struct field value to match declared field type
const n_elts = c.LLVMCountStructElementTypes(struct_ty);
if (n_elts > 0 and i < n_elts) {
const field_ty = c.LLVMStructGetTypeAtIndex(struct_ty, @intCast(i));
const val_ty = c.LLVMTypeOf(field_val);
if (val_ty != field_ty) {
field_val = self.coerceArg(field_val, field_ty);
}
}
}
result = c.LLVMBuildInsertValue(self.builder, result, field_val, @intCast(i), "si");
}
}
self.mapRef(result);
},
.struct_get => |fa| {
const base = self.resolveRef(fa.base);
// Safety: null base means unresolved reference — emit undef
if (base == null) {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
} else {
// Safety: check that base is an aggregate type (struct/array/vector), not scalar
const base_ty = c.LLVMTypeOf(base);
const base_ty_kind = c.LLVMGetTypeKind(base_ty);
if (base_ty_kind == c.LLVMVectorTypeKind or base_ty_kind == c.LLVMScalableVectorTypeKind) {
// Vector: use ExtractElement with an index
const idx = c.LLVMConstInt(self.cached_i32, @intCast(fa.field_index), 0);
const result = c.LLVMBuildExtractElement(self.builder, base, idx, "ve");
self.mapRef(result);
} else if (base_ty_kind == c.LLVMStructTypeKind or base_ty_kind == c.LLVMArrayTypeKind) {
// Validate field index is in bounds
const n_fields = if (base_ty_kind == c.LLVMStructTypeKind) c.LLVMCountStructElementTypes(base_ty) else 0;
// Check builder has valid insert point
const insert_bb = c.LLVMGetInsertBlock(self.builder);
if (insert_bb == null or (n_fields == 0 and base_ty_kind == c.LLVMStructTypeKind) or (n_fields > 0 and fa.field_index >= n_fields)) {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
} else {
const result = c.LLVMBuildExtractValue(self.builder, base, @intCast(fa.field_index), "sg");
self.mapRef(result);
}
} else {
// Base is not an aggregate (e.g., placeholder undef of scalar type)
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
}
}
},
.struct_gep => |fa| {
const base_ptr = self.resolveRef(fa.base);
// Safety: verify base is a pointer before GEP
const base_ty_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(base_ptr));
if (base_ty_kind == c.LLVMPointerTypeKind) {
const struct_llvm_ty = if (fa.base_type) |bt|
self.toLLVMType(self.resolveAggregate(bt))
else
self.resolveGepStructType(fa.base, instruction);
const st_kind = c.LLVMGetTypeKind(struct_llvm_ty);
if (st_kind == c.LLVMStructTypeKind or st_kind == c.LLVMArrayTypeKind) {
const result = c.LLVMBuildStructGEP2(self.builder, struct_llvm_ty, base_ptr, @intCast(fa.field_index), "gep");
self.mapRef(result);
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
},
// ── Enum ops ─────────────────────────────────────────────
.enum_init => |ei| {
if (ei.payload.isNone()) {
// Simple enum (no payload) — just a tag integer
const ty = self.toLLVMType(instruction.ty);
const ty_kind = c.LLVMGetTypeKind(ty);
if (ty_kind == c.LLVMIntegerTypeKind) {
// Plain enum or builtin integer → integer constant
self.mapRef(c.LLVMConstInt(ty, ei.tag, 0));
} else if (ty_kind == c.LLVMStructTypeKind) {
// Tagged union with no payload — header field 0 holds the tag
const header_ty = c.LLVMStructGetTypeAtIndex(ty, 0);
const tag_val = c.LLVMConstInt(header_ty, ei.tag, 0);
var result = c.LLVMGetUndef(ty);
result = c.LLVMBuildInsertValue(self.builder, result, tag_val, 0, "ei.tag");
self.mapRef(result);
} else {
self.mapRef(c.LLVMConstInt(self.cached_i64, ei.tag, 0));
}
} else {
// Tagged union with payload — { header, payload_bytes }
const union_ty = self.toLLVMType(instruction.ty);
const header_ty = c.LLVMStructGetTypeAtIndex(union_ty, 0);
const tag_val = c.LLVMConstInt(header_ty, ei.tag, 0);
const payload_val = self.resolveRef(ei.payload);
// alloca union, store tag, bitcast payload area, store payload
const tmp = c.LLVMBuildAlloca(self.builder, union_ty, "ei.tmp");
// Store tag at field 0
const tag_ptr = c.LLVMBuildStructGEP2(self.builder, union_ty, tmp, 0, "ei.tagp");
_ = c.LLVMBuildStore(self.builder, tag_val, tag_ptr);
// Store payload at field 1 (bitcast the byte array to payload type)
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, union_ty, tmp, 1, "ei.pp");
const payload_typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ei.pcast");
_ = c.LLVMBuildStore(self.builder, payload_val, payload_typed_ptr);
// Load the whole union value
self.mapRef(c.LLVMBuildLoad2(self.builder, union_ty, tmp, "ei.val"));
}
},
.enum_tag => |un| {
const val = self.resolveRef(un.operand);
// Check if this is a plain enum (integer) or tagged union (struct with tag at 0)
const val_ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMStructTypeKind) {
// Tagged union — extract field 0 (tag)
var tag = c.LLVMBuildExtractValue(self.builder, val, 0, "etag");
// Truncate to declared tag width if needed (e.g. i64 → i32 for u32 tags)
// This is essential for FFI unions where the i64 tag slot contains
// a smaller tag + uninitialized padding (e.g. SDL_Event's u32 type + u32 reserved)
const target_ty = self.toLLVMType(instruction.ty);
const extracted_bits = c.LLVMGetIntTypeWidth(c.LLVMTypeOf(tag));
const target_bits = c.LLVMGetIntTypeWidth(target_ty);
if (target_bits < extracted_bits) {
tag = c.LLVMBuildTrunc(self.builder, tag, target_ty, "etag.trunc");
}
self.mapRef(tag);
} else {
// Plain enum — the value IS the tag
self.mapRef(val);
}
},
.enum_payload => |fa| {
const base = self.resolveRef(fa.base);
const result_ty = self.toLLVMType(instruction.ty);
const base_ty = c.LLVMTypeOf(base);
const base_kind = c.LLVMGetTypeKind(base_ty);
if (base_kind == c.LLVMStructTypeKind) {
// Tagged union: alloca, store, GEP field 1 (payload area), bitcast, load
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ep.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "ep.pp");
const typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ep.cast");
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, typed_ptr, "ep.val"));
} else {
self.mapRef(c.LLVMGetUndef(result_ty));
}
},
// ── Union ops ────────────────────────────────────────────
.union_get => |fa| {
const base = self.resolveRef(fa.base);
const result_ty = self.toLLVMType(instruction.ty);
// Union field access: reinterpret the union's data area as the target type
const base_ty = c.LLVMTypeOf(base);
const kind = c.LLVMGetTypeKind(base_ty);
if (kind == c.LLVMStructTypeKind) {
// Tagged union { header, payload_bytes } — access payload at field 1
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ug.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "ug.pp");
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, payload_ptr, "ug.val"));
} else {
// Untagged union [N x i8] — alloca, store, reinterpret-load
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ug.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, tmp, "ug.val"));
}
},
.union_gep => |fa| {
const base_ptr = self.resolveRef(fa.base);
const base_ty_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(base_ptr));
if (base_ty_kind == c.LLVMPointerTypeKind) {
const union_llvm_ty = if (fa.base_type) |bt|
self.toLLVMType(self.resolveAggregate(bt))
else
self.resolveGepStructType(fa.base, instruction);
const st_kind = c.LLVMGetTypeKind(union_llvm_ty);
if (st_kind == c.LLVMStructTypeKind) {
// Tagged union — payload is at field 1
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, union_llvm_ty, base_ptr, 1, "ugep.pp");
self.mapRef(payload_ptr);
} else {
// Untagged union — data starts at offset 0
self.mapRef(base_ptr);
}
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
},
// ── Array/Slice ops ───────────────────────────────────────
.index_get => |bin| {
const base = self.resolveRef(bin.lhs);
const idx = self.resolveRef(bin.rhs);
const base_ty = c.LLVMTypeOf(base);
const kind = c.LLVMGetTypeKind(base_ty);
if (kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) {
// Vector — use extractelement
// Coerce index to i32 if needed
const idx32 = self.coerceArg(idx, self.cached_i32);
self.mapRef(c.LLVMBuildExtractElement(self.builder, base, idx32, "ve"));
} else if (kind == c.LLVMArrayTypeKind) {
// Fixed-size array value — alloca, store, GEP, load
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ig.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
const elem_ty = self.toLLVMType(instruction.ty);
var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), idx };
const ptr = c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "ig.ptr");
self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val"));
} else if (kind == c.LLVMPointerTypeKind) {
// Pointer (many-pointer or raw ptr) — GEP + load
const elem_ty = self.toLLVMType(instruction.ty);
var indices = [_]c.LLVMValueRef{idx};
const ptr = c.LLVMBuildGEP2(self.builder, elem_ty, base, &indices, 1, "ig.ptr");
self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val"));
} else if (kind == c.LLVMStructTypeKind) {
// Slice/string {ptr, len} — extract ptr, GEP, load
const data = c.LLVMBuildExtractValue(self.builder, base, 0, "ig.data");
const elem_ty = self.toLLVMType(instruction.ty);
var indices = [_]c.LLVMValueRef{idx};
const ptr = c.LLVMBuildGEP2(self.builder, elem_ty, data, &indices, 1, "ig.ptr");
self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val"));
} else {
// Non-aggregate base (lowering error) — emit undef
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
}
},
.index_gep => |bin| {
const base = self.resolveRef(bin.lhs);
const idx = self.resolveRef(bin.rhs);
const base_ty = c.LLVMTypeOf(base);
const kind = c.LLVMGetTypeKind(base_ty);
if (kind == c.LLVMArrayTypeKind) {
// Fixed-size array value — alloca, store, GEP
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "igp.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), idx };
self.mapRef(c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "igp.ptr"));
} else if (kind == c.LLVMPointerTypeKind) {
// Pointer — GEP with proper element type
const gep_elem = blk: {
// instruction.ty is the result type (ptr to element)
// Resolve the pointee type for the GEP element size
const info = self.ir_mod.types.get(instruction.ty);
break :blk switch (info) {
.pointer => |p| self.toLLVMType(p.pointee),
.many_pointer => |p| self.toLLVMType(p.element),
else => self.cached_i8, // fallback
};
};
var indices = [_]c.LLVMValueRef{idx};
self.mapRef(c.LLVMBuildGEP2(self.builder, gep_elem, base, &indices, 1, "igp.ptr"));
} else if (kind == c.LLVMStructTypeKind) {
// Slice/string {ptr, len} — extract ptr, GEP with proper element type
const data = c.LLVMBuildExtractValue(self.builder, base, 0, "igp.data");
const gep_elem = blk: {
const info = self.ir_mod.types.get(instruction.ty);
break :blk switch (info) {
.pointer => |p| self.toLLVMType(p.pointee),
.many_pointer => |p| self.toLLVMType(p.element),
else => self.cached_i8,
};
};
var indices = [_]c.LLVMValueRef{idx};
self.mapRef(c.LLVMBuildGEP2(self.builder, gep_elem, data, &indices, 1, "igp.ptr"));
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
},
.length => |un| {
const val = self.resolveRef(un.operand);
const val_ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMArrayTypeKind) {
const len = c.LLVMGetArrayLength2(val_ty);
self.mapRef(c.LLVMConstInt(self.cached_i64, len, 0));
} else if (kind == c.LLVMStructTypeKind) {
// Slice/string {ptr, len} — extract field 1 (len)
self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 1, "len"));
} else {
self.mapRef(c.LLVMGetUndef(self.cached_i64));
}
},
.data_ptr => |un| {
const val = self.resolveRef(un.operand);
const val_ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMStructTypeKind) {
self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 0, "dptr"));
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
}
},
.subslice => |ss| {
const base = self.resolveRef(ss.base);
var lo = self.resolveRef(ss.lo);
var hi = self.resolveRef(ss.hi);
// Normalize lo/hi to i64 for consistent arithmetic (indices are unsigned)
if (c.LLVMTypeOf(lo) != self.cached_i64) {
lo = c.LLVMBuildZExt(self.builder, lo, self.cached_i64, "ss.lo64");
}
if (c.LLVMTypeOf(hi) != self.cached_i64) {
hi = c.LLVMBuildZExt(self.builder, hi, self.cached_i64, "ss.hi64");
}
const base_ty = c.LLVMTypeOf(base);
const base_kind = c.LLVMGetTypeKind(base_ty);
const slice_ty = self.toLLVMType(instruction.ty);
// Resolve element type from the result slice type for correct GEP stride
const elem_ty = blk: {
const info = self.ir_mod.types.get(instruction.ty);
break :blk switch (info) {
.slice => |s| self.toLLVMType(s.element),
else => self.cached_i8,
};
};
if (base_kind == c.LLVMStructTypeKind) {
// Slice/string: extract data ptr, GEP by lo
const data = c.LLVMBuildExtractValue(self.builder, base, 0, "ss.data");
var lo_indices = [_]c.LLVMValueRef{lo};
const new_ptr = c.LLVMBuildGEP2(self.builder, elem_ty, data, &lo_indices, 1, "ss.ptr");
var new_len = c.LLVMBuildSub(self.builder, hi, lo, "ss.len");
// Ensure length is i64 for slice struct {ptr, i64}
if (c.LLVMTypeOf(new_len) != self.cached_i64) {
new_len = c.LLVMBuildSExt(self.builder, new_len, self.cached_i64, "ss.ext");
}
var result = c.LLVMGetUndef(slice_ty);
result = c.LLVMBuildInsertValue(self.builder, result, new_ptr, 0, "ss.wptr");
result = c.LLVMBuildInsertValue(self.builder, result, new_len, 1, "ss.wlen");
self.mapRef(result);
} else if (base_kind == c.LLVMArrayTypeKind) {
// Array: alloca, GEP to element at lo, compute len
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ss.arr");
_ = c.LLVMBuildStore(self.builder, base, tmp);
var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), lo };
const new_ptr = c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "ss.ptr");
var new_len = c.LLVMBuildSub(self.builder, hi, lo, "ss.len");
// Ensure length is i64 for slice struct {ptr, i64}
if (c.LLVMTypeOf(new_len) != self.cached_i64) {
new_len = c.LLVMBuildSExt(self.builder, new_len, self.cached_i64, "ss.ext");
}
var result = c.LLVMGetUndef(slice_ty);
result = c.LLVMBuildInsertValue(self.builder, result, new_ptr, 0, "ss.wptr");
result = c.LLVMBuildInsertValue(self.builder, result, new_len, 1, "ss.wlen");
self.mapRef(result);
} else {
self.mapRef(c.LLVMGetUndef(slice_ty));
}
},
.array_to_slice => |un| {
const arr = self.resolveRef(un.operand);
const arr_ty = c.LLVMTypeOf(arr);
const arr_kind = c.LLVMGetTypeKind(arr_ty);
if (arr_kind == c.LLVMArrayTypeKind) {
const len = c.LLVMGetArrayLength2(arr_ty);
const tmp = c.LLVMBuildAlloca(self.builder, arr_ty, "a2s.tmp");
_ = c.LLVMBuildStore(self.builder, arr, tmp);
var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), c.LLVMConstInt(self.cached_i64, 0, 0) };
const elem_ptr = c.LLVMBuildGEP2(self.builder, arr_ty, tmp, &indices, 2, "a2s.ptr");
const slice_ty = self.toLLVMType(instruction.ty);
var result = c.LLVMGetUndef(slice_ty);
result = c.LLVMBuildInsertValue(self.builder, result, elem_ptr, 0, "a2s.wptr");
const len_val = c.LLVMConstInt(self.cached_i64, len, 0);
result = c.LLVMBuildInsertValue(self.builder, result, len_val, 1, "a2s.wlen");
self.mapRef(result);
} else {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
}
},
// ── Call extensions ───────────────────────────────────────
.call_builtin => |bi| {
// Builtins that map to libc functions or LLVM intrinsics
switch (bi.builtin) {
.sqrt, .sin, .cos, .floor => {
const val = self.resolveRef(bi.args[0]);
const val_ty = c.LLVMTypeOf(val);
const val_kind = c.LLVMGetTypeKind(val_ty);
if (val_kind == c.LLVMFloatTypeKind) {
const f = self.getOrDeclareMathF32(bi.builtin);
var args = [_]c.LLVMValueRef{val};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF32Type(), f, &args, 1, @tagName(bi.builtin)));
} else {
const coerced = if (val_kind != c.LLVMDoubleTypeKind) self.coerceArg(val, self.cached_f64) else val;
const f = self.getOrDeclareMathF64(bi.builtin);
var args = [_]c.LLVMValueRef{coerced};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF64Type(), f, &args, 1, @tagName(bi.builtin)));
}
},
.out => {
// out(str): extract ptr and len from string fat pointer, call write(1, ptr, len)
const str_val = self.resolveRef(bi.args[0]);
const raw_ptr = c.LLVMBuildExtractValue(self.builder, str_val, 0, "str.ptr");
const str_len = c.LLVMBuildExtractValue(self.builder, str_val, 1, "str.len");
// On wasm32, count param is i32 (size_t)
const count = if (self.target_config.isWasm32())
c.LLVMBuildTrunc(self.builder, str_len, self.cached_i32, "len.tr")
else
str_len;
const write_fn = self.getOrDeclareWrite();
var write_args = [_]c.LLVMValueRef{
c.LLVMConstInt(self.cached_i32, 1, 0), // fd = stdout
raw_ptr,
count,
};
_ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, "");
self.advanceRefCounter();
},
.type_name => {
// Dynamic `type_name(t)` at runtime: extract
// the TypeId from the arg (an Any-boxed Type
// value: tag=`.s64.index()`, value=tid), GEP
// into the compiler-emitted `__sx_type_names`
// global, load the string. The arg's LLVM
// shape is the `{i64, i64}` Any aggregate
// (because the IR-side arg type is `.any`
// when boxed); for unboxed direct call sites
// (the arg IR type is `.s64` from
// `const_type`), the value IS the TypeId
// index directly.
const arg_ref = bi.args[0];
const arg_val = self.resolveRef(arg_ref);
const arg_ir_ty = self.getRefIRType(arg_ref) orelse @import("types.zig").TypeId.s64;
const tid_idx = blk: {
if (arg_ir_ty == .any) {
// Boxed: extract value field.
break :blk c.LLVMBuildExtractValue(self.builder, arg_val, 1, "tn.tid");
}
// Bare i64 (TypeId index).
break :blk arg_val;
};
const arr_global = self.getOrBuildTypeNameArray();
const arr_len = self.type_name_array_len;
const string_ty = self.getStringStructType();
const arr_ty = c.LLVMArrayType(string_ty, arr_len);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, tid_idx };
const gep = c.LLVMBuildInBoundsGEP2(self.builder, arr_ty, arr_global, &indices, 2, "tn.gep");
const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "tn.load");
self.mapRef(result);
},
.type_eq => {
// Dynamic `type_eq(a, b)` — both args are
// Type values. Extract TypeId from each Any
// box (or use directly if `.s64`-typed),
// icmp eq.
const a = blk: {
const v = self.resolveRef(bi.args[0]);
const ty = self.getRefIRType(bi.args[0]) orelse @import("types.zig").TypeId.s64;
if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.a");
break :blk v;
};
const b = blk: {
const v = self.resolveRef(bi.args[1]);
const ty = self.getRefIRType(bi.args[1]) orelse @import("types.zig").TypeId.s64;
if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.b");
break :blk v;
};
const eq_res = c.LLVMBuildICmp(self.builder, c.LLVMIntEQ, a, b, "te.eq");
self.mapRef(eq_res);
},
.has_impl => {
// Runtime has_impl needs a protocol-map
// snapshot — not wired yet. Silent false for
// now; the lower-time fold via
// `tryConstBoolCondition` covers every
// statically-resolvable call.
self.mapRef(c.LLVMConstInt(self.cached_i1, 0, 0));
},
else => {
// size_of, cast — handled by lowering or codegen glue
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
},
}
},
.compiler_call => {
// Compiler hooks are comptime-only; if one reaches emission, produce undef
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
},
.call_closure => |call_op| {
// Closure: { fn_ptr, env }.
//
// ABI (when module.has_implicit_ctx):
// trampoline signature: (__sx_ctx, env, args...)
// call_op.args[0] = __sx_ctx (prepended by lowering)
// call_op.args[1..] = user args
// extracted env_ptr = inserted at LLVM slot 1
//
// ABI (without implicit_ctx):
// trampoline signature: (env, args...)
// call_op.args = user args (no ctx prepend)
// extracted env_ptr = inserted at LLVM slot 0
const closure = self.resolveRef(call_op.callee);
const cl_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(closure));
if (cl_kind != c.LLVMStructTypeKind) {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
}
const fn_ptr = c.LLVMBuildExtractValue(self.builder, closure, 0, "cl.fn");
const env_ptr = c.LLVMBuildExtractValue(self.builder, closure, 1, "cl.env");
// Get the closure's declared parameter types from the IR type system
const callee_ir_ty = self.getRefIRType(call_op.callee);
const closure_params: ?[]const @import("types.zig").TypeId = if (callee_ir_ty) |cty| blk: {
if (!cty.isBuiltin()) {
const ci = self.ir_mod.types.get(cty);
if (ci == .closure) break :blk ci.closure.params;
}
break :blk null;
} else null;
const has_ctx = self.ir_mod.has_implicit_ctx;
const user_args_offset_in_op: usize = if (has_ctx) 1 else 0;
const user_args_count: usize = call_op.args.len -| user_args_offset_in_op;
const ctx_slots: usize = if (has_ctx) 1 else 0;
const total_args = ctx_slots + 1 + user_args_count; // [ctx?] + env + user_args
const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable;
defer self.alloc.free(args);
if (has_ctx) {
args[0] = self.resolveRef(call_op.args[0]); // ctx
}
args[ctx_slots] = env_ptr;
for (0..user_args_count) |j| {
args[ctx_slots + 1 + j] = self.resolveRef(call_op.args[user_args_offset_in_op + j]);
}
// Build function type using declared param types (not arg types).
// closure_params is user-visible (no ctx, no env), so they line
// up with args[ctx_slots+1..].
const ret_ty = self.toLLVMType(instruction.ty);
const param_tys = self.alloc.alloc(c.LLVMTypeRef, total_args) catch unreachable;
defer self.alloc.free(param_tys);
if (has_ctx) param_tys[0] = self.cached_ptr; // __sx_ctx
param_tys[ctx_slots] = self.cached_ptr; // env
if (closure_params) |cp| {
for (0..user_args_count) |j| {
const param_ir_ty = if (j < cp.len) cp[j] else null;
if (param_ir_ty) |pty| {
const llvm_pty = self.toLLVMType(pty);
param_tys[ctx_slots + 1 + j] = llvm_pty;
args[ctx_slots + 1 + j] = self.coerceArg(args[ctx_slots + 1 + j], llvm_pty);
} else {
param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]);
}
}
} else {
for (0..user_args_count) |j| {
param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]);
}
}
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, @intCast(total_args), 0);
const is_void = instruction.ty == .void;
const result = c.LLVMBuildCall2(self.builder, fn_ty, fn_ptr, args.ptr, @intCast(total_args), if (is_void) "" else "ccall");
if (!is_void) {
self.mapRef(result);
} else {
self.advanceRefCounter();
}
},
// ── Tuple ops ────────────────────────────────────────────
.tuple_init => |agg| {
const tuple_ty = self.toLLVMType(instruction.ty);
var result = c.LLVMGetUndef(tuple_ty);
for (agg.fields, 0..) |field_ref, i| {
const field_val = self.resolveRef(field_ref);
result = c.LLVMBuildInsertValue(self.builder, result, field_val, @intCast(i), "ti");
}
self.mapRef(result);
},
.tuple_get => |fa| {
const base = self.resolveRef(fa.base);
self.mapRef(c.LLVMBuildExtractValue(self.builder, base, @intCast(fa.field_index), "tg"));
},
// ── Optional ops ─────────────────────────────────────────
.optional_wrap => |un| {
var val = self.resolveRef(un.operand);
const opt_ty = self.toLLVMType(instruction.ty);
const opt_kind = c.LLVMGetTypeKind(opt_ty);
if (opt_kind == c.LLVMPointerTypeKind) {
// ?*T — pointer is the optional itself (null = none)
self.mapRef(val);
} else if (opt_kind == c.LLVMStructTypeKind) {
// Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure)
const num_fields = c.LLVMCountStructElementTypes(opt_ty);
const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(opt_ty, num_fields - 1) else self.cached_i1;
if (last_field_ty == self.cached_i1) {
// ?T → { T, i1 } — wrap value + true flag
const inner_ty = c.LLVMStructGetTypeAtIndex(opt_ty, 0);
val = self.coerceArg(val, inner_ty);
var result = c.LLVMGetUndef(opt_ty);
result = c.LLVMBuildInsertValue(self.builder, result, val, 0, "ow.val");
result = c.LLVMBuildInsertValue(self.builder, result, c.LLVMConstInt(self.cached_i1, 1, 0), 1, "ow.has");
self.mapRef(result);
} else {
// ?Closure → closure struct IS the optional, just pass through
self.mapRef(val);
}
} else {
self.mapRef(val);
}
},
.optional_unwrap => |un| {
const val = self.resolveRef(un.operand);
const val_ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMStructTypeKind) {
// Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure)
const num_fields = c.LLVMCountStructElementTypes(val_ty);
const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(val_ty, num_fields - 1) else self.cached_i1;
if (last_field_ty == self.cached_i1) {
// { T, i1 } → extract field 0
self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 0, "ou.val"));
} else {
// ?Closure → the struct itself is the value
self.mapRef(val);
}
} else {
// ?*T → pointer is the value itself
self.mapRef(val);
}
},
.optional_has_value => |un| {
const val = self.resolveRef(un.operand);
const val_ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMStructTypeKind) {
// Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure)
const num_fields = c.LLVMCountStructElementTypes(val_ty);
const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(val_ty, num_fields - 1) else self.cached_i1;
if (last_field_ty == self.cached_i1) {
// { T, i1 } → extract has_value flag
self.mapRef(c.LLVMBuildExtractValue(self.builder, val, num_fields - 1, "oh.has"));
} else {
// ?Closure {fn_ptr, env} → check if fn_ptr is null
const fn_ptr = c.LLVMBuildExtractValue(self.builder, val, 0, "oh.fn");
self.mapRef(c.LLVMBuildICmp(self.builder, c.LLVMIntNE, fn_ptr, c.LLVMConstNull(c.LLVMTypeOf(fn_ptr)), "oh.nn"));
}
} else {
// ?*T → compare with null
const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, val, c.LLVMConstNull(val_ty), "oh.nn");
self.mapRef(is_nonnull);
}
},
.optional_coalesce => |bin| {
// a ?? b — if a has value, use a's value; otherwise use b
const a = self.resolveRef(bin.lhs);
var b_val = self.resolveRef(bin.rhs);
const a_ty = c.LLVMTypeOf(a);
const kind = c.LLVMGetTypeKind(a_ty);
if (kind == c.LLVMStructTypeKind) {
const n_fields = c.LLVMCountStructElementTypes(a_ty);
const f1_ty = if (n_fields >= 2) c.LLVMStructGetTypeAtIndex(a_ty, 1) else null;
const is_ti1 = if (f1_ty) |ft| c.LLVMGetTypeKind(ft) == c.LLVMIntegerTypeKind and c.LLVMGetIntTypeWidth(ft) == 1 else false;
if (is_ti1) {
// Standard optional {T, i1}: extract has_value and unwrap
const has = c.LLVMBuildExtractValue(self.builder, a, 1, "oc.has");
const unwrapped = c.LLVMBuildExtractValue(self.builder, a, 0, "oc.val");
const uw_ty = c.LLVMTypeOf(unwrapped);
const b_ty = c.LLVMTypeOf(b_val);
if (uw_ty != b_ty) {
b_val = self.coerceArg(b_val, uw_ty);
}
self.mapRef(c.LLVMBuildSelect(self.builder, has, unwrapped, b_val, "oc.sel"));
} else {
// ?Closure {fn_ptr, env}: check if fn_ptr is null
const fn_ptr = c.LLVMBuildExtractValue(self.builder, a, 0, "oc.fn");
const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, fn_ptr, c.LLVMConstNull(c.LLVMTypeOf(fn_ptr)), "oc.nn");
// Select the full closure struct, not just the fn_ptr
self.mapRef(c.LLVMBuildSelect(self.builder, is_nonnull, a, b_val, "oc.sel"));
}
} else {
// ?*T — select on null
const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, a, c.LLVMConstNull(a_ty), "oc.nn");
self.mapRef(c.LLVMBuildSelect(self.builder, is_nonnull, a, b_val, "oc.sel"));
}
},
// ── Box/Unbox Any ────────────────────────────────────────
.box_any => |ba| {
const val = self.resolveRef(ba.operand);
const any_ty = self.getAnyStructType();
// Any = { type_tag: i64, value: i64 }
const tag = c.LLVMConstInt(self.cached_i64, self.anyTag(ba.source_type), 0);
// Bitcast value to i64, using SExt for signed types, ZExt otherwise
const is_signed = self.isSignedTypeEx(ba.source_type);
const val_as_i64 = if (is_signed) self.coerceToI64Signed(val) else self.coerceToI64(val);
var result = c.LLVMGetUndef(any_ty);
result = c.LLVMBuildInsertValue(self.builder, result, tag, 0, "ba.tag");
result = c.LLVMBuildInsertValue(self.builder, result, val_as_i64, 1, "ba.val");
self.mapRef(result);
},
.unbox_any => |un| {
const any_val = self.resolveRef(un.operand);
const any_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(any_val));
if (any_kind == c.LLVMStructTypeKind) {
const raw = c.LLVMBuildExtractValue(self.builder, any_val, 1, "ua.raw");
const target_ty = self.toLLVMType(instruction.ty);
self.mapRef(self.coerceFromI64(raw, target_ty));
} else {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
}
},
// ── Reflection ops ──────────────────────────────────────
.field_name_get => |fr| {
// Build global string array for this struct's field names, then GEP at runtime index
const global = self.getOrBuildFieldNameArray(fr.struct_type);
const idx = self.resolveRef(fr.index);
const string_ty = self.getStringStructType();
// Get struct field count for array type
const field_info = self.ir_mod.types.get(fr.struct_type);
const field_count: u32 = switch (field_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),
else => 0,
};
const array_ty = c.LLVMArrayType(string_ty, field_count);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, idx };
const gep = c.LLVMBuildInBoundsGEP2(self.builder, array_ty, global, &indices, 2, "fn.gep");
const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "fn.load");
self.mapRef(result);
},
.field_value_get => |fr| {
// Switch on index, each case: extractvalue field k → box as Any
self.emitFieldValueGet(fr, func_idx);
},
.error_tag_name_get => |u| {
// Tag id → name: GEP into the always-linked tag-name table at
// the runtime tag id (the error-set value, a u32). Out-of-range
// ids can't occur — ids come from the same registry the table
// is built from — so no bounds branch is needed.
const global = self.getOrBuildTagNameArray();
const tag_raw = self.resolveRef(u.operand);
const idx = c.LLVMBuildZExt(self.builder, tag_raw, self.cached_i64, "etn.idx");
const string_ty = self.getStringStructType();
const n: u32 = @intCast(self.ir_mod.types.tags.names.items.len);
const array_ty = c.LLVMArrayType(string_ty, n);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, idx };
const gep = c.LLVMBuildInBoundsGEP2(self.builder, array_ty, global, &indices, 2, "etn.gep");
const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "etn.load");
self.mapRef(result);
},
// ── Switch branch ────────────────────────────────────────
.switch_br => |sw| {
const operand = self.resolveRef(sw.operand);
const default_bb = self.getBlock(func_idx, sw.default);
const switch_inst = c.LLVMBuildSwitch(self.builder, operand, default_bb, @intCast(sw.cases.len));
for (sw.cases) |case| {
const case_val = c.LLVMConstInt(c.LLVMTypeOf(operand), @bitCast(case.value), 0);
const case_bb = self.getBlock(func_idx, case.target);
c.LLVMAddCase(switch_inst, case_val, case_bb);
}
self.advanceRefCounter();
},
// ── Closure creation ─────────────────────────────────────
.closure_create => |cc| {
const fn_val = self.func_map.get(cc.func.index()) orelse c.LLVMGetUndef(self.cached_ptr);
const env_val = if (cc.env.isNone()) c.LLVMConstNull(self.cached_ptr) else self.resolveRef(cc.env);
const closure_ty = self.getClosureStructType();
var result = c.LLVMGetUndef(closure_ty);
result = c.LLVMBuildInsertValue(self.builder, result, fn_val, 0, "cc.fn");
result = c.LLVMBuildInsertValue(self.builder, result, env_val, 1, "cc.env");
self.mapRef(result);
},
// ── Vector ops ───────────────────────────────────────────
.vec_splat => |un| {
const scalar = self.resolveRef(un.operand);
const vec_ty = self.toLLVMType(instruction.ty);
const vec_len = c.LLVMGetVectorSize(vec_ty);
// Build a splat: insertelement into undef for each lane
var result = c.LLVMGetUndef(vec_ty);
var i: c_uint = 0;
while (i < vec_len) : (i += 1) {
const idx_val = c.LLVMConstInt(self.cached_i32, i, 0);
result = c.LLVMBuildInsertElement(self.builder, result, scalar, idx_val, "splat");
}
self.mapRef(result);
},
.vec_extract => |bin| {
const vec = self.resolveRef(bin.lhs);
const idx = self.resolveRef(bin.rhs);
self.mapRef(c.LLVMBuildExtractElement(self.builder, vec, idx, "vext"));
},
.vec_insert => |tri| {
const vec = self.resolveRef(tri.a);
const idx = self.resolveRef(tri.b);
const val = self.resolveRef(tri.c);
self.mapRef(c.LLVMBuildInsertElement(self.builder, vec, val, idx, "vins"));
},
// ── Block params ─────────────────────────────────────────
.block_param => |bp| {
// Create a PHI node — incoming values are filled in by fixupPhiNodes
const ty = self.toLLVMType(instruction.ty);
const phi = c.LLVMBuildPhi(self.builder, ty, "bp");
self.pending_phis.append(self.alloc, .{
.phi = phi,
.block_id = bp.block,
.param_index = bp.param_index,
}) catch unreachable;
self.mapRef(phi);
},
// ── Misc ─────────────────────────────────────────────────
.placeholder => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
},
}
}
// ── Ref tracking ────────────────────────────────────────────────
fn mapRef(self: *LLVMEmitter, val: c.LLVMValueRef) void {
self.ref_map.put(self.ref_counter, val) catch unreachable;
self.ref_counter += 1;
}
fn advanceRefCounter(self: *LLVMEmitter) void {
self.ref_counter += 1;
}
fn resolveRef(self: *LLVMEmitter, ref: Ref) c.LLVMValueRef {
if (ref.isNone()) {
return c.LLVMGetUndef(self.cached_i64);
}
return self.ref_map.get(ref.index()) orelse c.LLVMGetUndef(self.cached_i64);
}
fn getBlock(self: *LLVMEmitter, func_idx: u32, block_id: BlockId) c.LLVMBasicBlockRef {
const key = makeBlockKey(func_idx, block_id.index());
return self.block_map.get(key) orelse {
std.debug.print("getBlock: missing block func={d} block={d}\n", .{ func_idx, block_id.index() });
unreachable;
};
}
// ── Struct/union GEP helper ────────────────────────────────────────
/// For struct_gep/union_gep: we need the LLVM type of the aggregate being pointed to.
/// The instruction's type is the *result* (pointer to field), so we need to look at
/// the IR instruction that produced the base pointer to find the aggregate type.
/// As a fallback, we scan back through the ref_map to find the alloca type.
fn getStructTypeForGep(self: *LLVMEmitter, instruction: *const Inst) c.LLVMTypeRef {
// For GEP, the base ref points to an alloca or another pointer.
// The instruction type is a pointer type (result of GEP), but we need the
// aggregate type. We get it from the base pointer's allocated type.
const fa = switch (instruction.op) {
.struct_gep => |f| f,
.union_gep => |f| f,
else => unreachable,
};
const base_val = self.resolveRef(fa.base);
// LLVMGetAllocatedType only works on alloca instructions
if (c.LLVMIsAAllocaInst(base_val) != null) {
const alloc_ty = c.LLVMGetAllocatedType(base_val);
if (alloc_ty != null) return alloc_ty;
}
// Fallback: trace LLVM value chain — if base came from a load,
// check the load's source pointer for an alloca
if (c.LLVMIsALoadInst(base_val) != null) {
const load_ptr = c.LLVMGetOperand(base_val, 0);
if (load_ptr != null and c.LLVMIsAAllocaInst(load_ptr) != null) {
const inner_alloc = c.LLVMGetAllocatedType(load_ptr);
if (inner_alloc != null) return inner_alloc;
}
}
// Fallback: look up the IR type of the base ref to find the pointee type
const base_ir_ty = self.getRefIRType(fa.base);
if (base_ir_ty) |ir_ty| {
if (!ir_ty.isBuiltin()) {
const info = self.ir_mod.types.get(ir_ty);
switch (info) {
.pointer => |p| return self.toLLVMType(p.pointee),
else => return self.toLLVMType(ir_ty),
}
}
}
return self.cached_i64;
}
/// Resolve the struct LLVM type for GEP operations.
/// Uses LLVM alloca type when available, falls back to IR type system.
fn resolveGepStructType(self: *LLVMEmitter, base_ref: Ref, instruction: *const Inst) c.LLVMTypeRef {
const base_val = self.resolveRef(base_ref);
// Strategy 1: base is an alloca — get allocated type directly
if (c.LLVMIsAAllocaInst(base_val) != null) {
const alloc_ty = c.LLVMGetAllocatedType(base_val);
if (alloc_ty != null) {
const kind = c.LLVMGetTypeKind(alloc_ty);
if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind) return alloc_ty;
}
}
// Strategy 2: Use IR type system — most accurate for chained GEPs (e.g. union_gep + struct_gep)
const base_ir_ty = self.getRefIRType(base_ref);
if (base_ir_ty) |ir_ty| {
// Resolve through pointer types to find the pointee struct
var resolved = ir_ty;
if (!resolved.isBuiltin()) {
const info = self.ir_mod.types.get(resolved);
if (info == .pointer) {
resolved = info.pointer.pointee;
}
}
if (!resolved.isBuiltin()) {
return self.toLLVMType(resolved);
}
}
// Strategy 3: base is a GEP result — get the source element type
if (c.LLVMIsAGetElementPtrInst(base_val) != null) {
const src_ty = c.LLVMGetGEPSourceElementType(base_val);
if (src_ty != null) {
const kind = c.LLVMGetTypeKind(src_ty);
if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind) return src_ty;
}
}
// Strategy 4: old fallback
_ = instruction;
return self.cached_i64;
}
/// Resolve through pointer types to get the underlying aggregate type.
fn resolveAggregate(self: *LLVMEmitter, ty: TypeId) TypeId {
if (!ty.isBuiltin()) {
const info = self.ir_mod.types.get(ty);
if (info == .pointer) return info.pointer.pointee;
}
return ty;
}
// ── Comparison helpers ────────────────────────────────────────────
fn emitCmp(self: *LLVMEmitter, bin: ir_inst.BinOp, _: TypeId, int_pred: c_uint, float_pred: c_uint) void {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
// Determine if float by inspecting operand LLVM type
var lhs_ty = c.LLVMTypeOf(lhs);
var kind = c.LLVMGetTypeKind(lhs_ty);
var rhs_ty = c.LLVMTypeOf(rhs);
var rhs_kind = c.LLVMGetTypeKind(rhs_ty);
// Unwrap single-element struct (1-tuple) to scalar for comparison
if (kind == c.LLVMStructTypeKind and rhs_kind != c.LLVMStructTypeKind) {
if (c.LLVMCountStructElementTypes(lhs_ty) == 1) {
lhs = c.LLVMBuildExtractValue(self.builder, lhs, 0, "tup.unwrap");
lhs_ty = c.LLVMTypeOf(lhs);
kind = c.LLVMGetTypeKind(lhs_ty);
}
} else if (rhs_kind == c.LLVMStructTypeKind and kind != c.LLVMStructTypeKind) {
if (c.LLVMCountStructElementTypes(rhs_ty) == 1) {
rhs = c.LLVMBuildExtractValue(self.builder, rhs, 0, "tup.unwrap");
rhs_ty = c.LLVMTypeOf(rhs);
rhs_kind = c.LLVMGetTypeKind(rhs_ty);
}
}
// Struct types (strings, slices, tagged unions): compare fields individually
if (kind == c.LLVMStructTypeKind and rhs_kind == c.LLVMStructTypeKind) {
const n_fields = c.LLVMCountStructElementTypes(lhs_ty);
if (n_fields >= 2) {
const is_eq = (int_pred == c.LLVMIntEQ);
const f0_l = c.LLVMBuildExtractValue(self.builder, lhs, 0, "sc.l0");
const f0_r = c.LLVMBuildExtractValue(self.builder, rhs, 0, "sc.r0");
const cmp0 = c.LLVMBuildICmp(self.builder, @intCast(int_pred), f0_l, f0_r, "sc.c0");
// Check if field 1 is an array (tagged union payload) — skip comparison
// For tagged unions {tag, [N x i8]}, the tag comparison alone is sufficient
const f1_ty = c.LLVMStructGetTypeAtIndex(lhs_ty, 1);
const f1_kind = c.LLVMGetTypeKind(f1_ty);
if (f1_kind == c.LLVMArrayTypeKind) {
// Tagged union: compare tag only
self.mapRef(cmp0);
return;
}
const f1_l = c.LLVMBuildExtractValue(self.builder, lhs, 1, "sc.l1");
const f1_r = c.LLVMBuildExtractValue(self.builder, rhs, 1, "sc.r1");
const cmp1 = c.LLVMBuildICmp(self.builder, @intCast(int_pred), f1_l, f1_r, "sc.c1");
const result = if (is_eq)
c.LLVMBuildAnd(self.builder, cmp0, cmp1, "sc.and")
else
c.LLVMBuildOr(self.builder, cmp0, cmp1, "sc.or");
self.mapRef(result);
return;
}
}
// Coerce operands to same type if needed
if (kind == c.LLVMIntegerTypeKind and rhs_kind == c.LLVMIntegerTypeKind) {
const lw = c.LLVMGetIntTypeWidth(lhs_ty);
const rw = c.LLVMGetIntTypeWidth(rhs_ty);
const is_unsigned = self.isRefUnsigned(bin.lhs) or self.isRefUnsigned(bin.rhs);
if (is_unsigned) {
if (lw < rw) lhs = c.LLVMBuildZExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildZExt(self.builder, rhs, lhs_ty, "cmp.ext");
} else {
if (lw < rw) lhs = c.LLVMBuildSExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildSExt(self.builder, rhs, lhs_ty, "cmp.ext");
}
}
// Pointer vs integer: coerce int to null pointer
if (kind == c.LLVMPointerTypeKind and rhs_kind == c.LLVMIntegerTypeKind) {
rhs = c.LLVMConstNull(lhs_ty);
} else if (kind == c.LLVMIntegerTypeKind and rhs_kind == c.LLVMPointerTypeKind) {
lhs = c.LLVMConstNull(rhs_ty);
}
const result_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(lhs));
const result = if (result_kind == c.LLVMFloatTypeKind or result_kind == c.LLVMDoubleTypeKind)
c.LLVMBuildFCmp(self.builder, @intCast(float_pred), lhs, rhs, "fcmp")
else
c.LLVMBuildICmp(self.builder, @intCast(int_pred), lhs, rhs, "icmp");
self.mapRef(result);
}
fn emitCmpOrdered(self: *LLVMEmitter, bin: ir_inst.BinOp, _: TypeId, signed_pred: c_uint, unsigned_pred: c_uint, float_pred: c_uint) void {
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
const lhs_ty = c.LLVMTypeOf(lhs);
const kind = c.LLVMGetTypeKind(lhs_ty);
// Determine signedness from IR operand type
const is_unsigned = self.isRefUnsigned(bin.lhs) or self.isRefUnsigned(bin.rhs);
// Coerce operands to same type if needed
if (kind == c.LLVMIntegerTypeKind) {
const rhs_ty = c.LLVMTypeOf(rhs);
const rhs_kind = c.LLVMGetTypeKind(rhs_ty);
if (rhs_kind == c.LLVMIntegerTypeKind) {
const lw = c.LLVMGetIntTypeWidth(lhs_ty);
const rw = c.LLVMGetIntTypeWidth(rhs_ty);
if (is_unsigned) {
if (lw < rw) lhs = c.LLVMBuildZExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildZExt(self.builder, rhs, lhs_ty, "cmp.ext");
} else {
if (lw < rw) lhs = c.LLVMBuildSExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildSExt(self.builder, rhs, lhs_ty, "cmp.ext");
}
}
}
const result = if (kind == c.LLVMFloatTypeKind or kind == c.LLVMDoubleTypeKind)
c.LLVMBuildFCmp(self.builder, @intCast(float_pred), lhs, rhs, "fcmp")
else if (is_unsigned)
c.LLVMBuildICmp(self.builder, @intCast(unsigned_pred), lhs, rhs, "icmp")
else
c.LLVMBuildICmp(self.builder, @intCast(signed_pred), lhs, rhs, "icmp");
self.mapRef(result);
}
/// String comparison via memcmp: compare length first, then content.
fn emitStrCmp(self: *LLVMEmitter, bin: ir_inst.BinOp, is_eq: bool) void {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
const b = self.builder;
const i32_ty = c.LLVMInt32TypeInContext(self.context);
const i1_ty = c.LLVMInt1TypeInContext(self.context);
const ptr_ty = c.LLVMPointerTypeInContext(self.context, 0);
// Extract ptr and len from both fat pointers
const lhs_ptr = c.LLVMBuildExtractValue(b, lhs, 0, "str.lp");
const lhs_len = c.LLVMBuildExtractValue(b, lhs, 1, "str.ll");
const rhs_ptr = c.LLVMBuildExtractValue(b, rhs, 0, "str.rp");
const rhs_len = c.LLVMBuildExtractValue(b, rhs, 1, "str.rl");
// Compare lengths first
const len_eq = c.LLVMBuildICmp(b, c.LLVMIntEQ, lhs_len, rhs_len, "str.len_eq");
// Set up basic blocks
const cur_fn = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(b));
const memcmp_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "str.memcmp");
const merge_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "str.merge");
const cur_bb = c.LLVMGetInsertBlock(b);
_ = c.LLVMBuildCondBr(b, len_eq, memcmp_bb, merge_bb);
// memcmp block
c.LLVMPositionBuilderAtEnd(b, memcmp_bb);
const size_ty = self.sizeType();
const memcmp_fn = c.LLVMGetNamedFunction(self.llvm_module, "memcmp") orelse blk: {
var params = [_]c.LLVMTypeRef{ ptr_ty, ptr_ty, size_ty };
const fn_type = c.LLVMFunctionType(i32_ty, &params, 3, 0);
break :blk c.LLVMAddFunction(self.llvm_module, "memcmp", fn_type);
};
const cmp_len = self.coerceArg(lhs_len, size_ty);
var args = [_]c.LLVMValueRef{ lhs_ptr, rhs_ptr, cmp_len };
const fn_ty = c.LLVMGlobalGetValueType(memcmp_fn);
const cmp_result = c.LLVMBuildCall2(b, fn_ty, memcmp_fn, &args, 3, "memcmp");
const content_eq = c.LLVMBuildICmp(b, c.LLVMIntEQ, cmp_result, c.LLVMConstInt(i32_ty, 0, 0), "str.ceq");
_ = c.LLVMBuildBr(b, merge_bb);
// Merge block: phi(len_mismatch=false, memcmp_result)
c.LLVMPositionBuilderAtEnd(b, merge_bb);
const phi = c.LLVMBuildPhi(b, i1_ty, "str.eq");
const false_val = c.LLVMConstInt(i1_ty, 0, 0);
var phi_vals = [_]c.LLVMValueRef{ false_val, content_eq };
var phi_bbs = [_]c.LLVMBasicBlockRef{ cur_bb, memcmp_bb };
c.LLVMAddIncoming(phi, &phi_vals, &phi_bbs, 2);
const result = if (is_eq)
phi
else
c.LLVMBuildNot(b, phi, "str.ne");
self.mapRef(result);
}
// ── Conversion helpers ──────────────────────────────────────────
fn emitConversion(self: *LLVMEmitter, operand: c.LLVMValueRef, from: TypeId, to: TypeId, to_ty: c.LLVMTypeRef) c.LLVMValueRef {
const from_float = isFloatOrVecFloat(from, &self.ir_mod.types);
const to_float = isFloatOrVecFloat(to, &self.ir_mod.types);
if (from_float and to_float) {
// float→float: FPExt or FPTrunc
const from_bits = floatBits(from);
const to_bits = floatBits(to);
return if (to_bits > from_bits)
c.LLVMBuildFPExt(self.builder, operand, to_ty, "fpext")
else
c.LLVMBuildFPTrunc(self.builder, operand, to_ty, "fptrunc");
}
if (from_float and !to_float) {
return if (isSignedType(to))
c.LLVMBuildFPToSI(self.builder, operand, to_ty, "fptosi")
else
c.LLVMBuildFPToUI(self.builder, operand, to_ty, "fptoui");
}
if (!from_float and to_float) {
return if (isSignedType(from))
c.LLVMBuildSIToFP(self.builder, operand, to_ty, "sitofp")
else
c.LLVMBuildUIToFP(self.builder, operand, to_ty, "uitofp");
}
// int→int: SExt, ZExt, or Trunc
const ptr_bits: u32 = @as(u32, self.ir_mod.types.pointer_size) * 8;
const from_bits = if (intBits(from) == 0) ptr_bits else intBits(from);
const to_bits = if (intBits(to) == 0) ptr_bits else intBits(to);
if (to_bits > from_bits) {
return if (isSignedType(from))
c.LLVMBuildSExt(self.builder, operand, to_ty, "sext")
else
c.LLVMBuildZExt(self.builder, operand, to_ty, "zext");
} else if (to_bits < from_bits) {
return c.LLVMBuildTrunc(self.builder, operand, to_ty, "trunc");
}
// Same width — no-op (bitcast or just return)
return operand;
}
// ── Malloc/Free declarations ────────────────────────────────────
fn getOrDeclareMalloc(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "malloc")) |f| return f;
const fn_ty = self.getMallocType();
return c.LLVMAddFunction(self.llvm_module, "malloc", fn_ty);
}
fn getOrDeclareFree(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "free")) |f| return f;
const fn_ty = self.getFreeType();
return c.LLVMAddFunction(self.llvm_module, "free", fn_ty);
}
/// Returns the LLVM type for C `size_t`: i32 on wasm32, i64 on 64-bit targets (including wasm64).
fn sizeType(self: *LLVMEmitter) c.LLVMTypeRef {
return if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64;
}
fn getMallocType(self: *LLVMEmitter) c.LLVMTypeRef {
// malloc(size_t) → ptr
var param_types = [_]c.LLVMTypeRef{self.sizeType()};
return c.LLVMFunctionType(self.cached_ptr, &param_types, 1, 0);
}
fn getFreeType(self: *LLVMEmitter) c.LLVMTypeRef {
// free(ptr) → void
var param_types = [_]c.LLVMTypeRef{self.cached_ptr};
return c.LLVMFunctionType(self.cached_void, &param_types, 1, 0);
}
fn getOrDeclareMemcpy(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "memcpy")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "memcpy", self.getMemcpyType());
}
fn getMemcpyType(self: *LLVMEmitter) c.LLVMTypeRef {
// memcpy(ptr, ptr, size_t) → ptr
var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.sizeType() };
return c.LLVMFunctionType(self.cached_ptr, &param_types, 3, 0);
}
fn getOrDeclareMemset(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "memset")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "memset", self.getMemsetType());
}
fn getMemsetType(self: *LLVMEmitter) c.LLVMTypeRef {
// memset(ptr, i32, size_t) → ptr
var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_i32, self.sizeType() };
return c.LLVMFunctionType(self.cached_ptr, &param_types, 3, 0);
}
fn getOrDeclareMathF64(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef {
const name: [*:0]const u8 = switch (id) {
.sqrt => "sqrt",
.sin => "sin",
.cos => "cos",
.floor => "floor",
else => unreachable,
};
if (c.LLVMGetNamedFunction(self.llvm_module, name)) |f| return f;
return c.LLVMAddFunction(self.llvm_module, name, self.getMathF64Type());
}
fn getMathF64Type(self: *LLVMEmitter) c.LLVMTypeRef {
var param_types = [_]c.LLVMTypeRef{self.cached_f64};
return c.LLVMFunctionType(self.cached_f64, &param_types, 1, 0);
}
fn getOrDeclareMathF32(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef {
const name: [*:0]const u8 = switch (id) {
.sqrt => "sqrtf",
.sin => "sinf",
.cos => "cosf",
.floor => "floorf",
else => unreachable,
};
if (c.LLVMGetNamedFunction(self.llvm_module, name)) |f| return f;
return c.LLVMAddFunction(self.llvm_module, name, self.getMathF32Type());
}
fn getMathF32Type(self: *LLVMEmitter) c.LLVMTypeRef {
var param_types = [_]c.LLVMTypeRef{self.cached_f32};
return c.LLVMFunctionType(self.cached_f32, &param_types, 1, 0);
}
fn getOrDeclareMemcmp(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "memcmp")) |f| return f;
// memcmp(ptr, ptr, size_t) → i32
var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.sizeType() };
const fn_ty = c.LLVMFunctionType(self.cached_i32, &param_types, 3, 0);
return c.LLVMAddFunction(self.llvm_module, "memcmp", fn_ty);
}
fn getOrDeclareWrite(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "write")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "write", self.getWriteType());
}
fn getWriteType(self: *LLVMEmitter) c.LLVMTypeRef {
// write(fd: i32, buf: ptr, count: size_t) → ssize_t
const st = self.sizeType();
var param_types = [_]c.LLVMTypeRef{ self.cached_i32, self.cached_ptr, st };
return c.LLVMFunctionType(st, &param_types, 3, 0);
}
fn getOrDeclareSnprintf(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "snprintf")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "snprintf", self.getSnprintfType());
}
fn getSnprintfType(self: *LLVMEmitter) c.LLVMTypeRef {
// snprintf(buf: ptr, size: i32, fmt: ptr, ...) → i32 (variadic)
var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_i32, self.cached_ptr };
return c.LLVMFunctionType(self.cached_i32, &param_types, 3, 1); // 1 = variadic
}
/// Check if a function name is a known libc builtin that has a dedicated
/// getOrDeclare* helper with correct C-compatible types.
fn isBuiltinLibcName(name: []const u8) bool {
const builtins = [_][]const u8{ "malloc", "free", "memcpy", "memset", "memcmp", "write", "snprintf" };
for (builtins) |b| {
if (std.mem.eql(u8, name, b)) return true;
}
return false;
}
/// Get or declare a builtin libc function by name, using the correct C-compatible type.
fn getOrDeclareBuiltinByName(self: *LLVMEmitter, name: []const u8) ?c.LLVMValueRef {
if (std.mem.eql(u8, name, "malloc")) return self.getOrDeclareMalloc();
if (std.mem.eql(u8, name, "free")) return self.getOrDeclareFree();
if (std.mem.eql(u8, name, "memcpy")) return self.getOrDeclareMemcpy();
if (std.mem.eql(u8, name, "memset")) return self.getOrDeclareMemset();
if (std.mem.eql(u8, name, "memcmp")) return self.getOrDeclareMemcmp();
if (std.mem.eql(u8, name, "write")) return self.getOrDeclareWrite();
if (std.mem.eql(u8, name, "snprintf")) return self.getOrDeclareSnprintf();
return null;
}
/// Build a string fat pointer {ptr, len} from raw pointer and length.
fn buildStringValue(self: *LLVMEmitter, ptr: c.LLVMValueRef, len: c.LLVMValueRef) c.LLVMValueRef {
const str_ty = self.getStringStructType();
const undef = c.LLVMGetUndef(str_ty);
const with_ptr = c.LLVMBuildInsertValue(self.builder, undef, ptr, 0, "s.ptr");
return c.LLVMBuildInsertValue(self.builder, with_ptr, len, 1, "s.len");
}
// ── Value coercion helpers ──────────────────────────────────────
/// Coerce any scalar value to i64 for boxing into Any.
fn coerceToI64(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef {
const ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(ty);
if (kind == c.LLVMVoidTypeKind) {
return c.LLVMConstInt(self.cached_i64, 0, 0);
}
if (kind == c.LLVMPointerTypeKind) {
return c.LLVMBuildPtrToInt(self.builder, val, self.cached_i64, "p2i");
}
if (kind == c.LLVMFloatTypeKind) {
// f32 → bitcast to i32 → zext to i64
const as_i32 = c.LLVMBuildBitCast(self.builder, val, self.cached_i32, "f2i32");
return c.LLVMBuildZExt(self.builder, as_i32, self.cached_i64, "z64");
}
if (kind == c.LLVMDoubleTypeKind) {
return c.LLVMBuildBitCast(self.builder, val, self.cached_i64, "d2i");
}
if (kind == c.LLVMIntegerTypeKind) {
const width = c.LLVMGetIntTypeWidth(ty);
if (width < 64) {
return c.LLVMBuildZExt(self.builder, val, self.cached_i64, "z64");
}
return val; // already i64
}
// Struct/Array/Vector types: store to alloca, ptrtoint for the pointer
if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind or kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, ty, "ba.tmp");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildPtrToInt(self.builder, tmp, self.cached_i64, "ba.p2i");
}
return val;
}
/// Coerce signed integer to i64 using sign-extension.
fn coerceToI64Signed(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef {
const ty = c.LLVMTypeOf(val);
const kind = c.LLVMGetTypeKind(ty);
if (kind == c.LLVMIntegerTypeKind) {
const width = c.LLVMGetIntTypeWidth(ty);
if (width < 64) {
return c.LLVMBuildSExt(self.builder, val, self.cached_i64, "s64");
}
return val;
}
// Fallback for non-integer types
return self.coerceToI64(val);
}
/// Check if a TypeId represents a signed integer type (including arbitrary-width).
fn isSignedTypeEx(self: *LLVMEmitter, ty: TypeId) bool {
if (isSignedType(ty)) return true;
if (!ty.isBuiltin()) {
const info = self.ir_mod.types.get(ty);
return info == .signed;
}
return false;
}
/// Map a TypeId to its Any tag value.
/// Uses TypeId.index() directly — this matches resolveTypeCategoryTags in lower.zig
/// which also uses TypeId indices for type-switch comparisons.
/// For arbitrary-width ints (user-defined signed/unsigned), map to the closest
/// builtin TypeId so the "case int:" branch matches correctly.
/// Map a TypeId to its Any tag value.
/// Uses TypeId.index() directly — this matches resolveTypeCategoryTags in lower.zig
/// which also uses TypeId indices for type-switch comparisons.
/// For arbitrary-width ints (user-defined signed/unsigned), map to the closest
/// builtin TypeId so the "case int:" branch matches correctly.
fn anyTag(self: *LLVMEmitter, ty: TypeId) u64 {
if (ty.isBuiltin()) return ty.index();
// For user-defined types, check if they're arbitrary-width ints
const info = self.ir_mod.types.get(ty);
return switch (info) {
.signed => |w| switch (w) {
8 => TypeId.s8.index(),
16 => TypeId.s16.index(),
32 => TypeId.s32.index(),
64 => TypeId.s64.index(),
else => if (w <= 32) TypeId.s32.index() else TypeId.s64.index(),
},
.unsigned => |w| switch (w) {
8 => TypeId.u8.index(),
16 => TypeId.u16.index(),
32 => TypeId.u32.index(),
64 => TypeId.u64.index(),
else => if (w <= 32) TypeId.u32.index() else TypeId.u64.index(),
},
else => ty.index(),
};
}
/// Coerce i64 back to the target type for unboxing from Any.
fn coerceFromI64(self: *LLVMEmitter, val: c.LLVMValueRef, target: c.LLVMTypeRef) c.LLVMValueRef {
const kind = c.LLVMGetTypeKind(target);
if (kind == c.LLVMPointerTypeKind) {
return c.LLVMBuildIntToPtr(self.builder, val, target, "i2p");
}
if (kind == c.LLVMFloatTypeKind) {
const as_i32 = c.LLVMBuildTrunc(self.builder, val, self.cached_i32, "tr32");
return c.LLVMBuildBitCast(self.builder, as_i32, target, "i2f");
}
if (kind == c.LLVMDoubleTypeKind) {
return c.LLVMBuildBitCast(self.builder, val, target, "i2d");
}
if (kind == c.LLVMIntegerTypeKind) {
const width = c.LLVMGetIntTypeWidth(target);
if (width < 64) {
return c.LLVMBuildTrunc(self.builder, val, target, "tr");
}
}
// Struct/Array/Vector types: interpret i64 as pointer, load the value
if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind or kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) {
const ptr = c.LLVMBuildIntToPtr(self.builder, val, self.cached_ptr, "ua.ptr");
return c.LLVMBuildLoad2(self.builder, target, ptr, "ua.load");
}
return val;
}
/// Coerce a call argument to match the expected parameter type.
/// Handles int width mismatches (trunc/ext), float width, and int↔float.
fn coerceArg(self: *LLVMEmitter, val: c.LLVMValueRef, param_ty: c.LLVMTypeRef) c.LLVMValueRef {
const val_ty = c.LLVMTypeOf(val);
if (val_ty == param_ty) return val;
const val_kind = c.LLVMGetTypeKind(val_ty);
const param_kind = c.LLVMGetTypeKind(param_ty);
// Int → Int (width mismatch)
if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMIntegerTypeKind) {
const val_w = c.LLVMGetIntTypeWidth(val_ty);
const param_w = c.LLVMGetIntTypeWidth(param_ty);
if (val_w > param_w) {
return c.LLVMBuildTrunc(self.builder, val, param_ty, "ca.tr");
} else {
// Use ZExt by default — preserves bit pattern for unsigned types.
// Signed widening is handled by explicit widen instructions from the IR.
return c.LLVMBuildZExt(self.builder, val, param_ty, "ca.ext");
}
}
// Float → Float (width mismatch)
if ((val_kind == c.LLVMFloatTypeKind or val_kind == c.LLVMDoubleTypeKind) and
(param_kind == c.LLVMFloatTypeKind or param_kind == c.LLVMDoubleTypeKind))
{
if (val_kind == c.LLVMFloatTypeKind and param_kind == c.LLVMDoubleTypeKind) {
return c.LLVMBuildFPExt(self.builder, val, param_ty, "ca.fpext");
} else {
return c.LLVMBuildFPTrunc(self.builder, val, param_ty, "ca.fptrunc");
}
}
// Int → Float (use SIToFP for i1/bool, UIToFP otherwise for safe default)
if (val_kind == c.LLVMIntegerTypeKind and (param_kind == c.LLVMFloatTypeKind or param_kind == c.LLVMDoubleTypeKind)) {
const val_w = c.LLVMGetIntTypeWidth(val_ty);
if (val_w == 1) {
return c.LLVMBuildUIToFP(self.builder, val, param_ty, "ca.uitofp");
}
// Default to SIToFP since most sx integers are signed (s64).
// Explicit unsigned conversions go through the IR widen/narrow path.
return c.LLVMBuildSIToFP(self.builder, val, param_ty, "ca.sitofp");
}
// Float → Int
if ((val_kind == c.LLVMFloatTypeKind or val_kind == c.LLVMDoubleTypeKind) and param_kind == c.LLVMIntegerTypeKind) {
return c.LLVMBuildFPToSI(self.builder, val, param_ty, "ca.fptosi");
}
// Ptr → Struct (closure auto-promotion: fn_ptr → {fn_ptr, null_env})
if (val_kind == c.LLVMPointerTypeKind and param_kind == c.LLVMStructTypeKind) {
const num_fields = c.LLVMCountStructElementTypes(param_ty);
if (num_fields == 2) {
const f0 = c.LLVMStructGetTypeAtIndex(param_ty, 0);
const f1 = c.LLVMStructGetTypeAtIndex(param_ty, 1);
if (c.LLVMGetTypeKind(f0) == c.LLVMPointerTypeKind and c.LLVMGetTypeKind(f1) == c.LLVMPointerTypeKind) {
var result = c.LLVMGetUndef(param_ty);
result = c.LLVMBuildInsertValue(self.builder, result, val, 0, "ca.cls.fn");
result = c.LLVMBuildInsertValue(self.builder, result, c.LLVMConstNull(f1), 1, "ca.cls.env");
return result;
}
}
}
// Scalar → Vector (splat: broadcast scalar to all lanes)
if (param_kind == c.LLVMVectorTypeKind or param_kind == c.LLVMScalableVectorTypeKind) {
const vec_elem_ty = c.LLVMGetElementType(param_ty);
const vec_len = c.LLVMGetVectorSize(param_ty);
// First coerce scalar to the vector element type
const scalar = self.coerceArg(val, vec_elem_ty);
// Then splat into a vector
var result = c.LLVMGetUndef(param_ty);
var lane: c_uint = 0;
while (lane < vec_len) : (lane += 1) {
const idx = c.LLVMConstInt(self.cached_i32, lane, 0);
result = c.LLVMBuildInsertElement(self.builder, result, scalar, idx, "splat");
}
return result;
}
// Struct → Ptr (string/slice decay: extract field 0 = raw pointer)
// Only for 2-field structs {ptr, i64} (fat pointers) — avoids breaking other struct→ptr cases
if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMPointerTypeKind) {
const num_fields = c.LLVMCountStructElementTypes(val_ty);
if (num_fields == 2) {
const field0_ty = c.LLVMStructGetTypeAtIndex(val_ty, 0);
if (c.LLVMGetTypeKind(field0_ty) == c.LLVMPointerTypeKind) {
return c.LLVMBuildExtractValue(self.builder, val, 0, "ca.decay");
}
}
}
// Struct → Integer (C ABI coercion: store struct to memory, load as integer)
if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMIntegerTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, param_ty, "abi.tmp");
_ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(param_ty), tmp);
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.coerce");
}
// Integer → Struct (C ABI return coercion: store integer to memory, load as struct)
if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMStructTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "abi.ret.tmp");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce");
}
// Struct → Array (C ABI coercion for 9..16-byte structs — paired with
// abiCoerceParamType's `[2 x i64]` slot for that size class). Same
// memory-bitcast pattern as the integer case; the array type carries
// 16 bytes of storage so we alloca with param_ty to guarantee size.
if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMArrayTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, param_ty, "abi.struct2arr");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.coerce.arr");
}
// Array → Struct (return-side counterpart for 9..16-byte structs)
if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMStructTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "abi.arr2struct");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce.arr");
}
// Array → Ptr (array decay: alloca + GEP to first element)
if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMPointerTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "ca.arr");
_ = c.LLVMBuildStore(self.builder, val, tmp);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [_]c.LLVMValueRef{ zero, zero };
return c.LLVMBuildGEP2(self.builder, val_ty, tmp, &indices, 2, "ca.decay");
}
// Int → Ptr (null literal: inttoptr)
if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMPointerTypeKind) {
return c.LLVMBuildIntToPtr(self.builder, val, param_ty, "ca.itp");
}
return val;
}
/// Look up the IR type of a Ref in the current function (for store coercion).
fn getRefIRType(self: *LLVMEmitter, ref: Ref) ?TypeId {
const func = &self.ir_mod.functions.items[self.current_func_idx];
const idx = ref.index();
// Check if it's a function param (refs 0..N-1)
if (idx < func.params.len) return func.params[idx].ty;
for (func.blocks.items) |blk| {
if (idx >= blk.first_ref and idx < blk.first_ref + blk.insts.items.len) {
return blk.insts.items[idx - blk.first_ref].ty;
}
}
return null;
}
/// Coerce both binary operands to match the instruction's result type.
/// E.g. if result is i64 but one operand is i32, sext it.
fn matchBinOpTypes(self: *LLVMEmitter, lhs: *c.LLVMValueRef, rhs: *c.LLVMValueRef, result_ty: TypeId) void {
const target = self.toLLVMType(result_ty);
lhs.* = self.coerceArg(lhs.*, target);
rhs.* = self.coerceArg(rhs.*, target);
}
// ── Type conversion ─────────────────────────────────────────────
pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef {
return switch (ty) {
.void => self.cached_void,
.bool => self.cached_i1,
.s8 => self.cached_i8,
.s16 => self.cached_i16,
.s32 => self.cached_i32,
.s64 => self.cached_i64,
.u8 => self.cached_i8,
.u16 => self.cached_i16,
.u32 => self.cached_i32,
.u64 => self.cached_i64,
.f32 => self.cached_f32,
.f64 => self.cached_f64,
.string => self.getStringStructType(),
.any => self.getAnyStructType(),
.noreturn => self.cached_void,
.isize, .usize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64,
else => self.toLLVMTypeInfo(ty),
};
}
fn toLLVMTypeInfo(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef {
const info = self.ir_mod.types.get(ty);
return switch (info) {
.signed => |w| switch (w) {
1 => self.cached_i1,
8 => self.cached_i8,
16 => self.cached_i16,
32 => self.cached_i32,
64 => self.cached_i64,
else => c.LLVMIntTypeInContext(self.context, w),
},
.unsigned => |w| switch (w) {
1 => self.cached_i1,
8 => self.cached_i8,
16 => self.cached_i16,
32 => self.cached_i32,
64 => self.cached_i64,
else => c.LLVMIntTypeInContext(self.context, w),
},
.f32 => self.cached_f32,
.f64 => self.cached_f64,
.void => self.cached_void,
.bool => self.cached_i1,
.error_set => self.cached_i32, // u32 tag id on the error channel
.string => self.getStringStructType(),
.pointer, .many_pointer, .function => self.cached_ptr,
.closure => self.getClosureStructType(),
.slice => self.getStringStructType(), // same {ptr, i64} layout
.optional => |opt| {
// ?*T / ?fn → bare pointer (null = none)
const child_info = self.ir_mod.types.get(opt.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function) {
return self.cached_ptr;
}
if (child_info == .closure) {
return self.getClosureStructType();
}
// ?Protocol → protocol struct (ctx ptr = field 0 is null when none).
if (child_info == .@"struct" and child_info.@"struct".is_protocol) {
return self.toLLVMType(opt.child);
}
// ?T → { T, i1 }
var field_types: [2]c.LLVMTypeRef = .{
self.toLLVMType(opt.child),
self.cached_i1,
};
return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
},
.array => |arr| {
const elem = self.toLLVMType(arr.element);
return c.LLVMArrayType2(elem, arr.length);
},
.vector => |vec| {
const elem = self.toLLVMType(vec.element);
return c.LLVMVectorType(elem, vec.length);
},
.any => self.getAnyStructType(),
.noreturn => self.cached_void,
.@"struct" => |s| {
// Build LLVM struct type from fields
const n: c_uint = @intCast(s.fields.len);
const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, s.fields.len) catch unreachable;
defer self.alloc.free(field_llvm_types);
for (s.fields, 0..) |field, j| {
field_llvm_types[j] = self.toLLVMType(field.ty);
}
return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0);
},
.@"enum" => |e| {
// Use backing type if declared (e.g. enum u32 → i32), else i64
if (e.backing_type) |bt| return self.toLLVMType(bt);
return self.cached_i64;
},
.@"union" => |u| {
// Untagged union — just [N x i8]
var max_size: usize = 0;
for (u.fields) |field| {
const sz = self.ir_mod.types.typeSizeBytes(field.ty);
if (sz > max_size) max_size = sz;
}
if (max_size == 0) max_size = 8;
return c.LLVMArrayType2(self.cached_i8, @intCast(max_size));
},
.tagged_union => |u| {
// Tagged union — { header, [N x i8] }
var max_size: usize = 0;
for (u.fields) |field| {
const sz = self.ir_mod.types.typeSizeBytes(field.ty);
if (sz > max_size) max_size = sz;
}
if (max_size == 0) max_size = 8;
var header_size: usize = self.ir_mod.types.typeSizeBytes(u.tag_type);
if (u.backing_type) |bt| {
const bi = self.ir_mod.types.get(bt);
if (bi == .@"struct" and bi.@"struct".fields.len > 1) {
header_size = 0;
const fields = bi.@"struct".fields;
for (fields[0 .. fields.len - 1]) |f| {
header_size += self.ir_mod.types.typeSizeBytes(f.ty);
}
const backing_payload = self.ir_mod.types.typeSizeBytes(fields[fields.len - 1].ty);
if (backing_payload > max_size) max_size = backing_payload;
}
}
const header_llvm = c.LLVMIntTypeInContext(self.context, @intCast(header_size * 8));
var field_types: [2]c.LLVMTypeRef = .{
header_llvm,
c.LLVMArrayType2(self.cached_i8, @intCast(max_size)),
};
return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
},
.tuple => |t| {
const n: c_uint = @intCast(t.fields.len);
const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, t.fields.len) catch unreachable;
defer self.alloc.free(field_llvm_types);
for (t.fields, 0..) |f, j| {
field_llvm_types[j] = self.toLLVMType(f);
}
return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0);
},
.protocol => {
// Protocol values: { ctx: *void, vtable_or_fn_ptrs... }
// For now, use opaque ptr
return self.cached_ptr;
},
.usize, .isize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64,
// Comptime-only: a pack is expanded to flat positional args before
// codegen, so it must never reach LLVM type emission.
.pack => @panic("pack type has no LLVM representation (comptime-only)"),
// Tripwire: a failed type resolution must have been diagnosed and
// aborted long before LLVM emission.
.unresolved => @panic("unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted"),
};
}
// ── C ABI coercion for foreign functions ──────────────────────────
//
// On ARM64 (and x86_64), the C calling convention coerces small struct
// arguments to integers for register passing:
// - String/slice {ptr, i64} → ptr (extract raw pointer)
// - Small integer struct (≤ 8 bytes, non-HFA) → i64
// - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it)
fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef {
return self.abiCoerceParamTypeEx(ir_ty, llvm_ty, true);
}
/// Same as `abiCoerceParamType` but with an explicit
/// `is_foreign_c_api` knob. When true, sx `string` / `[]T` slices
/// collapse to `ptr` — the libc convention where the user writes
/// `string` to mean `char *` and the length is dropped. When
/// false (sx-internal `callconv(.c)` like block trampolines), the
/// full slice shape is preserved and goes through the general
/// struct-coerce path (16-byte slice → `[2 x i64]`, lands in two
/// registers on AArch64 — the true C ABI for a 16-byte
/// aggregate). Without the split, sx-to-sx calls through a
/// `(*Block, string) -> void callconv(.c)` fn-pointer mismatched
/// the caller's `{ptr, i64}` value against the trampoline's
/// collapsed `ptr` param.
fn abiCoerceParamTypeEx(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef, is_foreign_c_api: bool) c.LLVMTypeRef {
if (is_foreign_c_api) {
if (ir_ty == .string) return self.cached_ptr;
if (!ir_ty.isBuiltin()) {
const info = self.ir_mod.types.get(ir_ty);
if (info == .slice) return self.cached_ptr;
}
}
// WASM32: usize/isize are pointer-sized (i32 on wasm32).
// Other integer types (s64, u64) keep their declared size — they represent
// genuinely 64-bit values (SDL_WindowFlags, timestamps, etc.).
if (self.target_config.isWasm32()) {
if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32;
return llvm_ty;
}
// Only coerce struct types
if (c.LLVMGetTypeKind(llvm_ty) != c.LLVMStructTypeKind) return llvm_ty;
// Check if it's an HFA (all float or all double fields) — leave as-is
const n_fields = c.LLVMCountStructElementTypes(llvm_ty);
if (n_fields >= 1 and n_fields <= 4) {
var all_float = true;
var all_double = true;
var fi: c_uint = 0;
while (fi < n_fields) : (fi += 1) {
const ft = c.LLVMStructGetTypeAtIndex(llvm_ty, fi);
const fk = c.LLVMGetTypeKind(ft);
if (fk != c.LLVMFloatTypeKind) all_float = false;
if (fk != c.LLVMDoubleTypeKind) all_double = false;
}
if (all_float or all_double) return llvm_ty;
}
// Small struct (≤ 8 bytes) → coerce to i64
const size = c.LLVMABISizeOfType(
c.LLVMGetModuleDataLayout(self.llvm_module),
llvm_ty,
);
if (size <= 8) return self.cached_i64;
// Medium struct (9-16 bytes) → coerce to [2 x i64]
if (size <= 16) {
return c.LLVMArrayType2(self.cached_i64, 2);
}
// Large composite (> 16 bytes) → pass by reference: ptr + byval(<T>) at
// the call/sig sites. LLVM's AArch64/x86_64 backend lowers byval to
// the right ABI sequence (caller copy + indirect arg).
return self.cached_ptr;
}
fn needsByval(self: *LLVMEmitter, ir_ty: TypeId, raw_llvm_ty: c.LLVMTypeRef) bool {
if (self.target_config.isWasm32()) return false;
if (ir_ty == .string) return false;
if (!ir_ty.isBuiltin()) {
const info = self.ir_mod.types.get(ir_ty);
if (info == .slice) return false;
}
if (c.LLVMGetTypeKind(raw_llvm_ty) != c.LLVMStructTypeKind) return false;
const n = c.LLVMCountStructElementTypes(raw_llvm_ty);
if (n >= 1 and n <= 4) {
var all_f = true;
var all_d = true;
var i: c_uint = 0;
while (i < n) : (i += 1) {
const ft = c.LLVMStructGetTypeAtIndex(raw_llvm_ty, i);
const fk = c.LLVMGetTypeKind(ft);
if (fk != c.LLVMFloatTypeKind) all_f = false;
if (fk != c.LLVMDoubleTypeKind) all_d = false;
}
if (all_f or all_d) return false;
}
const size = c.LLVMABISizeOfType(c.LLVMGetModuleDataLayout(self.llvm_module), raw_llvm_ty);
return size > 16;
}
fn materializeByvalArg(self: *LLVMEmitter, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef {
const tmp = c.LLVMBuildAlloca(self.builder, struct_ty, "byval.tmp");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return tmp;
}
// ── Cached composite types ──────────────────────────────────────
fn getStringStructType(self: *LLVMEmitter) c.LLVMTypeRef {
if (self.string_struct_type) |t| return t;
var field_types = [_]c.LLVMTypeRef{
self.cached_ptr, // ptr
self.cached_i64, // len
};
self.string_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
return self.string_struct_type.?;
}
fn getAnyStructType(self: *LLVMEmitter) c.LLVMTypeRef {
if (self.any_struct_type) |t| return t;
var field_types = [_]c.LLVMTypeRef{
self.cached_i64, // type tag
self.cached_i64, // value
};
self.any_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
return self.any_struct_type.?;
}
fn getClosureStructType(self: *LLVMEmitter) c.LLVMTypeRef {
if (self.closure_struct_type) |t| return t;
var field_types = [_]c.LLVMTypeRef{
self.cached_ptr, // fn_ptr
self.cached_ptr, // env
};
self.closure_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
return self.closure_struct_type.?;
}
// ── String constant emission ────────────────────────────────────
/// Build a constant string { ptr, i64 } value without using the builder
/// (safe to call during global initialization, before any function body is emitted).
fn emitConstStringGlobal(self: *LLVMEmitter, str: []const u8) c.LLVMValueRef {
const str_z = self.alloc.dupeZ(u8, str) catch unreachable;
defer self.alloc.free(str_z);
const len: c_uint = @intCast(str.len + 1); // include null terminator
const str_const = c.LLVMConstStringInContext(self.context, str_z.ptr, len - 1, 0);
const arr_ty = c.LLVMArrayType2(self.cached_i8, len);
const str_global_val = c.LLVMAddGlobal(self.llvm_module, arr_ty, "str.data");
c.LLVMSetInitializer(str_global_val, str_const);
c.LLVMSetGlobalConstant(str_global_val, 1);
c.LLVMSetLinkage(str_global_val, c.LLVMPrivateLinkage);
c.LLVMSetUnnamedAddress(str_global_val, c.LLVMGlobalUnnamedAddr);
// Build constant { ptr, i64 } aggregate
const len_val = c.LLVMConstInt(self.cached_i64, str.len, 0);
var fields = [_]c.LLVMValueRef{ str_global_val, len_val };
return c.LLVMConstStructInContext(self.context, &fields, 2, 0);
}
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
const kind = c.LLVMGetTypeKind(llvm_ty);
const is_struct = kind == c.LLVMStructTypeKind;
const n: c_uint = @intCast(agg.len);
const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty);
defer self.alloc.free(vals);
for (agg, 0..) |cv, i| {
const elem_ty = if (is_struct)
c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i))
else
c.LLVMGetElementType(llvm_ty);
vals[i] = switch (cv) {
.int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1),
.float => |v| c.LLVMConstReal(elem_ty, v),
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty),
else => c.LLVMConstNull(elem_ty),
};
}
if (is_struct) {
return c.LLVMConstNamedStruct(llvm_ty, vals.ptr, n);
}
const elem_ty = c.LLVMGetElementType(llvm_ty);
return c.LLVMConstArray(elem_ty, vals.ptr, n);
}
fn emitStringConstant(self: *LLVMEmitter, str: []const u8) c.LLVMValueRef {
// LLVMBuildGlobalStringPtr needs a null-terminated C string
const str_z = self.alloc.dupeZ(u8, str) catch unreachable;
defer self.alloc.free(str_z);
// Create a global constant string and return a fat pointer { ptr, len }
const str_global = c.LLVMBuildGlobalStringPtr(self.builder, str_z.ptr, "str");
const len_val = c.LLVMConstInt(self.cached_i64, str.len, 0);
const str_ty = self.getStringStructType();
const undef = c.LLVMGetUndef(str_ty);
const with_ptr = c.LLVMBuildInsertValue(self.builder, undef, str_global, 0, "str.ptr");
return c.LLVMBuildInsertValue(self.builder, with_ptr, len_val, 1, "str.len");
}
/// Emit a NUL-terminated C string as a private LLVM global and return
/// the pointer to its first byte. Used for FindClass(env, "<path>") etc.
/// where the runtime expects raw `const char *`, not the sx slice shape.
fn emitCStringGlobal(self: *LLVMEmitter, str: []const u8, name: [*:0]const u8) c.LLVMValueRef {
const z = self.alloc.dupeZ(u8, str) catch unreachable;
defer self.alloc.free(z);
return c.LLVMBuildGlobalStringPtr(self.builder, z.ptr, name);
}
/// Expand a JNI constructor dispatch (`Foo.new(args)` in sx). Chain:
/// `FindClass(env, parent_class_path)` → `GetMethodID(env, clazz,
/// "<init>", sig)` → `NewObject(env, clazz, mid, args...)`. Returns
/// the new jobject. Per-call lookups — no caching yet.
fn emitJniConstructor(self: *LLVMEmitter, msg: ir_inst.JniMsgSend, ret_ty_id: TypeId) void {
const env = self.resolveRef(msg.env);
const sig_ptr = self.extractSlicePtr(self.resolveRef(msg.sig));
const name_ptr = self.extractSlicePtr(self.resolveRef(msg.name));
const ifs = c.LLVMBuildLoad2(self.builder, self.cached_ptr, env, "jni.ifs");
const path = msg.parent_class_path orelse "";
const path_global = self.emitCStringGlobal(path, "jni.ctor.path");
const find_class = self.loadJniFn(ifs, Jni.FindClass, "jni.FindClass");
var fc_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const fc_ty = c.LLVMFunctionType(self.cached_ptr, &fc_params, 2, 0);
var fc_args = [_]c.LLVMValueRef{ env, path_global };
const cls = c.LLVMBuildCall2(self.builder, fc_ty, find_class, &fc_args, 2, "jni.ctor.cls");
const get_mid = self.loadJniFn(ifs, Jni.GetMethodID, "jni.GetMethodID");
var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr };
const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0);
var gmid_args = [_]c.LLVMValueRef{ env, cls, name_ptr, sig_ptr };
const mid = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.ctor.mid");
const new_object = self.loadJniFn(ifs, Jni.NewObject, "jni.NewObject");
const raw_ret = self.toLLVMType(ret_ty_id);
const total_call_params: usize = 3 + msg.args.len;
const call_param_types = self.alloc.alloc(c.LLVMTypeRef, total_call_params) catch unreachable;
defer self.alloc.free(call_param_types);
const call_args = self.alloc.alloc(c.LLVMValueRef, total_call_params) catch unreachable;
defer self.alloc.free(call_args);
call_param_types[0] = self.cached_ptr;
call_param_types[1] = self.cached_ptr;
call_param_types[2] = self.cached_ptr;
call_args[0] = env;
call_args[1] = cls;
call_args[2] = mid;
for (msg.args, 0..) |arg_ref, i| {
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
const raw_llvm = self.toLLVMType(raw_ty);
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
call_param_types[i + 3] = coerced_ty;
call_args[i + 3] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const call_fn_ty = c.LLVMFunctionType(raw_ret, call_param_types.ptr, @intCast(total_call_params), 0);
const result = c.LLVMBuildCall2(self.builder, call_fn_ty, new_object, call_args.ptr, @intCast(total_call_params), "jni.new.obj");
self.mapRef(result);
}
// ── Reflection emission helpers ────────────────────────────────
/// Build (or return cached) a global constant array of {ptr, i64}
/// string values indexed by `TypeId.index()`. Lets the dynamic
/// `type_name(t)` builtin look up the type's display name at
/// runtime — `gep arr[tid]; load string`. The array's length is
/// the current `infos.items.len`; new types interned after this
/// is built fall outside the array (the gep would OOB), so
/// callers must build LAZILY after all types are registered.
fn getOrBuildTypeNameArray(self: *LLVMEmitter) c.LLVMValueRef {
if (self.type_name_array) |g| return g;
const n: u32 = @intCast(self.ir_mod.types.infos.items.len);
const string_ty = self.getStringStructType();
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
var i: u32 = 0;
while (i < n) : (i += 1) {
const tid = @import("types.zig").TypeId.fromIndex(i);
const name_str = self.ir_mod.types.formatTypeName(self.alloc, tid);
const str_z = self.alloc.dupeZ(u8, name_str) catch unreachable;
defer self.alloc.free(str_z);
const global_str = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(name_str.len + 1)), "tn.str");
c.LLVMSetInitializer(global_str, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(name_str.len + 1), 1));
c.LLVMSetGlobalConstant(global_str, 1);
c.LLVMSetLinkage(global_str, c.LLVMPrivateLinkage);
const len_val = c.LLVMConstInt(self.cached_i64, name_str.len, 0);
var struct_fields = [2]c.LLVMValueRef{ global_str, len_val };
const const_struct = c.LLVMConstStructInContext(self.context, &struct_fields, 2, 0);
field_vals.append(self.alloc, const_struct) catch unreachable;
}
const arr_ty = c.LLVMArrayType(string_ty, n);
const arr_init = c.LLVMConstArray(string_ty, field_vals.items.ptr, n);
const global = c.LLVMAddGlobal(self.llvm_module, arr_ty, "__sx_type_names");
c.LLVMSetInitializer(global, arr_init);
c.LLVMSetGlobalConstant(global, 1);
c.LLVMSetLinkage(global, c.LLVMPrivateLinkage);
self.type_name_array = global;
self.type_name_array_len = n;
return global;
}
/// Build (or return cached) a global constant array of {ptr, i64} string values
/// for the field names of a struct type.
fn getOrBuildFieldNameArray(self: *LLVMEmitter, struct_type: TypeId) c.LLVMValueRef {
if (self.field_name_arrays.get(struct_type.index())) |g| return g;
const info = self.ir_mod.types.get(struct_type);
// Collect name StringIds from struct fields, union fields, or enum variants
var name_ids = std.ArrayList(StringId).empty;
defer name_ids.deinit(self.alloc);
switch (info) {
.@"struct" => |s| {
for (s.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable;
},
.@"union" => |u| {
for (u.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable;
},
.tagged_union => |u| {
for (u.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable;
},
.@"enum" => |e| {
for (e.variants) |v| name_ids.append(self.alloc, v) catch unreachable;
},
else => {},
}
const string_ty = self.getStringStructType();
const n: u32 = @intCast(name_ids.items.len);
// Build constant initializer: [N x {ptr, i64}]
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (name_ids.items) |name_id| {
const name_str = self.ir_mod.types.getString(name_id);
const str_z = self.alloc.dupeZ(u8, name_str) catch unreachable;
defer self.alloc.free(str_z);
const global_str = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(name_str.len + 1)), "fld.str");
c.LLVMSetInitializer(global_str, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(name_str.len + 1), 1));
c.LLVMSetGlobalConstant(global_str, 1);
c.LLVMSetLinkage(global_str, c.LLVMPrivateLinkage);
// Build fat pointer {ptr, len} as constant struct
const len_val = c.LLVMConstInt(self.cached_i64, name_str.len, 0);
var struct_fields = [2]c.LLVMValueRef{ global_str, len_val };
const const_struct = c.LLVMConstStructInContext(self.context, &struct_fields, 2, 0);
field_vals.append(self.alloc, const_struct) catch unreachable;
}
// Create global array [N x {ptr, i64}]
const array_ty = c.LLVMArrayType(string_ty, n);
const array_init = c.LLVMConstArray(string_ty, field_vals.items.ptr, n);
const global = c.LLVMAddGlobal(self.llvm_module, array_ty, "field_names");
c.LLVMSetInitializer(global, array_init);
c.LLVMSetGlobalConstant(global, 1);
c.LLVMSetLinkage(global, c.LLVMPrivateLinkage);
self.field_name_arrays.put(struct_type.index(), global) catch unreachable;
return global;
}
/// Failable main entry-point wrapper (ERR E4.2). At the LLVM level main
/// returns i32. `tag_val` is the u32 error tag (0 = "no error"); `value` is
/// the integer value slot for a value-carrying `-> (int, !)` main, or null
/// for a pure `-> !` main. Emit the branch: tag == 0 → `ret i32 <value-or-0>`
/// (success — exit code truncated to u8 downstream); else resolve the tag
/// name from the always-linked tag-name table, hand it + the tag to
/// `sx_trace_report_unhandled` (prints the header + return trace to stderr),
/// and `ret i32 1`.
fn emitFailableMainRet(self: *LLVMEmitter, value: ?c.LLVMValueRef, tag_val: c.LLVMValueRef) void {
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const tag_i32 = self.coerceArg(tag_val, self.cached_i32);
const is_err = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, tag_i32, c.LLVMConstInt(self.cached_i32, 0, 0), "main.iserr");
const ok_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.ok");
const err_bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, "main.err");
_ = c.LLVMBuildCondBr(self.builder, is_err, err_bb, ok_bb);
// Success: exit the value (truncated to u8 by the JIT/OS) or 0.
c.LLVMPositionBuilderAtEnd(self.builder, ok_bb);
const ok_ret = if (value) |v| self.coerceArg(v, self.cached_i32) else c.LLVMConstInt(self.cached_i32, 0, 0);
_ = c.LLVMBuildRet(self.builder, ok_ret);
// Error: resolve the tag name, report to stderr, exit 1.
c.LLVMPositionBuilderAtEnd(self.builder, err_bb);
const global = self.getOrBuildTagNameArray();
const idx = c.LLVMBuildZExt(self.builder, tag_i32, self.cached_i64, "main.tagidx");
const string_ty = self.getStringStructType();
const n: u32 = @intCast(self.ir_mod.types.tags.names.items.len);
const array_ty = c.LLVMArrayType(string_ty, n);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, idx };
const gep = c.LLVMBuildInBoundsGEP2(self.builder, array_ty, global, &indices, 2, "main.tag.gep");
const name_struct = c.LLVMBuildLoad2(self.builder, string_ty, gep, "main.tag.name");
const name_ptr = c.LLVMBuildExtractValue(self.builder, name_struct, 0, "main.tag.ptr");
const name_len = c.LLVMBuildExtractValue(self.builder, name_struct, 1, "main.tag.len");
const reporter, const reporter_ty = self.lazyDeclareCRuntime(
"sx_trace_report_unhandled",
&[_]c.LLVMTypeRef{ self.cached_i32, self.cached_ptr, self.cached_i64 },
self.cached_void,
0,
);
var args = [3]c.LLVMValueRef{ tag_i32, name_ptr, name_len };
_ = c.LLVMBuildCall2(self.builder, reporter_ty, reporter, &args, 3, "");
_ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 1, 0));
}
/// The always-linked tag-name table: a `[N x {ptr, i64}]` global of tag
/// names indexed by global tag id (the `TagRegistry` namespace; slot 0 is
/// the reserved "" no-error name). `error_tag_name_get` GEPs into it at the
/// runtime tag id. Built once per module. Always emitted (not trace-gated)
/// so `{}` interpolation of an error tag works even in release builds.
fn getOrBuildTagNameArray(self: *LLVMEmitter) c.LLVMValueRef {
if (self.tag_name_array) |g| return g;
const string_ty = self.getStringStructType();
const names = self.ir_mod.types.tags.names.items;
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (names) |name_str| {
const str_z = self.alloc.dupeZ(u8, name_str) catch unreachable;
defer self.alloc.free(str_z);
const global_str = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(name_str.len + 1)), "tag.str");
c.LLVMSetInitializer(global_str, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(name_str.len + 1), 1));
c.LLVMSetGlobalConstant(global_str, 1);
c.LLVMSetLinkage(global_str, c.LLVMPrivateLinkage);
const len_val = c.LLVMConstInt(self.cached_i64, name_str.len, 0);
var struct_fields = [2]c.LLVMValueRef{ global_str, len_val };
const const_struct = c.LLVMConstStructInContext(self.context, &struct_fields, 2, 0);
field_vals.append(self.alloc, const_struct) catch unreachable;
}
const n: u32 = @intCast(names.len);
const array_ty = c.LLVMArrayType(string_ty, n);
const array_init = c.LLVMConstArray(string_ty, field_vals.items.ptr, n);
const global = c.LLVMAddGlobal(self.llvm_module, array_ty, "tag_names");
c.LLVMSetInitializer(global, array_init);
c.LLVMSetGlobalConstant(global, 1);
c.LLVMSetLinkage(global, c.LLVMPrivateLinkage);
self.tag_name_array = global;
return global;
}
/// Emit field_value_get: switch on runtime index, each case extracts a field and boxes it as Any.
fn emitFieldValueGet(self: *LLVMEmitter, fr: ir_inst.FieldReflect, func_idx: u32) void {
const base_val = self.resolveRef(fr.base);
const idx_val = self.resolveRef(fr.index);
const info = self.ir_mod.types.get(fr.struct_type);
const fields = switch (info) {
.@"struct" => |s| s.fields,
.@"union" => |u| u.fields,
.tagged_union => |u| u.fields,
else => &[_]TypeInfo.StructInfo.Field{},
};
if (fields.len == 0) {
// No fields (e.g., plain enum) — return void-tagged Any so payload is empty
const any_ty = self.getAnyStructType();
const void_tag = c.LLVMConstInt(self.cached_i64, TypeId.void.index(), 0);
var void_any = c.LLVMGetUndef(any_ty);
void_any = c.LLVMBuildInsertValue(self.builder, void_any, void_tag, 0, "fv.vtag");
void_any = c.LLVMBuildInsertValue(self.builder, void_any, c.LLVMConstInt(self.cached_i64, 0, 0), 1, "fv.vval");
self.mapRef(void_any);
return;
}
const any_ty = self.getAnyStructType();
const current_func = self.func_map.get(func_idx) orelse {
self.mapRef(c.LLVMGetUndef(any_ty));
return;
};
// Create merge block
const merge_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.merge");
// Create default block (returns undef)
const default_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.default");
// Build switch
const switch_inst = c.LLVMBuildSwitch(self.builder, idx_val, default_bb, @intCast(fields.len));
// Emit case blocks
var case_blocks = std.ArrayList(c.LLVMBasicBlockRef).empty;
defer case_blocks.deinit(self.alloc);
var case_values = std.ArrayList(c.LLVMValueRef).empty;
defer case_values.deinit(self.alloc);
const is_union = info == .@"union" or info == .tagged_union;
for (fields, 0..) |field, i| {
const case_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.case");
c.LLVMAddCase(switch_inst, c.LLVMConstInt(self.cached_i64, @intCast(i), 0), case_bb);
c.LLVMPositionBuilderAtEnd(self.builder, case_bb);
var field_val: c.LLVMValueRef = undefined;
if (is_union) {
// Union: extract payload via alloca + GEP to payload area + bitcast + load
if (field.ty == .void) {
// Void variant has no payload — use zero
field_val = c.LLVMConstInt(self.cached_i64, 0, 0);
} else {
const base_ty = c.LLVMTypeOf(base_val);
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "fv.utmp");
_ = c.LLVMBuildStore(self.builder, base_val, tmp);
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "fv.pp");
const field_llvm_ty = self.toLLVMType(field.ty);
const typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "fv.cast");
field_val = c.LLVMBuildLoad2(self.builder, field_llvm_ty, typed_ptr, "fv.field");
}
} else {
// Struct: direct extractvalue by field index
field_val = c.LLVMBuildExtractValue(self.builder, base_val, @intCast(i), "fv.field");
}
// Box as Any: {type_tag, value_as_i64}
const tag = c.LLVMConstInt(self.cached_i64, self.anyTag(field.ty), 0);
const is_field_signed = self.isSignedTypeEx(field.ty);
const val_i64 = if (is_field_signed) self.coerceToI64Signed(field_val) else self.coerceToI64(field_val);
var any_val = c.LLVMGetUndef(any_ty);
any_val = c.LLVMBuildInsertValue(self.builder, any_val, tag, 0, "fv.tag");
any_val = c.LLVMBuildInsertValue(self.builder, any_val, val_i64, 1, "fv.val");
_ = c.LLVMBuildBr(self.builder, merge_bb);
case_blocks.append(self.alloc, case_bb) catch unreachable;
case_values.append(self.alloc, any_val) catch unreachable;
}
// Default block: return undef Any
c.LLVMPositionBuilderAtEnd(self.builder, default_bb);
_ = c.LLVMBuildBr(self.builder, merge_bb);
// Merge block: PHI
c.LLVMPositionBuilderAtEnd(self.builder, merge_bb);
const phi = c.LLVMBuildPhi(self.builder, any_ty, "fv.phi");
// Add incoming from case blocks
for (case_blocks.items, case_values.items) |bb, val| {
c.LLVMAddIncoming(phi, @constCast(&val), @constCast(&bb), 1);
}
// Add incoming from default block
const undef_any = c.LLVMGetUndef(any_ty);
c.LLVMAddIncoming(phi, @constCast(&undef_any), @constCast(&default_bb), 1);
self.mapRef(phi);
}
// ── Helpers ─────────────────────────────────────────────────────
fn makeBlockKey(func_idx: u32, block_idx: u32) u64 {
return (@as(u64, func_idx) << 32) | @as(u64, block_idx);
}
/// Dump the LLVM module to a string (for testing).
pub fn dumpToString(self: *LLVMEmitter) []const u8 {
const raw = c.LLVMPrintModuleToString(self.llvm_module);
return std.mem.span(raw);
}
/// Verify the LLVM module. Returns true if valid.
pub fn verify(self: *LLVMEmitter) bool {
return c.LLVMVerifyModule(self.llvm_module, c.LLVMReturnStatusAction, null) == 0;
}
/// Verify the LLVM module, returning an error message on failure.
pub fn verifyWithMessage(self: *LLVMEmitter) !void {
var err_msg: [*c]u8 = null;
if (c.LLVMVerifyModule(self.llvm_module, c.LLVMReturnStatusAction, &err_msg) != 0) {
if (err_msg != null) {
const msg = std.mem.span(err_msg);
// Dump IR to /tmp for debugging
_ = c.LLVMPrintModuleToFile(self.llvm_module, "/tmp/sx_debug.ll", null);
std.debug.print("LLVM verification failed: {s}\n", .{msg});
c.LLVMDisposeMessage(err_msg);
}
return error.VerificationFailed;
}
}
/// Print the LLVM IR to stderr.
pub fn printIR(self: *LLVMEmitter) void {
const ir_str = c.LLVMPrintModuleToString(self.llvm_module);
defer c.LLVMDisposeMessage(ir_str);
const len = std.mem.len(ir_str);
std.debug.print("{s}\n", .{ir_str[0..len]});
}
/// Emit the module as an object file to disk.
pub fn emitObject(self: *LLVMEmitter, output_path: [*:0]const u8) !void {
return self.emitToFile(output_path, c.LLVMObjectFile);
}
/// Emit the module as an assembly file to disk.
pub fn emitAssembly(self: *LLVMEmitter, output_path: [*:0]const u8) !void {
return self.emitToFile(output_path, c.LLVMAssemblyFile);
}
/// Emit the module as LLVM bitcode to disk (for emcc to recompile with a newer LLVM).
pub fn emitBitcode(self: *LLVMEmitter, output_path: [*:0]const u8) !void {
if (c.LLVMWriteBitcodeToFile(self.llvm_module, output_path) != 0) {
return error.EmitFailed;
}
}
/// Dump the LLVM IR to a file for debugging.
pub fn dumpIRToFile(self: *LLVMEmitter, path: [*:0]const u8) void {
_ = c.LLVMPrintModuleToFile(self.llvm_module, path, null);
}
/// Emit the module as an object file to a memory buffer (for JIT).
pub fn emitObjectToMemory(self: *LLVMEmitter) !c.LLVMMemoryBufferRef {
const tm = self.target_machine orelse return error.NoTargetMachine;
var err_msg: [*c]u8 = null;
var buf: c.LLVMMemoryBufferRef = null;
if (c.LLVMTargetMachineEmitToMemoryBuffer(tm, self.llvm_module, c.LLVMObjectFile, &err_msg, &buf) != 0) {
if (err_msg != null) {
std.debug.print("failed to emit object to memory: {s}\n", .{std.mem.span(err_msg)});
c.LLVMDisposeMessage(err_msg);
}
return error.EmitFailed;
}
return buf;
}
fn emitToFile(self: *LLVMEmitter, output_path: [*:0]const u8, file_type: c.LLVMCodeGenFileType) !void {
const tm = self.target_machine orelse return error.NoTargetMachine;
var err_msg: [*c]u8 = null;
if (c.LLVMTargetMachineEmitToFile(tm, self.llvm_module, output_path, file_type, &err_msg) != 0) {
if (err_msg != null) {
std.debug.print("failed to emit file: {s}\n", .{std.mem.span(err_msg)});
c.LLVMDisposeMessage(err_msg);
}
return error.EmitFailed;
}
}
/// Check if an IR Ref's type is an unsigned integer (u8, u16, u32, u64).
fn isRefUnsigned(self: *LLVMEmitter, ref: Ref) bool {
if (ref.isNone()) return false;
const func = &self.ir_mod.functions.items[self.current_func_idx];
const ref_idx = ref.index();
// Check function parameters first (refs 0..N-1)
if (ref_idx < func.params.len) {
const ty = func.params[ref_idx].ty;
return ty == .u8 or ty == .u16 or ty == .u32 or ty == .u64;
}
for (func.blocks.items) |*block| {
const first = block.first_ref;
if (ref_idx >= first and ref_idx < first + @as(u32, @intCast(block.insts.items.len))) {
const ty = block.insts.items[ref_idx - first].ty;
return ty == .u8 or ty == .u16 or ty == .u32 or ty == .u64;
}
}
return false;
}
};
// ── Type classification helpers ─────────────────────────────────────
fn isFloatType(ty: TypeId) bool {
return ty == .f32 or ty == .f64;
}
/// Check if a TypeId is a float type, including float vectors.
fn isFloatOrVecFloat(ty: TypeId, types: *const TypeTable) bool {
if (ty == .f32 or ty == .f64) return true;
if (!ty.isBuiltin()) {
const info = types.get(ty);
if (info == .vector) return info.vector.element == .f32 or info.vector.element == .f64;
}
return false;
}
fn isSignedType(ty: TypeId) bool {
return switch (ty) {
.s8, .s16, .s32, .s64, .isize => true,
else => false,
};
}
fn floatBits(ty: TypeId) u32 {
return switch (ty) {
.f32 => 32,
.f64 => 64,
else => 0,
};
}
fn intBits(ty: TypeId) u32 {
return switch (ty) {
.s8, .u8 => 8,
.s16, .u16 => 16,
.s32, .u32 => 32,
.s64, .u64 => 64,
.bool => 1,
.usize, .isize => 0, // target-dependent — caller must query pointer_size
else => 64,
};
}