diff --git a/.gitignore b/.gitignore index 45d8dbe..b6bb4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ zig-out .DS_Store .vscode/ -.sx-cache \ No newline at end of file +.sx-cache +current/ \ No newline at end of file diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index b671a82..f6010a8 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1,5 +1,6 @@ #import "modules/std.sx"; #import "modules/math/math.sx"; +#import "modules/compiler.sx"; pkg :: #import "modules/testpkg"; // ============================================================ @@ -3064,5 +3065,60 @@ END; print("opt-if5: {}\n", val2 ?? 0.0); } + // --- usize / isize --- + { + a : usize = 42; + b : isize = 0 - 7; + print("usize: {}\n", a); + print("isize: {}\n", b); + + // arithmetic + c : usize = a + 8; + print("usize+8: {}\n", c); + + // coercion from s32 + x : s32 = 10; + y : usize = xx x; + print("s32->usize: {}\n", y); + + // coercion to s64 + z : s64 = xx a; + print("usize->s64: {}\n", z); + } + + // --- inline if (compile-time conditionals) --- + print("=== inline if ===\n"); + { + // POINTER_SIZE is 8 on desktop (64-bit) + inline if POINTER_SIZE == 8 { + print("64-bit\n"); + } else { + print("32-bit\n"); + } + + // OS enum comparison + inline if OS == .wasm { + print("wasm\n"); + } else { + print("not wasm\n"); + } + + // != comparison + inline if OS != .unknown { + print("known os\n"); + } else { + print("unknown os\n"); + } + + // nested inline if + inline if POINTER_SIZE != 4 { + inline if OS != .wasm { + print("desktop 64-bit\n"); + } else { + print("wasm 64-bit??\n"); + } + } + } + print("=== DONE ===\n"); } diff --git a/examples/modules/compiler.sx b/examples/modules/compiler.sx new file mode 100644 index 0000000..80f831b --- /dev/null +++ b/examples/modules/compiler.sx @@ -0,0 +1,6 @@ +OperatingSystem :: enum { macos; linux; windows; wasm; unknown; } +Architecture :: enum { aarch64; x86_64; wasm32; unknown; } + +OS : OperatingSystem = .unknown; +ARCH : Architecture = .unknown; +POINTER_SIZE : s64 = 8; diff --git a/examples/modules/opengl.sx b/examples/modules/opengl.sx index e9acb27..2a4c368 100644 --- a/examples/modules/opengl.sx +++ b/examples/modules/opengl.sx @@ -49,11 +49,11 @@ glGenVertexArrays : (s32, *u32) -> void = ---; glGenBuffers : (s32, *u32) -> void = ---; glBindVertexArray : (u32) -> void = ---; glBindBuffer : (u32, u32) -> void = ---; -glBufferData : (u32, s64, *void, u32) -> void = ---; +glBufferData : (u32, isize, *void, u32) -> void = ---; glVertexAttribPointer : (u32, s32, u32, u8, s32, *void) -> void = ---; glEnableVertexAttribArray : (u32) -> void = ---; -glGetUniformLocation : (u32, [:0]u8) -> s32 = ---; -glUniformMatrix4fv : (s32, s32, u8, [16]f32) -> void = ---; +glGetUniformLocation : (u32, [*]u8) -> s32 = ---; +glUniformMatrix4fv : (s32, s32, u8, [*]f32) -> void = ---; glUniform3f : (s32, f32, f32, f32) -> void = ---; glDepthFunc : (u32) -> void = ---; glUniform1f : (s32, f32) -> void = ---; @@ -72,10 +72,11 @@ GL_ONE_MINUS_SRC_ALPHA :u32: 0x0303; GL_TEXTURE0 :u32: 0x84C0; GL_LINEAR :u32: 0x2601; GL_RED :u32: 0x1903; +GL_R8 :u32: 0x8229; GL_UNPACK_ALIGNMENT :u32: 0x0CF5; glScissor : (s32, s32, s32, s32) -> void = ---; -glBufferSubData : (u32, s64, s64, *void) -> void = ---; +glBufferSubData : (u32, isize, isize, *void) -> void = ---; glGenTextures : (s32, *u32) -> void = ---; glBindTexture : (u32, u32) -> void = ---; glTexImage2D : (u32, s32, s32, s32, s32, s32, u32, u32, *void) -> void = ---; @@ -88,7 +89,7 @@ glPixelStorei : (u32, s32) -> void = ---; // Loader: call once after creating GL context // Pass in a proc loader (e.g. SDL_GL_GetProcAddress) -load_gl :: (get_proc: ([:0]u8) -> *void) { +load_gl :: (get_proc: ([*]u8) -> *void) { glClearColor = xx get_proc("glClearColor"); glClear = xx get_proc("glClear"); glEnable = xx get_proc("glEnable"); diff --git a/examples/modules/sdl3.sx b/examples/modules/sdl3.sx index 77c0465..678031c 100644 --- a/examples/modules/sdl3.sx +++ b/examples/modules/sdl3.sx @@ -16,6 +16,7 @@ SDL_GL_CONTEXT_PROFILE_MASK :s32: 20; // SDL_GLProfile SDL_GL_CONTEXT_PROFILE_CORE :s32: 0x1; +SDL_GL_CONTEXT_PROFILE_ES :s32: 0x4; // SDL_GLContextFlag SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG :s32: 0x2; diff --git a/examples/modules/socket.sx b/examples/modules/socket.sx index ebc0eab..29f3177 100644 --- a/examples/modules/socket.sx +++ b/examples/modules/socket.sx @@ -9,8 +9,8 @@ setsockopt :: (fd: s32, level: s32, optname: s32, optval: *s32, optlen: u32) -> bind :: (fd: s32, addr: *SockAddr, addrlen: u32) -> s32 #foreign libc; listen :: (fd: s32, backlog: s32) -> s32 #foreign libc; accept :: (fd: s32, addr: *SockAddr, addrlen: *u32) -> s32 #foreign libc; -read :: (fd: s32, buf: [*]u8, count: s64) -> s64 #foreign libc; -write :: (fd: s32, buf: [*]u8, count: s64) -> s64 #foreign libc; +read :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc; +write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc; close :: (fd: s32) -> s32 #foreign libc; // Constants (macOS) diff --git a/src/ast.zig b/src/ast.zig index 26ef935..a4ddb9c 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -208,6 +208,7 @@ pub const IfExpr = struct { then_branch: *Node, else_branch: ?*Node, is_inline: bool, // true for `if cond then a else b` + is_comptime: bool = false, // true for `inline if` — compile-time branch elimination binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding }; diff --git a/src/c_import.zig b/src/c_import.zig index 5edd010..04a13d4 100644 --- a/src/c_import.zig +++ b/src/c_import.zig @@ -303,6 +303,53 @@ pub fn loadCObjectsForJIT( }; } +/// Compile C sources using emcc for Emscripten/WASM targets. +/// Shells out to `emcc -c` for each source file, returns temp object file paths. +pub fn compileCWithEmcc( + allocator: std.mem.Allocator, + io: std.Io, + infos: []const CImportInfo, +) ![]const []const u8 { + var paths = std.ArrayList([]const u8).empty; + var obj_idx: usize = 0; + + for (infos) |info| { + if (info.sources.len == 0) continue; + + for (info.sources) |src| { + const out_path = try std.fmt.allocPrint(allocator, "/tmp/sx_emcc_{d}.o", .{obj_idx}); + obj_idx += 1; + + var argv = std.ArrayList([]const u8).empty; + try argv.appendSlice(allocator, &.{ "emcc", "-c", "-O2", src, "-o", out_path }); + // Add include paths + for (info.includes) |inc| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-I{s}", .{dirName(inc)})); + } + for (info.defines) |def| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-D{s}", .{def})); + } + for (info.flags) |flag| { + try argv.append(allocator, flag); + } + + const argv_slice = try argv.toOwnedSlice(allocator); + var child = std.process.spawn(io, .{ .argv = argv_slice }) catch { + std.debug.print("error: failed to spawn emcc for '{s}'\n", .{src}); + return error.CompileError; + }; + const result = child.wait(io) catch return error.CompileError; + if (result != .exited or result.exited != 0) { + std.debug.print("error: emcc failed for '{s}'\n", .{src}); + return error.CompileError; + } + try paths.append(allocator, out_path); + } + } + + return try paths.toOwnedSlice(allocator); +} + /// For build mode: write .o buffers to temp files, return paths for the linker. pub fn writeCObjectFiles( allocator: std.mem.Allocator, diff --git a/src/core.zig b/src/core.zig index fb67b2e..d96680c 100644 --- a/src/core.zig +++ b/src/core.zig @@ -116,9 +116,14 @@ pub const Compilation = struct { pub fn lowerToIR(self: *Compilation) ir.Module { const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator); var module = ir.Module.init(self.allocator); + //TODO: find a better place for this + if (self.target_config.isWasm()) { + module.types.pointer_size = 4; + } var lowering = ir.Lowering.init(&module); lowering.main_file = self.file_path; lowering.resolved_root = root; + lowering.target_config = self.target_config; lowering.lowerRoot(root); return module; } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index e0585a6..0335e91 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -2103,8 +2103,9 @@ pub const LLVMEmitter = struct { } // int→int: SExt, ZExt, or Trunc - const from_bits = intBits(from); - const to_bits = intBits(to); + 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") @@ -2539,6 +2540,7 @@ pub const LLVMEmitter = struct { .string => self.getStringStructType(), .any => self.getAnyStructType(), .noreturn => self.cached_void, + .isize, .usize => if (self.target_config.isWasm()) self.cached_i32 else self.cached_i64, else => self.toLLVMTypeInfo(ty), }; } @@ -2665,6 +2667,7 @@ pub const LLVMEmitter = struct { // For now, use opaque ptr return self.cached_ptr; }, + .usize, .isize => if (self.target_config.isWasm()) self.cached_i32 else self.cached_i64, }; } @@ -2684,12 +2687,11 @@ pub const LLVMEmitter = struct { if (info == .slice) return self.cached_ptr; } - // WASM32: i64 → i32 for C ABI (size_t/ssize_t are 32-bit on wasm32) + // 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.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; - } + if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32; return llvm_ty; } @@ -3075,7 +3077,7 @@ fn isFloatOrVecFloat(ty: TypeId, types: *const TypeTable) bool { fn isSignedType(ty: TypeId) bool { return switch (ty) { - .s8, .s16, .s32, .s64 => true, + .s8, .s16, .s32, .s64, .isize => true, else => false, }; } @@ -3095,6 +3097,7 @@ fn intBits(ty: TypeId) u32 { .s32, .u32 => 32, .s64, .u64 => 64, .bool => 1, + .usize, .isize => 0, // target-dependent — caller must query pointer_size else => 64, }; } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b3f6d61..fb4a207 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -104,6 +104,13 @@ pub const Lowering = struct { foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames type_alias_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // type alias name → target TypeId ufcs_alias_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // UFCS alias name → target function name + target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if) + comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH) + + pub const ComptimeValue = union(enum) { + int_val: i64, + enum_tag: struct { ty: TypeId, tag: u32 }, + }; const StructConstInfo = struct { value: *const Node, @@ -165,12 +172,68 @@ pub const Lowering = struct { }; // Pass 1: scan — register all function ASTs, struct types, extern stubs self.scanDecls(decls); + // Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config + self.injectComptimeConstants(); // Pass 2: lower main (and comptime side-effects) self.lowerMainAndComptime(decls); // Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered self.lowerDeferredTypeFns(); } + /// Inject compile-time constants from target_config into comptime_constants. + /// Called after scanDecls so that enum types (OperatingSystem, Architecture) are registered. + fn injectComptimeConstants(self: *Lowering) void { + const tc = self.target_config orelse return; + + // OS: OperatingSystem enum { macos; linux; windows; wasm; unknown; } + const os_name_id = self.module.types.internString("OperatingSystem"); + if (self.module.types.findByName(os_name_id)) |os_ty| { + const os_info = self.module.types.get(os_ty); + if (os_info == .@"enum") { + const tag: u32 = if (tc.isWasm()) + self.findVariantIndex(os_info.@"enum".variants, "wasm") + else if (tc.isWindows()) + self.findVariantIndex(os_info.@"enum".variants, "windows") + else if (tc.isLinux()) + self.findVariantIndex(os_info.@"enum".variants, "linux") + else if (tc.isMacOS()) + self.findVariantIndex(os_info.@"enum".variants, "macos") + else + self.findVariantIndex(os_info.@"enum".variants, "unknown"); + self.comptime_constants.put("OS", .{ .enum_tag = .{ .ty = os_ty, .tag = tag } }) catch {}; + } + } + + // ARCH: Architecture enum { aarch64; x86_64; wasm32; unknown; } + const arch_name_id = self.module.types.internString("Architecture"); + if (self.module.types.findByName(arch_name_id)) |arch_ty| { + const arch_info = self.module.types.get(arch_ty); + if (arch_info == .@"enum") { + const tag: u32 = if (tc.isWasm()) + self.findVariantIndex(arch_info.@"enum".variants, "wasm32") + else if (tc.isAarch64()) + self.findVariantIndex(arch_info.@"enum".variants, "aarch64") + else if (tc.isX86_64()) + self.findVariantIndex(arch_info.@"enum".variants, "x86_64") + else + self.findVariantIndex(arch_info.@"enum".variants, "unknown"); + self.comptime_constants.put("ARCH", .{ .enum_tag = .{ .ty = arch_ty, .tag = tag } }) catch {}; + } + } + + // POINTER_SIZE: s64 (4 for wasm, 8 otherwise) + const ptr_size: i64 = if (tc.isWasm()) 4 else 8; + self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {}; + } + + fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 { + const name_id = self.module.types.internString(name); + for (variants, 0..) |v, i| { + if (v == name_id) return @intCast(i); + } + return 0; // fallback to first variant + } + /// Lower functions that were deferred because they use type-category matching. /// At this point, main is fully lowered and all types are in the TypeTable. fn lowerDeferredTypeFns(self: *Lowering) void { @@ -381,6 +444,8 @@ pub const Lowering = struct { const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) { .undef_literal => .zeroinit, .int_literal => |il| .{ .int = il.value }, + .bool_literal => |bl| .{ .boolean = bl.value }, + .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, else => null, } else null; @@ -723,6 +788,15 @@ pub const Lowering = struct { } } + /// Lower an `inline if` branch — block body emits statements, expression returns value. + fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref { + if (node.data == .block) { + self.lowerBlock(node); + return self.builder.constInt(0, .void); + } + return self.lowerExpr(node); + } + /// Lower a block and return the last expression's value (for implicit returns). fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { // Set force_block_value so nested if-else expressions produce values @@ -1845,6 +1919,19 @@ pub const Lowering = struct { // ── Control flow ──────────────────────────────────────────────── fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { + // inline if: evaluate condition at compile time, only lower taken branch + if (ie.is_comptime) { + if (self.evalComptimeCondition(ie.condition)) |is_true| { + if (is_true) { + return self.lowerInlineBranch(ie.then_branch); + } else if (ie.else_branch) |eb| { + return self.lowerInlineBranch(eb); + } + return self.builder.constInt(0, .void); + } + // Condition couldn't be evaluated — fall through to runtime + } + // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors if (self.tryConstBoolCondition(ie.condition)) |is_true| { if (is_true) { @@ -1999,6 +2086,46 @@ pub const Lowering = struct { return null; } + /// Evaluate a compile-time condition for `inline if`. + /// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`. + fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool { + if (node.data != .binary_op) return null; + const bo = &node.data.binary_op; + if (bo.op != .eq and bo.op != .neq) return null; + + // LHS must be an identifier that's in comptime_constants + const name = switch (bo.lhs.data) { + .identifier => |id| id.name, + else => return null, + }; + const cv = self.comptime_constants.get(name) orelse return null; + + switch (cv) { + .enum_tag => |et| { + // RHS must be an enum literal (.variant) + const variant_name = switch (bo.rhs.data) { + .enum_literal => |el| el.name, + else => return null, + }; + // Look up variant index in the enum type + const enum_info = self.module.types.get(et.ty); + if (enum_info != .@"enum") return null; + const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name); + const result = et.tag == variant_idx; + return if (bo.op == .eq) result else !result; + }, + .int_val => |iv| { + // RHS must be an integer literal + const rhs_val: i64 = switch (bo.rhs.data) { + .int_literal => |il| il.value, + else => return null, + }; + const result = iv == rhs_val; + return if (bo.op == .eq) result else !result; + }, + } + } + fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { const header_bb = self.freshBlock("while.hdr"); const body_bb = self.freshBlock("while.body"); @@ -2857,6 +2984,10 @@ pub const Lowering = struct { const info = self.module.types.get(obj_ty); switch (info) { .tagged_union => |u| { + // .tag → extract the enum tag value with the correct tag type + if (std.mem.eql(u8, field, "tag")) { + return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); + } // Tagged union — use enum_payload for (u.fields, 0..) |f, i| { if (f.name == field_name_id) { @@ -3727,8 +3858,19 @@ pub const Lowering = struct { } // Method call: obj.method(args) → prepend obj (or &obj for *Self receivers) - const obj_ty = self.inferExprType(fa.object); - const obj = self.lowerExpr(fa.object); + // For ptr.*.method(): pass the pointer directly instead of loading + re-addressing. + // This ensures mutations through self: *T are visible after the call. + var obj_ty: TypeId = undefined; + var obj: Ref = undefined; + var effective_obj_node: *const Node = fa.object; + if (fa.object.data == .deref_expr) { + effective_obj_node = fa.object.data.deref_expr.operand; + obj_ty = self.inferExprType(effective_obj_node); + obj = self.lowerExpr(effective_obj_node); + } else { + obj_ty = self.inferExprType(fa.object); + obj = self.lowerExpr(fa.object); + } // Check if field is a closure type — call as closure, not method if (!obj_ty.isBuiltin()) { @@ -3781,7 +3923,7 @@ pub const Lowering = struct { 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.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); self.coerceCallArgs(method_args.items, params); return self.builder.call(fid, method_args.items, ret_ty); } @@ -3800,7 +3942,7 @@ pub const Lowering = struct { 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.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); // Note: coerceCallArgs can trigger protocol thunk creation // (module.addFunction), invalidating func pointer. // Use pre-extracted params/ret_ty instead of func.* after this. @@ -4005,6 +4147,14 @@ pub const Lowering = struct { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) break :blk tti.closure.params; + // Unwrap ?Closure(...) → Closure(...) + if (tti == .optional) { + const inner = tti.optional.child; + if (!inner.isBuiltin()) { + const inner_info = self.module.types.get(inner); + if (inner_info == .closure) break :blk inner_info.closure.params; + } + } } break :blk null; } else null; @@ -4031,6 +4181,14 @@ pub const Lowering = struct { if (!tt.isBuiltin()) { const tti = self.module.types.get(tt); if (tti == .closure) break :blk tti.closure.ret; + // Unwrap ?Closure(...) → Closure(...) + if (tti == .optional) { + const inner = tti.optional.child; + if (!inner.isBuiltin()) { + const inner_info = self.module.types.get(inner); + if (inner_info == .closure) break :blk inner_info.closure.ret; + } + } } } // Arrow lambda without explicit return type — infer from body expression @@ -4388,8 +4546,7 @@ pub const Lowering = struct { offset = (offset + field_align - 1) & ~(field_align - 1); offset += field_size; } - // Align total to struct alignment (max field alignment, minimum 8) - if (max_align < 8) max_align = 8; + // Align total to max field alignment (matches LLVM's struct alignment) return (offset + max_align - 1) & ~(max_align - 1); } @@ -5716,6 +5873,8 @@ pub const Lowering = struct { if (ty == .void) return "void"; if (ty == .string) return "string"; if (ty == .any) return "Any"; + if (ty == .usize) return "usize"; + if (ty == .isize) return "isize"; const info = self.module.types.get(ty); return switch (info) { @@ -5880,6 +6039,8 @@ pub const Lowering = struct { if (ty == .void) return "void"; if (ty == .string) return "string"; if (ty == .any) return "Any"; + if (ty == .usize) return "usize"; + if (ty == .isize) return "isize"; const info = self.module.types.get(ty); return switch (info) { @@ -5932,6 +6093,8 @@ pub const Lowering = struct { tags.append(self.alloc, TypeId.u16.index()) catch {}; tags.append(self.alloc, TypeId.u32.index()) catch {}; tags.append(self.alloc, TypeId.u64.index()) catch {}; + tags.append(self.alloc, TypeId.usize.index()) catch {}; + tags.append(self.alloc, TypeId.isize.index()) catch {}; return tags.items; } if (std.mem.eql(u8, name, "float")) { @@ -7220,6 +7383,7 @@ pub const Lowering = struct { // Build call args: ctx + user args // Protocol method params use *void for Self-typed params. If the caller passes // a struct value, we need to alloca+store and pass the pointer instead. + // Also coerce argument types to match declared param types (e.g., s64 → s32). var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); call_args.append(self.alloc, ctx) catch unreachable; @@ -7233,7 +7397,9 @@ pub const Lowering = struct { self.builder.store(slot, a); call_args.append(self.alloc, slot) catch unreachable; } else { - call_args.append(self.alloc, a) catch unreachable; + // Coerce to match declared parameter type (critical for WASM strict signatures) + const coerced = self.coerceToType(a, arg_ty, expected_ty); + call_args.append(self.alloc, coerced) catch unreachable; } } const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable; @@ -8010,7 +8176,7 @@ pub const Lowering = struct { fn isInt(ty: TypeId) bool { return switch (ty) { - .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64 => true, + .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true, else => false, }; } @@ -8034,6 +8200,7 @@ pub const Lowering = struct { .s16, .u16 => 16, .s32, .u32 => 32, .s64, .u64 => 64, + .usize, .isize => 0, // target-dependent — use typeBitsEx .f32 => 32, .f64 => 64, else => 0, @@ -8041,6 +8208,7 @@ pub const Lowering = struct { } fn typeBitsEx(self: *Lowering, ty: TypeId) u32 { + if (ty == .usize or ty == .isize) return @as(u32, self.module.types.pointer_size) * 8; const b = typeBits(ty); if (b > 0) return b; if (!ty.isBuiltin()) { diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 94e498c..5d6db8f 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -65,6 +65,8 @@ pub fn bridgeType(ty: sx_types.Type, table: *TypeTable) TypeId { .boolean => .bool, .string_type => .string, .any_type => .any, + .usize_type => .usize, + .isize_type => .isize, .enum_type => |name| resolveNamedType(name, .@"enum", table), .struct_type => |name| resolveNamedType(name, .@"struct", table), .union_type => |name| resolveNamedType(name, .@"union", table), @@ -222,6 +224,8 @@ pub fn resolveTypePrimitive(name: []const u8) ?TypeId { if (std.mem.eql(u8, name, "Any")) return .any; if (std.mem.eql(u8, name, "Type")) return .any; // Type-as-value: boxed string if (std.mem.eql(u8, name, "noreturn")) return .noreturn; + if (std.mem.eql(u8, name, "usize")) return .usize; + if (std.mem.eql(u8, name, "isize")) return .isize; return null; } diff --git a/src/ir/types.zig b/src/ir/types.zig index 9cf4f62..586827f 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -21,10 +21,11 @@ pub const TypeId = enum(u32) { string = 12, // [:0]u8 any = 13, noreturn = 14, - _reserved = 15, - _, // user-defined types start at 16 + isize = 15, + usize = 16, + _, // user-defined types start at 17 - pub const first_user: u32 = 16; + pub const first_user: u32 = 17; pub fn index(self: TypeId) u32 { return @intFromEnum(self); @@ -69,6 +70,8 @@ pub const TypeInfo = union(enum) { any, protocol: ProtocolInfo, noreturn, + usize, + isize, pub const StructInfo = struct { name: StringId, @@ -225,6 +228,8 @@ pub const TypeTable = struct { /// Maps TypeInfo → TypeId for dedup of structural types intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80), alloc: Allocator, + /// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets). + pointer_size: u8 = 8, pub fn init(alloc: Allocator) TypeTable { var table = TypeTable{ @@ -251,7 +256,8 @@ pub const TypeTable = struct { .string, // 12 .any, // 13 .noreturn, // 14 - .void, // 15: reserved (placeholder) + .isize, // 15: isize (pointer-sized signed) + .usize, // 16: usize (pointer-sized unsigned) }; for (&builtins) |info| { table.infos.append(alloc, info) catch unreachable; @@ -402,25 +408,27 @@ pub const TypeTable = struct { /// This is the authoritative size computation used for closure env sizing and /// verified against LLVMABISizeOfType. pub fn typeSizeBytes(self: *const TypeTable, ty: TypeId) usize { + const ptr_size: usize = self.pointer_size; if (ty == .void) return 0; if (ty == .bool) return 1; if (ty == .u8 or ty == .s8) return 1; if (ty == .u16 or ty == .s16) return 2; if (ty == .s32 or ty == .u32 or ty == .f32) return 4; if (ty == .s64 or ty == .u64 or ty == .f64) return 8; - if (ty == .string) return 16; // {ptr, i64} - if (ty.isBuiltin()) return 8; // default for unknown builtins + if (ty == .usize or ty == .isize) return ptr_size; + if (ty == .string) return 16; // {ptr, i64} — always 16 (i64 alignment pads on wasm32) + if (ty.isBuiltin()) return ptr_size; // default for unknown builtins const info = self.get(ty); return switch (info) { - .pointer, .many_pointer, .function => 8, - .slice => 16, // {ptr, i64} - .closure => 16, // {fn_ptr, env_ptr} + .pointer, .many_pointer, .function => ptr_size, + .slice => 16, // {ptr, i64} — same layout as string + .closure => 2 * ptr_size, // {fn_ptr, env_ptr} .optional => |o| blk: { const child_info = self.get(o.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function) - break :blk 8; + break :blk ptr_size; if (child_info == .closure) - break :blk 16; // {fn_ptr, env_ptr} + break :blk 2 * ptr_size; const cs = self.typeSizeBytes(o.child); const ca = self.typeAlignBytes(o.child); // { T, i1 } — i1 goes right after T, then pad to struct alignment @@ -480,8 +488,8 @@ pub const TypeTable = struct { } break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1); }, - .any => 16, - .protocol => 16, + .any => 2 * ptr_size, // {type_tag, data_ptr} + .protocol => 2 * ptr_size, // {ctx, vtable} .@"enum" => |e| { if (e.backing_type) |bt| return self.typeSizeBytes(bt); return 8; @@ -492,22 +500,25 @@ pub const TypeTable = struct { /// Compute the ABI alignment in bytes for a type, matching LLVM's rules. pub fn typeAlignBytes(self: *const TypeTable, ty: TypeId) usize { + const ptr_align: usize = self.pointer_size; if (ty == .void) return 1; if (ty == .bool) return 1; if (ty == .u8 or ty == .s8) return 1; if (ty == .u16 or ty == .s16) return 2; if (ty == .s32 or ty == .u32 or ty == .f32) return 4; if (ty == .s64 or ty == .u64 or ty == .f64) return 8; - if (ty == .string) return 8; - if (ty.isBuiltin()) return 8; + if (ty == .usize or ty == .isize) return ptr_align; + if (ty == .string) return 8; // i64 drives alignment + if (ty.isBuiltin()) return ptr_align; const info = self.get(ty); return switch (info) { - .pointer, .many_pointer, .function => 8, - .slice, .closure => 8, + .pointer, .many_pointer, .function => ptr_align, + .slice => 8, // i64 drives alignment + .closure => ptr_align, // {ptr, ptr} .optional => |o| blk: { const child_info = self.get(o.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function or child_info == .closure) - break :blk 8; + break :blk ptr_align; break :blk self.typeAlignBytes(o.child); }, .@"struct" => |s| blk: { @@ -566,6 +577,8 @@ pub const TypeTable = struct { .string => "string", .any => "Any", .noreturn => "noreturn", + .isize => "isize", + .usize => "usize", else => { // User types — format from TypeInfo const info = self.get(id); @@ -609,7 +622,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void { switch (info) { .signed => |w| h.update(&.{w}), .unsigned => |w| h.update(&.{w}), - .f32, .f64, .void, .bool, .string, .any, .noreturn => {}, + .f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => {}, .pointer => |p| h.update(std.mem.asBytes(&p.pointee)), .many_pointer => |p| h.update(std.mem.asBytes(&p.element)), .slice => |s| h.update(std.mem.asBytes(&s.element)), @@ -650,7 +663,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool { return switch (a) { .signed => |w| w == b.signed, .unsigned => |w| w == b.unsigned, - .f32, .f64, .void, .bool, .string, .any, .noreturn => true, + .f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => true, .pointer => |p| p.pointee == b.pointer.pointee, .many_pointer => |p| p.element == b.many_pointer.element, .slice => |s| s.element == b.slice.element, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 4f77bcb..ee10b77 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1477,6 +1477,7 @@ pub const Server = struct { .kw_closure, .kw_protocol, .kw_impl, + .kw_inline, .hash_run, .hash_import, .hash_insert, diff --git a/src/main.zig b/src/main.zig index d9943a3..3f31d67 100644 --- a/src/main.zig +++ b/src/main.zig @@ -223,6 +223,11 @@ fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Com const c_infos = try comp.collectCImportSources(); if (c_infos.len == 0) return &.{}; + // For Emscripten targets, use emcc to cross-compile C sources + if (comp.target_config.isEmscripten()) { + return try sx.c_import.compileCWithEmcc(allocator, io, c_infos); + } + const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos); return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs); } diff --git a/src/parser.zig b/src/parser.zig index 8d8a3c8..4addeda 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -82,6 +82,18 @@ pub const Parser = struct { return self.parseImplBlock(start); } + // Top-level `inline if` — compile-time conditional + if (self.current.tag == .kw_inline) { + if (self.peekNext() == .kw_if) { + self.advance(); // skip 'inline' + const expr = try self.parseIfExpr(); + if (expr.data == .if_expr) { + expr.data.if_expr.is_comptime = true; + } + return expr; + } + } + // All top-level declarations start with an identifier if (!self.isIdentLike() and self.current.tag != .kw_Self) { return self.fail("expected identifier at top level"); @@ -1375,6 +1387,19 @@ pub const Parser = struct { return try self.createNode(start, .{ .insert_expr = .{ .expr = inner } }); } + // inline if — compile-time conditional + if (self.current.tag == .kw_inline) { + if (self.peekNext() == .kw_if) { + self.advance(); // skip 'inline' + const expr = try self.parseIfExpr(); + if (expr.data == .if_expr) { + expr.data.if_expr.is_comptime = true; + } + try self.expectSemicolonAfter(expr); + return expr; + } + } + // Block-form if/while/for as statements — parse directly to prevent // postfix chaining (e.g. `if cond { ... }.field` being misparsed) if (self.current.tag == .kw_if) { diff --git a/src/target.zig b/src/target.zig index dedf528..f5f14f8 100644 --- a/src/target.zig +++ b/src/target.zig @@ -58,6 +58,16 @@ pub const TargetConfig = struct { return self.tripleHasPrefix("wasm32", "wasm64"); } + /// Check if target triple indicates macOS/Darwin. + pub fn isMacOS(self: TargetConfig) bool { + return self.tripleContains("darwin") or self.tripleContains("macos"); + } + + /// Check if target triple indicates Linux. + pub fn isLinux(self: TargetConfig) bool { + return self.tripleContains("linux"); + } + /// Check if target triple indicates Emscripten (contains "emscripten"). pub fn isEmscripten(self: TargetConfig) bool { return self.tripleContains("emscripten"); @@ -164,11 +174,10 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex 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})); - } + // Skip -l flags for Emscripten: libraries like SDL3 are provided via + // -sUSE_SDL=3, not -lSDL3. User provides everything via --lflags. - // Extra linker flags (e.g. -sUSE_SDL=2, -sUSE_WEBGL2=1, --preload-file) + // Extra linker flags (e.g. -sUSE_SDL=3, -sUSE_WEBGL2=1, --preload-file) for (target_config.extra_link_flags) |flag| { try argv.append(allocator, flag); } diff --git a/src/token.zig b/src/token.zig index 048be2c..87df06c 100644 --- a/src/token.zig +++ b/src/token.zig @@ -36,6 +36,7 @@ pub const Tag = enum { kw_protocol, // protocol kw_impl, // impl kw_Self, // Self (in protocol declarations) + kw_inline, // inline (compile-time if/for/while) // Symbols colon, // : @@ -224,6 +225,7 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{ .{ "protocol", .kw_protocol }, .{ "impl", .kw_impl }, .{ "Self", .kw_Self }, + .{ "inline", .kw_inline }, }); pub fn getKeyword(bytes: []const u8) ?Tag { diff --git a/src/types.zig b/src/types.zig index 74ce309..0522d36 100644 --- a/src/types.zig +++ b/src/types.zig @@ -24,6 +24,8 @@ pub const Type = union(enum) { function_type: FunctionTypeInfo, closure_type: ClosureTypeInfo, any_type, + usize_type, + isize_type, optional_type: OptionalTypeInfo, meta_type: MetaTypeInfo, tuple_type: TupleTypeInfo, @@ -82,7 +84,7 @@ pub const Type = union(enum) { return switch (self) { .signed => |w| w == other.signed, .unsigned => |w| w == other.unsigned, - .f32, .f64, .void_type, .boolean, .string_type, .any_type => true, + .f32, .f64, .void_type, .boolean, .string_type, .any_type, .usize_type, .isize_type => true, .enum_type => |n| std.mem.eql(u8, n, other.enum_type), .struct_type => |n| std.mem.eql(u8, n, other.struct_type), .union_type => |n| std.mem.eql(u8, n, other.union_type), @@ -149,12 +151,17 @@ pub const Type = union(enum) { return null; }, 'u' => { + if (std.mem.eql(u8, name, "usize")) return .usize_type; if (name.len >= 2) { const width = std.fmt.parseInt(u8, name[1..], 10) catch return null; if (width >= 1 and width <= 64) return Type.u(width); } return null; }, + 'i' => { + if (std.mem.eql(u8, name, "isize")) return .isize_type; + return null; + }, 'b' => if (std.mem.eql(u8, name, "bool")) .boolean else null, 'f' => { if (std.mem.eql(u8, name, "f32")) return .f32; @@ -223,6 +230,8 @@ pub const Type = union(enum) { .boolean => "bool", .string_type => "string", .void_type => "void", + .usize_type => "usize", + .isize_type => "isize", .struct_type => |n| n, .enum_type => |n| n, .union_type => |n| n, @@ -544,6 +553,8 @@ pub const Type = union(enum) { .string_type => "string", .void_type => "void", .any_type => "Any", + .usize_type => "usize", + .isize_type => "isize", .enum_type => |name| name, .struct_type => |name| name, .union_type => |name| name, diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 49caee0..4568456 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -592,4 +592,14 @@ opt-if2: 10.000000 opt-if3: 10.000000 opt-if4: 0.000000 opt-if5: 42.000000 +usize: 42 +isize: -7 +usize+8: 50 +s32->usize: 10 +usize->s64: 42 +=== inline if === +64-bit +not wasm +known os +desktop 64-bit === DONE ===