This commit is contained in:
agra
2026-03-02 17:18:47 +02:00
parent ba9c4d69ce
commit 2f4f898d54
20 changed files with 418 additions and 49 deletions

View File

@@ -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
};

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,
};
}

View File

@@ -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()) {

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -1477,6 +1477,7 @@ pub const Server = struct {
.kw_closure,
.kw_protocol,
.kw_impl,
.kw_inline,
.hash_run,
.hash_import,
.hash_insert,

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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,