From ba9c4d69ced697bed107a93a555a3e19f323b23a Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 2 Mar 2026 09:49:43 +0200 Subject: [PATCH] wasm --- examples/modules/opengl.sx | 37 +++++ src/ir/emit_llvm.zig | 175 +++++++++++++++++----- src/ir/lower.zig | 288 +++++++++++++++++++++++++++++-------- src/ir/module.zig | 4 + src/main.zig | 19 ++- src/target.zig | 40 +++++- 6 files changed, 460 insertions(+), 103 deletions(-) diff --git a/examples/modules/opengl.sx b/examples/modules/opengl.sx index cac5add..e9acb27 100644 --- a/examples/modules/opengl.sx +++ b/examples/modules/opengl.sx @@ -59,6 +59,32 @@ glDepthFunc : (u32) -> void = ---; glUniform1f : (s32, f32) -> void = ---; GL_LESS :u32: 0x0201; GL_LEQUAL :u32: 0x0203; +GL_SCISSOR_TEST :u32: 0x0C11; +GL_DYNAMIC_DRAW :u32: 0x88E8; +GL_TEXTURE_2D :u32: 0x0DE1; +GL_TEXTURE_MIN_FILTER :u32: 0x2801; +GL_TEXTURE_MAG_FILTER :u32: 0x2800; +GL_NEAREST :u32: 0x2600; +GL_RGBA :u32: 0x1908; +GL_UNSIGNED_BYTE :u32: 0x1401; +GL_SRC_ALPHA :u32: 0x0302; +GL_ONE_MINUS_SRC_ALPHA :u32: 0x0303; +GL_TEXTURE0 :u32: 0x84C0; +GL_LINEAR :u32: 0x2601; +GL_RED :u32: 0x1903; +GL_UNPACK_ALIGNMENT :u32: 0x0CF5; + +glScissor : (s32, s32, s32, s32) -> void = ---; +glBufferSubData : (u32, s64, s64, *void) -> void = ---; +glGenTextures : (s32, *u32) -> void = ---; +glBindTexture : (u32, u32) -> void = ---; +glTexImage2D : (u32, s32, s32, s32, s32, s32, u32, u32, *void) -> void = ---; +glTexParameteri : (u32, u32, s32) -> void = ---; +glBlendFunc : (u32, u32) -> void = ---; +glReadPixels : (s32, s32, s32, s32, u32, u32, *void) -> void = ---; +glActiveTexture : (u32) -> void = ---; +glUniform1i : (s32, s32) -> void = ---; +glPixelStorei : (u32, s32) -> void = ---; // Loader: call once after creating GL context // Pass in a proc loader (e.g. SDL_GL_GetProcAddress) @@ -95,6 +121,17 @@ load_gl :: (get_proc: ([:0]u8) -> *void) { glUniform3f = xx get_proc("glUniform3f"); glDepthFunc = xx get_proc("glDepthFunc"); glUniform1f = xx get_proc("glUniform1f"); + glScissor = xx get_proc("glScissor"); + glBufferSubData = xx get_proc("glBufferSubData"); + glGenTextures = xx get_proc("glGenTextures"); + glBindTexture = xx get_proc("glBindTexture"); + glTexImage2D = xx get_proc("glTexImage2D"); + glTexParameteri = xx get_proc("glTexParameteri"); + glBlendFunc = xx get_proc("glBlendFunc"); + glReadPixels = xx get_proc("glReadPixels"); + glActiveTexture = xx get_proc("glActiveTexture"); + glUniform1i = xx get_proc("glUniform1i"); + glPixelStorei = xx get_proc("glPixelStorei"); } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 17f215a..e0585a6 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -83,6 +83,9 @@ pub const LLVMEmitter = struct { // Cached field name arrays for reflection (TypeId → LLVM global) field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef), + // Target configuration (stored for ABI decisions during emission) + target_config: TargetConfig, + const PendingPhi = struct { phi: c.LLVMValueRef, block_id: BlockId, // the block this phi belongs to @@ -154,6 +157,7 @@ pub const LLVMEmitter = struct { .any_struct_type = null, .closure_struct_type = null, .field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), + .target_config = target_config, }; } @@ -195,6 +199,9 @@ pub const LLVMEmitter = struct { /// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types. fn verifySizes(self: *LLVMEmitter) void { + // Skip for WASM: wasm32 has 4-byte pointers vs IR's assumed 8-byte, + // so struct sizes will differ. LLVM handles emission correctly. + if (self.target_config.isWasm()) return; const dl = c.LLVMGetModuleDataLayout(self.llvm_module); if (dl == null) return; const type_count = self.ir_mod.types.infos.items.len; @@ -290,6 +297,19 @@ pub const LLVMEmitter = struct { 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) @@ -663,7 +683,7 @@ pub const LLVMEmitter = struct { }, .heap_alloc => |un| { // malloc(size) → *void - const size = self.resolveRef(un.operand); + const size = self.coerceArg(self.resolveRef(un.operand), self.sizeType()); const malloc_fn = self.getOrDeclareMalloc(); var args = [_]c.LLVMValueRef{size}; const result = c.LLVMBuildCall2( @@ -977,7 +997,7 @@ pub const LLVMEmitter = struct { if (val_w > elt_w) { field_val = c.LLVMBuildTrunc(self.builder, field_val, elt, "atrunc"); } else if (val_w < elt_w) { - field_val = c.LLVMBuildSExt(self.builder, field_val, elt, "aext"); + field_val = c.LLVMBuildZExt(self.builder, field_val, elt, "aext"); } } } @@ -1277,12 +1297,12 @@ pub const LLVMEmitter = struct { 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 + // Normalize lo/hi to i64 for consistent arithmetic (indices are unsigned) if (c.LLVMTypeOf(lo) != self.cached_i64) { - lo = c.LLVMBuildSExt(self.builder, lo, self.cached_i64, "ss.lo64"); + lo = c.LLVMBuildZExt(self.builder, lo, self.cached_i64, "ss.lo64"); } if (c.LLVMTypeOf(hi) != self.cached_i64) { - hi = c.LLVMBuildSExt(self.builder, hi, self.cached_i64, "ss.hi64"); + hi = c.LLVMBuildZExt(self.builder, hi, self.cached_i64, "ss.hi64"); } const base_ty = c.LLVMTypeOf(base); const base_kind = c.LLVMGetTypeKind(base_ty); @@ -1354,7 +1374,7 @@ pub const LLVMEmitter = struct { // Builtins that map to libc functions or LLVM intrinsics switch (bi.builtin) { .malloc => { - const size = self.resolveRef(bi.args[0]); + const size = self.coerceArg(self.resolveRef(bi.args[0]), self.sizeType()); const malloc_fn = self.getOrDeclareMalloc(); var args = [_]c.LLVMValueRef{size}; self.mapRef(c.LLVMBuildCall2(self.builder, self.getMallocType(), malloc_fn, &args, 1, "malloc")); @@ -1369,7 +1389,7 @@ pub const LLVMEmitter = struct { .memcpy => { const dst = self.resolveRef(bi.args[0]); const src = self.resolveRef(bi.args[1]); - const len = self.resolveRef(bi.args[2]); + const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType()); const memcpy_fn = self.getOrDeclareMemcpy(); var args = [_]c.LLVMValueRef{ dst, src, len }; _ = c.LLVMBuildCall2(self.builder, self.getMemcpyType(), memcpy_fn, &args, 3, ""); @@ -1378,11 +1398,9 @@ pub const LLVMEmitter = struct { .memset => { const dst = self.resolveRef(bi.args[0]); var val = self.resolveRef(bi.args[1]); - const len = self.resolveRef(bi.args[2]); - // memset expects i32 for byte value — truncate if needed - if (c.LLVMTypeOf(val) != self.cached_i32) { - val = c.LLVMBuildTrunc(self.builder, val, self.cached_i32, "ms.trunc"); - } + const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType()); + // memset expects i32 for byte value — coerce width + val = self.coerceArg(val, self.cached_i32); const memset_fn = self.getOrDeclareMemset(); var args = [_]c.LLVMValueRef{ dst, val, len }; _ = c.LLVMBuildCall2(self.builder, self.getMemsetType(), memset_fn, &args, 3, ""); @@ -1408,11 +1426,16 @@ pub const LLVMEmitter = struct { 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.isWasm()) + 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, - str_len, + count, }; _ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, ""); self.advanceRefCounter(); @@ -1579,15 +1602,26 @@ pub const LLVMEmitter = struct { const a_ty = c.LLVMTypeOf(a); const kind = c.LLVMGetTypeKind(a_ty); if (kind == c.LLVMStructTypeKind) { - const has = c.LLVMBuildExtractValue(self.builder, a, 1, "oc.has"); - const unwrapped = c.LLVMBuildExtractValue(self.builder, a, 0, "oc.val"); - // Coerce b_val to match unwrapped type - 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); + 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")); } - self.mapRef(c.LLVMBuildSelect(self.builder, has, unwrapped, 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"); @@ -1927,8 +1961,14 @@ pub const LLVMEmitter = struct { if (kind == c.LLVMIntegerTypeKind and rhs_kind == c.LLVMIntegerTypeKind) { const lw = c.LLVMGetIntTypeWidth(lhs_ty); const rw = c.LLVMGetIntTypeWidth(rhs_ty); - 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"); + 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) { @@ -1981,7 +2021,6 @@ pub const LLVMEmitter = struct { const lhs = self.resolveRef(bin.lhs); const rhs = self.resolveRef(bin.rhs); const b = self.builder; - const i64_ty = c.LLVMInt64TypeInContext(self.context); const i32_ty = c.LLVMInt32TypeInContext(self.context); const i1_ty = c.LLVMInt1TypeInContext(self.context); const ptr_ty = c.LLVMPointerTypeInContext(self.context, 0); @@ -2005,12 +2044,14 @@ pub const LLVMEmitter = struct { // 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, i64_ty }; + var params = [_]c.LLVMTypeRef{ ptr_ty, ptr_ty, size_ty }; const fn_type = c.LLVMFunctionType(i32_ty, ¶ms, 3, 0); break :blk c.LLVMAddFunction(self.llvm_module, "memcmp", fn_type); }; - var args = [_]c.LLVMValueRef{ lhs_ptr, rhs_ptr, lhs_len }; + 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"); @@ -2090,9 +2131,14 @@ pub const LLVMEmitter = struct { 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. + fn sizeType(self: *LLVMEmitter) c.LLVMTypeRef { + return if (self.target_config.isWasm()) self.cached_i32 else self.cached_i64; + } + fn getMallocType(self: *LLVMEmitter) c.LLVMTypeRef { - // malloc(i64) → ptr - var param_types = [_]c.LLVMTypeRef{self.cached_i64}; + // malloc(size_t) → ptr + var param_types = [_]c.LLVMTypeRef{self.sizeType()}; return c.LLVMFunctionType(self.cached_ptr, ¶m_types, 1, 0); } @@ -2108,8 +2154,8 @@ pub const LLVMEmitter = struct { } fn getMemcpyType(self: *LLVMEmitter) c.LLVMTypeRef { - // memcpy(ptr, ptr, i64) → ptr - var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_i64 }; + // 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, ¶m_types, 3, 0); } @@ -2119,8 +2165,8 @@ pub const LLVMEmitter = struct { } fn getMemsetType(self: *LLVMEmitter) c.LLVMTypeRef { - // memset(ptr, i32, i64) → ptr - var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_i32, self.cached_i64 }; + // 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, ¶m_types, 3, 0); } @@ -2158,15 +2204,24 @@ pub const LLVMEmitter = struct { return c.LLVMFunctionType(self.cached_f32, ¶m_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, ¶m_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: i64) → i64 - var param_types = [_]c.LLVMTypeRef{ self.cached_i32, self.cached_ptr, self.cached_i64 }; - return c.LLVMFunctionType(self.cached_i64, ¶m_types, 3, 0); + // 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, ¶m_types, 3, 0); } fn getOrDeclareSnprintf(self: *LLVMEmitter) c.LLVMValueRef { @@ -2180,6 +2235,28 @@ pub const LLVMEmitter = struct { return c.LLVMFunctionType(self.cached_i32, ¶m_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(); @@ -2324,7 +2401,9 @@ pub const LLVMEmitter = struct { if (val_w > param_w) { return c.LLVMBuildTrunc(self.builder, val, param_ty, "ca.tr"); } else { - return c.LLVMBuildSExt(self.builder, val, param_ty, "ca.ext"); + // 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) @@ -2337,8 +2416,14 @@ pub const LLVMEmitter = struct { return c.LLVMBuildFPTrunc(self.builder, val, param_ty, "ca.fptrunc"); } } - // Int → Float + // 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 @@ -2592,13 +2677,22 @@ pub const LLVMEmitter = struct { // - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it) fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef { - // String/slice → raw pointer + // String/slice → raw pointer (universal across all targets for foreign calls) 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: i64 → i32 for C ABI (size_t/ssize_t are 32-bit on wasm32) + if (self.target_config.isWasm()) { + if (c.LLVMGetTypeKind(llvm_ty) == c.LLVMIntegerTypeKind and c.LLVMGetIntTypeWidth(llvm_ty) == 64) { + // s64/u64 in extern decls → i32 on wasm32 (matches C's size_t, ssize_t, etc.) + return self.cached_i32; + } + return llvm_ty; + } + // Only coerce struct types if (c.LLVMGetTypeKind(llvm_ty) != c.LLVMStructTypeKind) return llvm_ty; @@ -2947,6 +3041,11 @@ pub const LLVMEmitter = struct { 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))) { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d4e2e73..b3f6d61 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1063,7 +1063,12 @@ pub const Lowering = struct { // Fallback: global variable assignment if (!handled) { if (self.global_names.get(id.name)) |gi| { - self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = val } }, .void); + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void) + self.coerceToType(val, val_ty, gi.ty) + else + val; + self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void); } } }, @@ -1178,7 +1183,20 @@ pub const Lowering = struct { .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); if (asgn.op == .assign) { - self.builder.store(ptr, val); + const pointee_ty = blk: { + const ptr_ty = self.inferExprType(de.operand); + if (!ptr_ty.isBuiltin()) { + const info = self.module.types.get(ptr_ty); + if (info == .pointer) break :blk info.pointer.pointee; + } + break :blk ptr_ty; + }; + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) + self.coerceToType(val, val_ty, pointee_ty) + else + val; + self.builder.store(ptr, store_val); } else { const pointee_ty = self.inferExprType(de.operand); const elem_ty = blk: { @@ -1266,7 +1284,12 @@ pub const Lowering = struct { /// Store a value to a GEP, handling both plain and compound assignment. fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void { if (op == .assign) { - self.builder.store(gep, val); + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != ty and val_ty != .void and ty != .void) + self.coerceToType(val, val_ty, ty) + else + val; + self.builder.store(gep, store_val); } else { const loaded = self.builder.load(gep, ty); const result = self.emitCompoundOp(loaded, val, op, ty); @@ -1433,7 +1456,7 @@ pub const Lowering = struct { break :blk switch (uop.op) { .negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)), .not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool), - .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, .s64), + .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)), .xx => self.lowerXX(operand, uop.operand), .address_of => blk2: { const inner_ty = self.inferExprType(uop.operand); @@ -1522,6 +1545,33 @@ pub const Lowering = struct { } fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { + // Short-circuit: `a and b` → if a then b else false + if (bop.op == .and_op) { + const lhs = self.lowerExpr(bop.lhs); + const rhs_bb = self.freshBlock("and.rhs"); + const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool}); + const false_val = self.builder.constBool(false); + self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val}); + self.builder.switchToBlock(rhs_bb); + const rhs = self.lowerExpr(bop.rhs); + self.builder.br(merge_bb, &.{rhs}); + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, .bool); + } + // Short-circuit: `a or b` → if a then true else b + if (bop.op == .or_op) { + const lhs = self.lowerExpr(bop.lhs); + const rhs_bb = self.freshBlock("or.rhs"); + const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool}); + const true_val = self.builder.constBool(true); + self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{}); + self.builder.switchToBlock(rhs_bb); + const rhs = self.lowerExpr(bop.rhs); + self.builder.br(merge_bb, &.{rhs}); + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, .bool); + } + // Special case: optional == null / optional != null if (bop.op == .eq or bop.op == .neq) { const lhs_is_null = bop.lhs.data == .null_literal; @@ -1879,8 +1929,12 @@ pub const Lowering = struct { const saved_target = self.target_type; if (is_value and result_type != .void) self.target_type = result_type; if (is_value) { - const v = self.lowerExpr(ie.then_branch); + var v = self.lowerExpr(ie.then_branch); if (!self.currentBlockHasTerminator()) { + const v_ty = self.builder.getRefType(v); + if (v_ty != result_type and v_ty != .void and result_type != .void) { + v = self.coerceToType(v, v_ty, result_type); + } self.builder.br(merge_bb, &.{v}); } } else { @@ -1894,8 +1948,12 @@ pub const Lowering = struct { if (has_else) { self.builder.switchToBlock(else_bb.?); if (is_value) { - const v = self.lowerExpr(ie.else_branch.?); + var v = self.lowerExpr(ie.else_branch.?); if (!self.currentBlockHasTerminator()) { + const v_ty = self.builder.getRefType(v); + if (v_ty != result_type and v_ty != .void and result_type != .void) { + v = self.coerceToType(v, v_ty, result_type); + } self.builder.br(merge_bb, &.{v}); } } else { @@ -3225,9 +3283,37 @@ pub const Lowering = struct { fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { const lhs = self.lowerExpr(nc.lhs); - const rhs = self.lowerExpr(nc.rhs); const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); - return self.builder.emit(.{ .optional_coalesce = .{ .lhs = lhs, .rhs = rhs } }, inner_ty); + + // Short-circuit: only evaluate RHS if LHS is null. + // IMPORTANT: optional_unwrap must be in the "has value" branch, + // not before the condBr — the interpreter errors on unwrapping null. + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); + + const then_bb = self.freshBlock("nc.has"); + const rhs_bb = self.freshBlock("nc.rhs"); + const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); + + // If has value, go to then_bb to unwrap; else go to rhs_bb + self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); + + // Then block: unwrap LHS and branch to merge + self.builder.switchToBlock(then_bb); + const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); + self.builder.br(merge_bb, &.{unwrapped}); + + // RHS block: evaluate fallback and branch to merge + self.builder.switchToBlock(rhs_bb); + var rhs = self.lowerExpr(nc.rhs); + const rhs_ty = self.builder.getRefType(rhs); + if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { + rhs = self.coerceToType(rhs, rhs_ty, inner_ty); + } + self.builder.br(merge_bb, &.{rhs}); + + // Continue at merge + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, inner_ty); } fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { @@ -3443,13 +3529,15 @@ pub const Lowering = struct { } if (self.resolveFuncByName(func_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; // Pack variadic args into a slice if the function has a variadic param if (self.fn_ast_map.get(func_name)) |fd| { self.packVariadicCallArgs(fd, c, &args); } // Coerce arguments to match parameter types - self.coerceCallArgs(args.items, func.params); - return self.builder.call(fid, args.items, func.ret); + self.coerceCallArgs(args.items, params); + return self.builder.call(fid, args.items, ret_ty); } } // May be a variable holding a function pointer (non-closure) @@ -3600,11 +3688,13 @@ pub const Lowering = struct { } if (self.resolveFuncByName(effective_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; if (self.fn_ast_map.get(effective_name)) |fd| { self.packVariadicCallArgs(fd, c, &args); } - self.coerceCallArgs(args.items, func.params); - return self.builder.call(fid, args.items, func.ret); + self.coerceCallArgs(args.items, params); + return self.builder.call(fid, args.items, ret_ty); } // Check if this is Type.variant(payload) — qualified enum construction if (ns_name) |type_name| { @@ -3689,9 +3779,11 @@ pub const Lowering = struct { } if (self.resolveFuncByName(mangled)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; self.fixupMethodReceiver(&method_args, func, fa.object, obj_ty); - self.coerceCallArgs(method_args.items, func.params); - return self.builder.call(fid, method_args.items, func.ret); + self.coerceCallArgs(method_args.items, params); + return self.builder.call(fid, method_args.items, ret_ty); } } } @@ -3706,16 +3798,21 @@ pub const Lowering = struct { } if (self.resolveFuncByName(qualified)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; self.fixupMethodReceiver(&method_args, func, fa.object, obj_ty); - self.coerceCallArgs(method_args.items, func.params); - return self.builder.call(fid, method_args.items, func.ret); + // Note: coerceCallArgs can trigger protocol thunk creation + // (module.addFunction), invalidating func pointer. + // Use pre-extracted params/ret_ty instead of func.* after this. + self.coerceCallArgs(method_args.items, params); + return self.builder.call(fid, method_args.items, ret_ty); } } // Try to resolve as bare function name (method) if (self.resolveFuncByName(fa.field)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - return self.builder.call(fid, method_args.items, func.ret); + const ret_ty = self.module.functions.items[@intFromEnum(fid)].ret; + return self.builder.call(fid, method_args.items, ret_ty); } return self.emitPlaceholder(fa.field); }, @@ -3739,8 +3836,10 @@ pub const Lowering = struct { } if (self.resolveFuncByName(qualified)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; - self.coerceCallArgs(args.items, func.params); - return self.builder.call(fid, args.items, func.ret); + const ret_ty = func.ret; + const params = func.params; + self.coerceCallArgs(args.items, params); + return self.builder.call(fid, args.items, ret_ty); } } } @@ -4014,7 +4113,9 @@ pub const Lowering = struct { if (ret_ty != .void) { if (self.lowerBlockValue(lam.body)) |val| { if (!self.currentBlockHasTerminator()) { - self.builder.ret(val, ret_ty); + const val_ty = self.builder.getRefType(val); + const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; + self.builder.ret(coerced, ret_ty); } } } else { @@ -4243,6 +4344,33 @@ pub const Lowering = struct { .deref_expr => |de| { self.collectCaptures(de.operand, param_names, captures); }, + .for_expr => |fe| { + self.collectCaptures(fe.iterable, param_names, captures); + // Register capture name as local so it's not captured + param_names.put(fe.capture_name, {}) catch {}; + self.collectCaptures(fe.body, param_names, captures); + }, + .slice_expr => |se| { + self.collectCaptures(se.object, param_names, captures); + if (se.start) |s| self.collectCaptures(s, param_names, captures); + if (se.end) |e| self.collectCaptures(e, param_names, captures); + }, + .tuple_literal => |tl| { + for (tl.elements) |elem| { + self.collectCaptures(elem.value, param_names, captures); + } + }, + .force_unwrap => |fu| { + self.collectCaptures(fu.operand, param_names, captures); + }, + .chained_comparison => |cc| { + for (cc.operands) |op| { + self.collectCaptures(op, param_names, captures); + } + }, + .defer_stmt => |ds| { + self.collectCaptures(ds.expr, param_names, captures); + }, else => {}, } } @@ -4283,23 +4411,22 @@ pub const Lowering = struct { fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref { // a < b < c → (a < b) and (b < c) + // Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice. if (cc.operands.len < 2 or cc.ops.len == 0) { return self.builder.constBool(true); } - var result = self.emitCmp( - self.lowerExpr(cc.operands[0]), - self.lowerExpr(cc.operands[1]), - cc.ops[0], - ); + var refs = std.ArrayList(Ref).empty; + defer refs.deinit(self.alloc); + for (cc.operands) |op| { + refs.append(self.alloc, self.lowerExpr(op)) catch unreachable; + } + + var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]); var i: usize = 1; while (i < cc.ops.len) : (i += 1) { - const next_cmp = self.emitCmp( - self.lowerExpr(cc.operands[i]), - self.lowerExpr(cc.operands[i + 1]), - cc.ops[i], - ); + const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]); result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool); } @@ -4380,7 +4507,12 @@ pub const Lowering = struct { if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (binding.is_alloca) { - self.builder.store(binding.ref, val); + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) + self.coerceToType(val, val_ty, binding.ty) + else + val; + self.builder.store(binding.ref, store_val); } } } @@ -4390,16 +4522,21 @@ pub const Lowering = struct { const obj_ty = self.inferExprType(ie.object); const elem_ty = self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void) + self.coerceToType(val, val_ty, elem_ty) + else + val; // For fixed-size arrays, use the alloca pointer directly const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null; if (obj_alloca) |alloca_ref| { const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty); - self.builder.store(gep, val); + self.builder.store(gep, store_val); } else { const obj = self.lowerExpr(ie.object); const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty); - self.builder.store(gep, val); + self.builder.store(gep, store_val); } }, .field_access => |fa| { @@ -4417,11 +4554,29 @@ pub const Lowering = struct { } } const gep = self.builder.structGepTyped(obj_ptr, field_idx, field_ty, obj_ty); - self.builder.store(gep, val); + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != field_ty and val_ty != .void and field_ty != .void) + self.coerceToType(val, val_ty, field_ty) + else + val; + self.builder.store(gep, store_val); }, .deref_expr => |de| { const ptr = self.lowerExpr(de.operand); - self.builder.store(ptr, val); + const pointee_ty = blk: { + const ptr_ty = self.inferExprType(de.operand); + if (!ptr_ty.isBuiltin()) { + const info = self.module.types.get(ptr_ty); + if (info == .pointer) break :blk info.pointer.pointee; + } + break :blk ptr_ty; + }; + const val_ty = self.builder.getRefType(val); + const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void) + self.coerceToType(val, val_ty, pointee_ty) + else + val; + self.builder.store(ptr, store_val); }, else => { _ = self.emitPlaceholder("multi_assign_target"); @@ -4995,6 +5150,8 @@ pub const Lowering = struct { // Resolve the monomorphized function and call it (stripping type args) if (self.resolveFuncByName(mangled_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; // Build value-only args (skip type param declaration args) // Use separate index for lowered_args since type params don't consume call args var value_args = std.ArrayList(Ref).empty; @@ -5011,8 +5168,8 @@ pub const Lowering = struct { } arg_idx += 1; } - self.coerceCallArgs(value_args.items, func.params); - return self.builder.call(fid, value_args.items, func.ret); + self.coerceCallArgs(value_args.items, params); + return self.builder.call(fid, value_args.items, ret_ty); } return self.emitPlaceholder(base_name); @@ -5209,6 +5366,8 @@ pub const Lowering = struct { // Build call args (replace cast arg with unboxed value, skip type param decl args) if (self.resolveFuncByName(mangled_name)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; + const callee_ret = func.ret; + const callee_params = func.params; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); for (fd.params, 0..) |p, pi| { @@ -5221,8 +5380,8 @@ pub const Lowering = struct { } } } - self.coerceCallArgs(call_args.items, func.params); - const result = self.builder.call(fid, call_args.items, func.ret); + self.coerceCallArgs(call_args.items, callee_params); + const result = self.builder.call(fid, call_args.items, callee_ret); if (result_slot) |slot| { self.builder.store(slot, result); } @@ -5234,15 +5393,16 @@ pub const Lowering = struct { self.lazyLowerFunction(resolve_name); } if (self.resolveFuncByName(resolve_name)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; + const callee_ret = self.module.functions.items[@intFromEnum(fid)].ret; + const callee_params = self.module.functions.items[@intFromEnum(fid)].params; var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); for (fd.params, 0..) |_, pi| { if (pi == cast_arg_idx) { // Coerce unboxed value (typed as ty_id) to param type var arg = unboxed; - if (pi < func.params.len) { - arg = self.coerceToType(arg, ty_id, func.params[pi].ty); + if (pi < callee_params.len) { + arg = self.coerceToType(arg, ty_id, callee_params[pi].ty); } call_args.append(self.alloc, arg) catch unreachable; } else if (pi < other_args.items.len) { @@ -5252,12 +5412,12 @@ pub const Lowering = struct { } } // Coerce non-cast args (source type unknown, use s64 default) - for (0..@min(call_args.items.len, func.params.len)) |ci| { + for (0..@min(call_args.items.len, callee_params.len)) |ci| { if (ci != cast_arg_idx) { - call_args.items[ci] = self.coerceToType(call_args.items[ci], .s64, func.params[ci].ty); + call_args.items[ci] = self.coerceToType(call_args.items[ci], .s64, callee_params[ci].ty); } } - const result = self.builder.call(fid, call_args.items, func.ret); + const result = self.builder.call(fid, call_args.items, callee_ret); if (result_slot) |slot| { self.builder.store(slot, result); } @@ -5363,7 +5523,8 @@ pub const Lowering = struct { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| { - const coerced = self.coerceToType(val, .s64, ret_ty); + const val_ty = self.builder.getRefType(val); + const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; self.builder.ret(coerced, ret_ty); } else { self.ensureTerminator(ret_ty); @@ -7372,26 +7533,25 @@ pub const Lowering = struct { fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId { if (fd.return_type == null) return .void; - // Build type bindings from call args (same logic as lowerGenericCall) + // Build ALL type bindings from call args before resolving return type + var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc); + defer tmp_bindings.deinit(); + for (fd.type_params) |tp| { // Strategy 1: direct type param decl ($T: Type) — param.name == tp.name + var found = false; for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { if (pi < c.args.len) { const ty = self.resolveTypeArg(c.args[pi]); - // Resolve return type with this binding - const saved = self.type_bindings; - var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc); - defer tmp_bindings.deinit(); tmp_bindings.put(tp.name, ty) catch {}; - self.type_bindings = tmp_bindings; - const ret = self.resolveTypeWithBindings(fd.return_type.?); - self.type_bindings = saved; - return ret; } + found = true; break; } } + if (found) continue; + // Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest var inferred_ty: ?TypeId = null; for (fd.params, 0..) |param, pi| { @@ -7414,16 +7574,18 @@ pub const Lowering = struct { } } if (inferred_ty) |ty| { - const saved = self.type_bindings; - var tmp_bindings = std.StringHashMap(TypeId).init(self.alloc); - defer tmp_bindings.deinit(); tmp_bindings.put(tp.name, ty) catch {}; - self.type_bindings = tmp_bindings; - const ret = self.resolveTypeWithBindings(fd.return_type.?); - self.type_bindings = saved; - return ret; } } + + // Resolve return type with all bindings + if (tmp_bindings.count() > 0) { + const saved = self.type_bindings; + self.type_bindings = tmp_bindings; + const ret = self.resolveTypeWithBindings(fd.return_type.?); + self.type_bindings = saved; + return ret; + } return .s64; } diff --git a/src/ir/module.zig b/src/ir/module.zig index e4f2e27..1a8c905 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -195,6 +195,10 @@ pub const Builder = struct { if (self.func == null) return .s64; const func = self.currentFunc(); const ref_idx = @intFromEnum(ref); + // Check function parameters first (refs 0..N-1) + if (ref_idx < func.params.len) { + return func.params[ref_idx].ty; + } 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))) { diff --git a/src/main.zig b/src/main.zig index 4cc1006..d9943a3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,6 +23,7 @@ pub fn main(init: std.process.Init) !void { var input_path: ?[]const u8 = null; var target_config = sx.target.TargetConfig{}; var lib_paths = std.ArrayList([]const u8).empty; + var link_flags = std.ArrayList([]const u8).empty; var show_timing: bool = false; var explicit_opt: bool = false; var enable_cache: bool = false; @@ -70,6 +71,10 @@ pub fn main(init: std.process.Init) !void { if (i >= args.len) { std.debug.print("error: -L requires a value\n", .{}); return; } try lib_paths.append(allocator, args[i]); } + } else if (std.mem.eql(u8, arg, "--lflags")) { + i += 1; + if (i >= args.len) { std.debug.print("error: --lflags requires a value\n", .{}); return; } + try link_flags.append(allocator, args[i]); } else if (!std.mem.startsWith(u8, arg, "-")) { input_path = arg; } else { @@ -79,6 +84,7 @@ pub fn main(init: std.process.Init) !void { } target_config.lib_paths = try lib_paths.toOwnedSlice(allocator); + target_config.extra_link_flags = try link_flags.toOwnedSlice(allocator); const path = input_path orelse { printUsage(); @@ -86,7 +92,13 @@ pub fn main(init: std.process.Init) !void { }; if (std.mem.eql(u8, command, "build")) { - const output_name = target_config.output_path orelse deriveOutputName(path); + const output_name = target_config.output_path orelse blk: { + const base = deriveOutputName(path); + if (target_config.isEmscripten()) { + break :blk try std.fmt.allocPrint(allocator, "{s}.html", .{base}); + } + break :blk base; + }; compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch return; std.debug.print("compiled: {s}\n", .{output_name}); } else if (std.mem.eql(u8, command, "ir")) { @@ -96,6 +108,10 @@ pub fn main(init: std.process.Init) !void { } else if (std.mem.eql(u8, command, "asm")) { emitAsm(allocator, io, path, target_config) catch return; } else if (std.mem.eql(u8, command, "run")) { + if (target_config.isWasm()) { + std.debug.print("error: 'run' is not supported for wasm targets. Use 'build' instead.\n", .{}); + return; + } // Default to -O0 for run (faster compile) unless user explicitly set --opt if (!explicit_opt) target_config.opt_level = .none; var timer = Timing.init(show_timing); @@ -238,6 +254,7 @@ fn printUsage() void { \\ -L Library search path (repeatable) \\ --linker Linker command (default: cc) \\ --sysroot Sysroot for cross-compilation + \\ --lflags Extra linker flag (repeatable, e.g. --lflags -sUSE_SDL=2) \\ --cache Enable build caching \\ --time Show compilation timing breakdown \\ diff --git a/src/target.zig b/src/target.zig index 5aeac66..dedf528 100644 --- a/src/target.zig +++ b/src/target.zig @@ -19,6 +19,8 @@ pub const TargetConfig = struct { linker: ?[]const u8 = null, /// Sysroot for cross-compilation (passed as --sysroot to linker). sysroot: ?[]const u8 = null, + /// Extra flags passed through to the linker (e.g. Emscripten -s flags). + extra_link_flags: []const []const u8 = &.{}, pub const OptLevel = enum { none, @@ -51,6 +53,16 @@ pub const TargetConfig = struct { return self.tripleContains("windows") or self.tripleContains("win32"); } + /// Check if target triple indicates WebAssembly (wasm32 or wasm64). + pub fn isWasm(self: TargetConfig) bool { + return self.tripleHasPrefix("wasm32", "wasm64"); + } + + /// Check if target triple indicates Emscripten (contains "emscripten"). + pub fn isEmscripten(self: TargetConfig) bool { + return self.tripleContains("emscripten"); + } + fn tripleHasPrefix(self: TargetConfig, prefix1: []const u8, prefix2: []const u8) bool { if (self.triple) |t| { const span = std.mem.span(t); @@ -139,7 +151,28 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 { pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void { var argv = std.ArrayList([]const u8).empty; - if (target_config.isWindows()) { + if (target_config.isEmscripten()) { + // Emscripten: use emcc as the linker/driver + const linker = target_config.linker orelse "emcc"; + try argv.appendSlice(allocator, &.{ linker, output_obj, "-o", output_bin }); + for (extra_objects) |eo| try argv.append(allocator, eo); + + if (target_config.sysroot) |sr| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr})); + } + + for (target_config.lib_paths) |lp| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp})); + } + for (libraries) |lib| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); + } + + // Extra linker flags (e.g. -sUSE_SDL=2, -sUSE_WEBGL2=1, --preload-file) + for (target_config.extra_link_flags) |flag| { + try argv.append(allocator, flag); + } + } else if (target_config.isWindows()) { // Windows: MSVC-style linker flags const linker = target_config.linker orelse "link.exe"; try argv.appendSlice(allocator, &.{ linker, output_obj }); @@ -176,6 +209,11 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex for (libraries) |lib| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); } + + // Extra linker flags + for (target_config.extra_link_flags) |flag| { + try argv.append(allocator, flag); + } } const argv_slice = try argv.toOwnedSlice(allocator);