This commit is contained in:
agra
2026-03-02 21:00:55 +02:00
parent 2f4f898d54
commit bbb5426777
42 changed files with 483 additions and 9023 deletions

View File

@@ -215,6 +215,7 @@ pub const IfExpr = struct {
pub const MatchExpr = struct {
subject: *Node,
arms: []const MatchArm,
is_comptime: bool = false,
};
pub const MatchArm = struct {

View File

@@ -309,6 +309,8 @@ pub fn compileCWithEmcc(
allocator: std.mem.Allocator,
io: std.Io,
infos: []const CImportInfo,
target_config: @import("target.zig").TargetConfig,
tmp_dir: []const u8,
) ![]const []const u8 {
var paths = std.ArrayList([]const u8).empty;
var obj_idx: usize = 0;
@@ -317,11 +319,15 @@ pub fn compileCWithEmcc(
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});
const out_path = try std.fmt.allocPrint(allocator, "{s}/sx_emcc_{d}.o", .{ tmp_dir, obj_idx });
obj_idx += 1;
var argv = std.ArrayList([]const u8).empty;
try argv.appendSlice(allocator, &.{ "emcc", "-c", "-O2", src, "-o", out_path });
// wasm64: compile C sources with memory64 support
if (target_config.isWasm64()) {
try argv.append(allocator, "-sMEMORY64");
}
// Add include paths
for (info.includes) |inc| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-I{s}", .{dirName(inc)}));
@@ -355,11 +361,12 @@ pub fn writeCObjectFiles(
allocator: std.mem.Allocator,
io: std.Io,
obj_bufs: []c.LLVMMemoryBufferRef,
tmp_dir: []const u8,
) ![]const []const u8 {
var paths = std.ArrayList([]const u8).empty;
for (obj_bufs, 0..) |buf, i| {
const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}.o", .{i});
const path = try std.fmt.allocPrint(allocator, "{s}/sx_c_{d}.o", .{ tmp_dir, i });
const start = c.LLVMGetBufferStart(buf);
const size = c.LLVMGetBufferSize(buf);
const data = @as([*]const u8, @ptrCast(start))[0..size];

View File

@@ -106,6 +106,18 @@ pub const Compilation = struct {
self.ir_emitter = emitter;
}
/// Get link flags accumulated from #run build blocks.
pub fn getBuildLinkFlags(self: *Compilation) []const []const u8 {
if (self.ir_emitter) |*e| return e.build_config.link_flags.items;
return &.{};
}
/// Get output path set from #run build blocks, if any.
pub fn getBuildOutputPath(self: *Compilation) ?[]const u8 {
if (self.ir_emitter) |*e| return e.build_config.output_path;
return null;
}
/// Collect C import source info from the resolved AST.
pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo {
const root = self.resolved_root orelse self.root orelse return &.{};
@@ -117,7 +129,7 @@ pub const Compilation = struct {
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()) {
if (self.target_config.isWasm32()) {
module.types.pointer_size = 4;
}
var lowering = ir.Lowering.init(&module);

View File

@@ -86,6 +86,9 @@ pub const LLVMEmitter = struct {
// Target configuration (stored for ABI decisions during emission)
target_config: TargetConfig,
// Build configuration accumulated from #run blocks
build_config: interp_mod.BuildConfig,
const PendingPhi = struct {
phi: c.LLVMValueRef,
block_id: BlockId, // the block this phi belongs to
@@ -158,10 +161,12 @@ pub const LLVMEmitter = struct {
.closure_struct_type = null,
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.target_config = target_config,
.build_config = .{},
};
}
pub fn deinit(self: *LLVMEmitter) void {
self.build_config.deinit(self.alloc);
self.ref_map.deinit();
self.func_map.deinit();
self.field_name_arrays.deinit();
@@ -199,9 +204,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,
// Skip for wasm32: 4-byte pointers vs IR's assumed 8-byte,
// so struct sizes will differ. LLVM handles emission correctly.
if (self.target_config.isWasm()) return;
if (self.target_config.isWasm32()) return;
const dl = c.LLVMGetModuleDataLayout(self.llvm_module);
if (dl == null) return;
const type_count = self.ir_mod.types.infos.items.len;
@@ -241,6 +246,7 @@ pub const LLVMEmitter = struct {
// Run the side-effect function via interpreter
const func_id = ir_inst.FuncId.fromIndex(@intCast(i));
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
_ = interp_inst.call(func_id, &.{}) catch {};
// Write comptime output to stderr (same as old comptime VM)
if (interp_inst.output.items.len > 0) {
@@ -263,6 +269,7 @@ pub const LLVMEmitter = struct {
// Evaluate comptime initializer if present
if (global.comptime_func) |func_id| {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
const result = interp_inst.call(func_id, &.{}) catch .void_val;
const init_val = self.valueToLLVMConst(result, llvm_ty);
c.LLVMSetInitializer(llvm_global, init_val);
@@ -793,6 +800,7 @@ pub const LLVMEmitter = struct {
const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
if (callee_func.is_comptime and call_op.args.len == 0) {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
defer interp_inst.deinit();
if (interp_inst.call(call_op.callee, &.{})) |result| {
if (result.asInt()) |v| {
@@ -1427,7 +1435,7 @@ pub const LLVMEmitter = struct {
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())
const count = if (self.target_config.isWasm32())
c.LLVMBuildTrunc(self.builder, str_len, self.cached_i32, "len.tr")
else
str_len;
@@ -2132,9 +2140,9 @@ 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.
/// Returns the LLVM type for C `size_t`: i32 on wasm32, i64 on 64-bit targets (including wasm64).
fn sizeType(self: *LLVMEmitter) c.LLVMTypeRef {
return if (self.target_config.isWasm()) self.cached_i32 else self.cached_i64;
return if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64;
}
fn getMallocType(self: *LLVMEmitter) c.LLVMTypeRef {
@@ -2540,7 +2548,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,
.isize, .usize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64,
else => self.toLLVMTypeInfo(ty),
};
}
@@ -2667,7 +2675,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,
.usize, .isize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64,
};
}
@@ -2690,7 +2698,7 @@ pub const LLVMEmitter = struct {
// 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 (self.target_config.isWasm32()) {
if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32;
return llvm_ty;
}
@@ -3007,6 +3015,13 @@ pub const LLVMEmitter = struct {
return self.emitToFile(output_path, c.LLVMAssemblyFile);
}
/// Emit the module as LLVM bitcode to disk (for emcc to recompile with a newer LLVM).
pub fn emitBitcode(self: *LLVMEmitter, output_path: [*:0]const u8) !void {
if (c.LLVMWriteBitcodeToFile(self.llvm_module, output_path) != 0) {
return error.EmitFailed;
}
}
/// Dump the LLVM IR to a file for debugging.
pub fn dumpIRToFile(self: *LLVMEmitter, path: [*:0]const u8) void {
_ = c.LLVMPrintModuleToFile(self.llvm_module, path, null);

View File

@@ -302,6 +302,9 @@ pub const BuiltinId = enum(u16) {
type_of,
alloc,
dealloc,
build_options,
build_options_add_link_flag,
build_options_set_output_path,
};
pub const ProtocolCall = struct {

View File

@@ -106,6 +106,18 @@ pub const InterpError = error{
Unreachable,
};
// ── BuildConfig ─────────────────────────────────────────────────────────
// Mutable build configuration accumulated by #run blocks via BuildOptions methods.
pub const BuildConfig = struct {
link_flags: std.ArrayList([]const u8) = .empty,
output_path: ?[]const u8 = null,
pub fn deinit(self: *BuildConfig, alloc: Allocator) void {
self.link_flags.deinit(alloc);
}
};
// ── Interpreter ─────────────────────────────────────────────────────────
pub const Interpreter = struct {
@@ -121,6 +133,9 @@ pub const Interpreter = struct {
// Global values: evaluated comptime globals, indexed by GlobalId
global_values: std.AutoHashMap(u32, Value),
// Mutable build configuration — set by LLVMEmitter, written by #run blocks
build_config: ?*BuildConfig = null,
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
return .{
.module = module,
@@ -1242,6 +1257,30 @@ pub const Interpreter = struct {
const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @floor(f) } };
},
.build_options => {
// Returns a void sentinel — the "handle" to BuildConfig
return .{ .value = .void_val };
},
.build_options_add_link_flag => {
// args: [opts_handle, flag_string]
const str_val = frame.getRef(bi.args[1]);
if (str_val.asString(self)) |s| {
if (self.build_config) |bc| {
bc.link_flags.append(self.alloc, self.alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime;
}
}
return .{ .value = .void_val };
},
.build_options_set_output_path => {
// args: [opts_handle, path_string]
const str_val = frame.getRef(bi.args[1]);
if (str_val.asString(self)) |s| {
if (self.build_config) |bc| {
bc.output_path = self.alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
}
return .{ .value = .void_val };
},
.cast, .type_of, .alloc, .dealloc => {
return error.CannotEvalComptime;
},

View File

@@ -204,13 +204,15 @@ pub const Lowering = struct {
}
}
// ARCH: Architecture enum { aarch64; x86_64; wasm32; unknown; }
// ARCH: Architecture enum { aarch64; x86_64; wasm32; wasm64; unknown; }
const arch_name_id = self.module.types.internString("Architecture");
if (self.module.types.findByName(arch_name_id)) |arch_ty| {
const arch_info = self.module.types.get(arch_ty);
if (arch_info == .@"enum") {
const tag: u32 = if (tc.isWasm())
const tag: u32 = if (tc.isWasm32())
self.findVariantIndex(arch_info.@"enum".variants, "wasm32")
else if (tc.isWasm64())
self.findVariantIndex(arch_info.@"enum".variants, "wasm64")
else if (tc.isAarch64())
self.findVariantIndex(arch_info.@"enum".variants, "aarch64")
else if (tc.isX86_64())
@@ -221,8 +223,8 @@ pub const Lowering = struct {
}
}
// POINTER_SIZE: s64 (4 for wasm, 8 otherwise)
const ptr_size: i64 = if (tc.isWasm()) 4 else 8;
// POINTER_SIZE: s64 (4 for wasm32, 8 for wasm64 and other 64-bit targets)
const ptr_size: i64 = if (tc.isWasm32()) 4 else 8;
self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {};
}
@@ -2126,6 +2128,52 @@ pub const Lowering = struct {
}
}
/// Evaluate a compile-time match expression for `inline if ... == { case ... }`.
/// Returns the body of the matching arm, or null if the match can't be resolved.
fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node {
// Subject must be a comptime constant identifier
const name = switch (me.subject.data) {
.identifier => |id| id.name,
else => return null,
};
const cv = self.comptime_constants.get(name) orelse return null;
switch (cv) {
.enum_tag => |et| {
const enum_info = self.module.types.get(et.ty);
if (enum_info != .@"enum") return null;
for (me.arms) |arm| {
if (arm.pattern == null) continue; // default arm
const variant_name = switch (arm.pattern.?.data) {
.enum_literal => |el| el.name,
else => continue,
};
const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name);
if (et.tag == variant_idx) return arm.body;
}
// No match — try default arm
for (me.arms) |arm| {
if (arm.pattern == null) return arm.body;
}
return null;
},
.int_val => |iv| {
for (me.arms) |arm| {
if (arm.pattern == null) continue;
const rhs_val: i64 = switch (arm.pattern.?.data) {
.int_literal => |il| il.value,
else => continue,
};
if (iv == rhs_val) return arm.body;
}
for (me.arms) |arm| {
if (arm.pattern == null) return arm.body;
}
return null;
},
}
}
fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
const header_bb = self.freshBlock("while.hdr");
const body_bb = self.freshBlock("while.body");
@@ -2240,6 +2288,14 @@ pub const Lowering = struct {
}
fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref {
// inline if match: evaluate at compile time, only lower the matching arm
if (me.is_comptime) {
if (self.evalComptimeMatch(me)) |arm_body| {
return self.lowerInlineBranch(arm_body);
}
// Couldn't evaluate — fall through to runtime
}
const is_type_match = isTypeCategoryMatch(me);
const subject = self.lowerExpr(me.subject);
@@ -3904,6 +3960,15 @@ pub const Lowering = struct {
// Try to resolve the method by struct type name
const struct_name = self.getStructTypeName(obj_ty);
if (struct_name) |sname| {
// Intercept BuildOptions compiler builtins
if (std.mem.eql(u8, sname, "BuildOptions")) {
if (std.mem.eql(u8, fa.field, "add_link_flag")) {
return self.builder.callBuiltin(.build_options_add_link_flag, method_args.items, .void);
} else if (std.mem.eql(u8, fa.field, "set_output_path")) {
return self.builder.callBuiltin(.build_options_set_output_path, method_args.items, .void);
}
}
// Try direct qualified name: StructName.method
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field;
@@ -7530,6 +7595,12 @@ pub const Lowering = struct {
const oi = self.module.types.get(obj_ty);
if (oi == .@"struct") {
const struct_name = self.module.types.getString(oi.@"struct".name);
// Intercept BuildOptions compiler builtins
if (std.mem.eql(u8, struct_name, "BuildOptions")) {
if (std.mem.eql(u8, cfa.field, "add_link_flag") or std.mem.eql(u8, cfa.field, "set_output_path")) {
return .void;
}
}
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field;
if (self.resolveFuncByName(qualified)) |fid| {
return self.module.functions.items[@intFromEnum(fid)].ret;

View File

@@ -8,6 +8,7 @@ pub const c = @cImport({
@cInclude("llvm-c/Orc.h");
@cInclude("llvm-c/Error.h");
@cInclude("llvm-c/BitReader.h");
@cInclude("llvm-c/BitWriter.h");
@cInclude("llvm-c/Linker.h");
// Clang shim for C header parsing + source compilation

View File

@@ -34,7 +34,25 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, arg, "--target")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --target requires a value\n", .{}); return; }
target_config.triple = (try allocator.dupeZ(u8, args[i])).ptr;
const raw = args[i];
// Shorthand aliases for common targets
const expanded = if (std.mem.eql(u8, raw, "wasm") or std.mem.eql(u8, raw, "wasm32") or std.mem.eql(u8, raw, "emscripten"))
"wasm32-unknown-emscripten"
else if (std.mem.eql(u8, raw, "wasm64"))
"wasm64-unknown-emscripten"
else if (std.mem.eql(u8, raw, "macos") or std.mem.eql(u8, raw, "macos-arm"))
"aarch64-apple-macos"
else if (std.mem.eql(u8, raw, "macos-x86"))
"x86_64-apple-macos"
else if (std.mem.eql(u8, raw, "linux") or std.mem.eql(u8, raw, "linux-x86"))
"x86_64-unknown-linux-gnu"
else if (std.mem.eql(u8, raw, "linux-arm"))
"aarch64-unknown-linux-gnu"
else if (std.mem.eql(u8, raw, "windows"))
"x86_64-windows-msvc"
else
raw;
target_config.triple = (try allocator.dupeZ(u8, expanded)).ptr;
} else if (std.mem.eql(u8, arg, "--cpu")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --cpu requires a value\n", .{}); return; }
@@ -100,7 +118,6 @@ pub fn main(init: std.process.Init) !void {
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")) {
emitIR(allocator, io, path, target_config) catch return;
} else if (std.mem.eql(u8, command, "ir-dump")) {
@@ -219,17 +236,17 @@ fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compi
}
/// Compile C sources from #import c blocks to .o files for linking.
fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) ![]const []const u8 {
fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation, tmp_dir: []const u8) ![]const []const u8 {
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);
return try sx.c_import.compileCWithEmcc(allocator, io, c_infos, comp.target_config, tmp_dir);
}
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos);
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs);
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs, tmp_dir);
}
fn parseOptLevel(s: []const u8) ?sx.target.TargetConfig.OptLevel {
@@ -252,7 +269,7 @@ fn printUsage() void {
\\ lsp Start language server (LSP)
\\
\\Options:
\\ --target <triple> Target triple (default: host)
\\ --target <target> Target triple or shorthand: wasm, macos, linux, windows (default: host)
\\ --cpu <name> CPU name (default: generic)
\\ --opt <level> Optimization: none/0, less/1, default/2, aggressive/3
\\ -o <path> Output path
@@ -408,7 +425,11 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
const root = comp.resolved_root orelse comp.root orelse return error.CompileError;
const libs = try extractLibraries(allocator, root);
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}.o", .{output_path}, 0);
// Create temp directory for build artifacts
const tmp_dir: []const u8 = ".sx-tmp";
std.Io.Dir.createDirPath(.cwd(), io, tmp_dir) catch {};
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}/main.o", .{tmp_dir}, 0);
// Cache: compute key and check for cached binary/.o
const key = computeCacheKey(source, &comp.import_sources, target_config);
@@ -453,15 +474,37 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// Compile C sources from #import c blocks to .o files
timer.mark();
const c_obj_paths = compileCForBuild(allocator, io, &comp) catch {
const c_obj_paths = compileCForBuild(allocator, io, &comp, tmp_dir) catch {
std.debug.print("error: C import compilation failed\n", .{});
return error.CompileError;
};
timer.record("c-import");
// Merge build config (from #run blocks) with CLI config
var merged_config = target_config;
const build_flags = comp.getBuildLinkFlags();
if (build_flags.len > 0) {
var all_flags: std.ArrayList([]const u8) = .empty;
for (target_config.extra_link_flags) |f| try all_flags.append(allocator, f);
for (build_flags) |f| try all_flags.append(allocator, f);
merged_config.extra_link_flags = try all_flags.toOwnedSlice(allocator);
}
// Override output path from #run if set (and no explicit -o was given on CLI)
const final_output = if (target_config.output_path == null)
(comp.getBuildOutputPath() orelse output_path)
else
output_path;
// Ensure output directory exists
if (std.mem.lastIndexOfScalar(u8, final_output, '/')) |sep| {
if (sep > 0) {
std.Io.Dir.createDirPath(.cwd(), io, final_output[0..sep]) catch {};
}
}
// Link (sx .o + C .o files)
timer.mark();
sx.target.link(allocator, io, obj_path, c_obj_paths, output_path, libs, target_config) catch {
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, merged_config) catch {
std.debug.print("error: linking failed\n", .{});
return error.CompileError;
};
@@ -472,11 +515,16 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
}
// Clean up object files
std.debug.print("compiled: {s}\n", .{final_output});
// Clean up temp directory and all build artifacts
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
const shell_tmp = std.fmt.allocPrint(allocator, "{s}.shell.html", .{obj_path}) catch null;
if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {};
for (c_obj_paths) |cop| {
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
}
std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {};
}
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool) !void {
@@ -556,13 +604,21 @@ fn hasTopLevelRun(root: *const sx.ast.Node) bool {
fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
var libs = std.ArrayList([]const u8).empty;
var seen = std.StringHashMap(void).init(allocator);
const addLib = struct {
fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void {
if (s.contains(name)) return;
try s.put(name, {});
try l.append(a, name);
}
}.f;
for (root.data.root.decls) |decl| {
switch (decl.data) {
.library_decl => |ld| try libs.append(allocator, ld.lib_name),
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
.namespace_decl => |ns| {
for (ns.decls) |nd| {
switch (nd.data) {
.library_decl => |ld| try libs.append(allocator, ld.lib_name),
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
else => {},
}
}

View File

@@ -89,6 +89,8 @@ pub const Parser = struct {
const expr = try self.parseIfExpr();
if (expr.data == .if_expr) {
expr.data.if_expr.is_comptime = true;
} else if (expr.data == .match_expr) {
expr.data.match_expr.is_comptime = true;
}
return expr;
}
@@ -1394,6 +1396,8 @@ pub const Parser = struct {
const expr = try self.parseIfExpr();
if (expr.data == .if_expr) {
expr.data.if_expr.is_comptime = true;
} else if (expr.data == .match_expr) {
expr.data.match_expr.is_comptime = true;
}
try self.expectSemicolonAfter(expr);
return expr;

View File

@@ -58,6 +58,16 @@ pub const TargetConfig = struct {
return self.tripleHasPrefix("wasm32", "wasm64");
}
/// Check if target triple indicates wasm32 specifically (4-byte pointers, i32 size_t).
pub fn isWasm32(self: TargetConfig) bool {
return self.tripleHasPrefix("wasm32", "wasm32");
}
/// Check if target triple indicates wasm64 specifically (8-byte pointers, i64 size_t).
pub fn isWasm64(self: TargetConfig) bool {
return self.tripleHasPrefix("wasm64", "wasm64");
}
/// Check if target triple indicates macOS/Darwin.
pub fn isMacOS(self: TargetConfig) bool {
return self.tripleContains("darwin") or self.tripleContains("macos");
@@ -177,9 +187,26 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
// 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=3, -sUSE_WEBGL2=1, --preload-file)
// wasm64: automatically add -sMEMORY64 for the linker
if (target_config.isWasm64()) {
try argv.append(allocator, "-sMEMORY64");
}
// Use the built-in sx HTML shell template (write to temp file for emcc)
if (std.mem.endsWith(u8, output_bin, ".html")) {
const shell_html = @embedFile("wasm_shell.html");
const shell_path = try std.fmt.allocPrint(allocator, "{s}.shell.html", .{output_obj});
std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = shell_path, .data = shell_html }) catch {};
try argv.appendSlice(allocator, &.{ "--shell-file", shell_path });
}
// Extra linker flags (e.g. -sUSE_SDL=3, -sUSE_WEBGL2=1, --preload-file assets)
// Split space-separated flags into individual argv entries.
for (target_config.extra_link_flags) |flag| {
try argv.append(allocator, flag);
var it = std.mem.tokenizeScalar(u8, flag, ' ');
while (it.next()) |part| {
try argv.append(allocator, part);
}
}
} else if (target_config.isWindows()) {
// Windows: MSVC-style linker flags
@@ -219,9 +246,12 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
}
// Extra linker flags
// Extra linker flags — split space-separated flags into individual argv entries.
for (target_config.extra_link_flags) |flag| {
try argv.append(allocator, flag);
var it = std.mem.tokenizeScalar(u8, flag, ' ');
while (it.next()) |part| {
try argv.append(allocator, part);
}
}
}

49
src/wasm_shell.html Normal file
View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>sx</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#1e1e24}
canvas{display:block;width:100vw;height:100vh;outline:none}
#overlay{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1e1e24;z-index:10;transition:opacity .4s}
#overlay.hidden{opacity:0;pointer-events:none}
#overlay .bar-track{width:min(280px,60vw);height:3px;background:#2a2a32;border-radius:2px;margin-top:18px}
#overlay .bar-fill{height:100%;width:0%;background:#7c7cff;border-radius:2px;transition:width .15s}
#overlay .status{color:#888;font:13px/1 -apple-system,system-ui,sans-serif;margin-top:10px;letter-spacing:.02em}
</style>
</head>
<body>
<div id="overlay">
<div class="bar-track"><div class="bar-fill" id="bar"></div></div>
<div class="status" id="status">Loading&hellip;</div>
</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
<script>
var bar=document.getElementById('bar');
var status=document.getElementById('status');
var overlay=document.getElementById('overlay');
var Module={
canvas:document.getElementById('canvas'),
print:function(){console.log(Array.prototype.slice.call(arguments).join(' '))},
printErr:function(){console.warn(Array.prototype.slice.call(arguments).join(' '))},
setStatus:function(t){
if(!t){overlay.classList.add('hidden');return}
var m=t.match(/\((\d+(?:\.\d+)?)\/(\d+)\)/);
if(m){bar.style.width=(parseInt(m[1])/parseInt(m[2])*100)+'%';status.textContent='Loading\u2026'}
else{status.textContent=t}
},
totalDependencies:0,
monitorRunDependencies:function(left){
this.totalDependencies=Math.max(this.totalDependencies,left);
this.setStatus(left?'Loading... ('+( this.totalDependencies-left)+'/'+this.totalDependencies+')':'');
}
};
Module.setStatus('Loading\u2026');
window.onerror=function(){Module.setStatus('Error — see console');};
</script>
{{{ SCRIPT }}}
</body>
</html>