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

@@ -7,7 +7,7 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const static_llvm = b.option(bool, "static-llvm", "Statically link LLVM (self-contained binary, no LLVM needed at runtime)") orelse false; const static_llvm = b.option(bool, "static-llvm", "Statically link LLVM (self-contained binary, no LLVM needed at runtime)") orelse false;
const llvm_prefix = b.option([]const u8, "llvm-prefix", "Path to LLVM installation") orelse "/opt/homebrew/opt/llvm@18"; const llvm_prefix = b.option([]const u8, "llvm-prefix", "Path to LLVM installation") orelse "/opt/homebrew/opt/llvm@19";
const include_dir = b.fmt("{s}/include", .{llvm_prefix}); const include_dir = b.fmt("{s}/include", .{llvm_prefix});
const lib_dir = b.fmt("{s}/lib", .{llvm_prefix}); const lib_dir = b.fmt("{s}/lib", .{llvm_prefix});
@@ -136,7 +136,7 @@ pub fn build(b: *std.Build) void {
mod.link_libcpp = true; mod.link_libcpp = true;
} }
} else { } else {
mod.linkSystemLibrary("LLVM-18", .{}); mod.linkSystemLibrary("LLVM-19", .{});
mod.linkSystemLibrary("clang-cpp", .{}); mod.linkSystemLibrary("clang-cpp", .{});
// clang-cpp is C++ — need libc++ on macOS // clang-cpp is C++ — need libc++ on macOS
if (target_os != .windows and target_os != .linux) { if (target_os != .windows and target_os != .linux) {

View File

@@ -128,9 +128,15 @@ buildCompilerInstance(const char *filename,
driver_args.push_back("-w"); driver_args.push_back("-w");
#ifdef SX_LLVM_PREFIX #ifdef SX_LLVM_PREFIX
static std::string resource_dir = std::string(SX_LLVM_PREFIX) + "/lib/clang/18"; static std::string resource_dir = std::string(SX_LLVM_PREFIX) + "/lib/clang/19";
driver_args.push_back("-resource-dir"); driver_args.push_back("-resource-dir");
driver_args.push_back(resource_dir.c_str()); driver_args.push_back(resource_dir.c_str());
// On macOS, ensure system SDK headers are found
#ifdef __APPLE__
driver_args.push_back("-isysroot");
driver_args.push_back("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
#endif
#endif #endif
for (const auto *f : extra_flags) for (const auto *f : extra_flags)

View File

@@ -0,0 +1,45 @@
#import "modules/std.sx";
#import "modules/compiler.sx";
// --- #run build configuration ---
// build_options() returns a BuildOptions struct at compile time.
// Methods on it (add_link_flag, set_output_path) are compiler builtins
// that configure the build without runtime cost.
configure_build :: () {
opts := build_options();
// These calls are intercepted by the compiler at compile time.
// On a normal (non-wasm) target, inline if gates them off.
inline if OS == .wasm {
opts.set_output_path("sx-out/wasm/test.html");
opts.add_link_flag("-sUSE_SDL=3");
}
}
#run configure_build();
// --- inline if with compiler constants ---
main :: () {
// Verify #run configure_build() executed without error
print("build config: ok\n");
// Verify compiler constants are available
print("pointer size: {}\n", POINTER_SIZE);
// Verify inline if with OS/ARCH works
inline if OS == {
case .macos: { print("os: macos\n"); }
case .linux: { print("os: linux\n"); }
case .windows: { print("os: windows\n"); }
case .wasm: { print("os: wasm\n"); }
else: { print("os: unknown\n"); }
}
// Verify POINTER_SIZE is usable in inline if
inline if POINTER_SIZE == 8 {
print("64-bit platform\n");
}
inline if POINTER_SIZE == 4 {
print("32-bit platform\n");
}
}

View File

@@ -1,25 +0,0 @@
#import "modules/std.sx";
// Issue: nested field assignment through pointer
// self.inner.field = value should work when self is a pointer
Inner :: struct {
len: s64;
cap: s64;
}
Outer :: struct {
inner: Inner;
count: s64;
reset :: (self: *Outer) {
self.inner.len = 0; // error: field assignment target must be a variable
self.count += 1;
}
}
main :: () {
o := Outer.{ inner = Inner.{ len = 5, cap = 10 }, count = 0 };
o.reset();
print("{}\n", o.inner.len);
}

View File

@@ -1,25 +0,0 @@
// Issue: enum literal inference in match expression used as assignment RHS
// When a match expression is assigned to a field with a known enum type,
// the enum literals in case arms should infer their type from the assignment target.
Color :: enum {
red;
green;
blue;
none;
}
Thing :: struct {
color: Color;
}
main :: () {
t : Thing = ---;
value : u8 = 1;
t.color = if value == {
case 1: .red; // error: cannot infer enum type for literal
case 2: .green;
case 3: .blue;
else: .none;
};
}

View File

@@ -1,18 +0,0 @@
#import "modules/std.sx";
Color :: struct {
r, g, b, a: u8;
}
COLOR_WHITE :: Color.{ r = 255, g = 255, b = 255, a = 255 };
// Additional case: struct constant with enum-typed fields
HAlign :: enum { leading; center; trailing; }
VAlign :: enum { top; center; bottom; }
Alignment :: struct {
h: HAlign;
v: VAlign;
}
ALIGN_CENTER :: Alignment.{ h = .center, v = .center };

View File

@@ -1,20 +0,0 @@
// Issue: top-level constants from imported files are not visible
// COLOR_WHITE works after fix, but ALIGN_CENTER (struct with enum fields) does not.
// Error: undefined identifier 'ALIGN_CENTER'
#import "modules/std.sx";
#import "examples/issue-0004-defs.sx";
Thing :: struct {
color: Color;
alignment: Alignment;
make :: () -> Thing {
Thing.{ color = COLOR_WHITE, alignment = ALIGN_CENTER };
}
}
main :: () {
t := Thing.make();
print("{}\n", t.color.r);
}

View File

@@ -1,23 +0,0 @@
// Issue: match on u8 value with enum result assigned to typed field
// The switch value is u8 but case constants are s64 (default int literal type).
// Compiler should cast case constants to match the switch value type.
// LLVM error: Switch constants must all be same type as switch value!
out :: (str: string) -> void #builtin;
Button :: enum {
none;
left;
middle;
right;
}
main :: () {
val : u8 = 2;
result : Button = if val == {
case 1: .left;
case 2: .middle;
case 3: .right;
else: .none;
};
}

View File

@@ -1,39 +0,0 @@
// Issue: chained method call on struct field operates on a copy
// `a.field.method()` where method takes *Self creates a temporary copy of `field`
// instead of borrowing `a.field` as a pointer.
// The mutation is lost because it modifies the copy, not the original.
out :: (str: string) -> void #builtin;
Counter :: struct {
value: s64;
inc :: (self: *Counter) {
self.value += 1;
}
}
Parent :: struct {
counter: Counter;
}
main :: () {
p := Parent.{ counter = Counter.{ value = 0 } };
// This should increment p.counter.value, but the mutation is lost:
p.counter.inc();
if p.counter.value == 0 {
out("BUG: p.counter.value is still 0 after inc()\n");
} else {
out("OK: p.counter.value is 1\n");
}
// Workaround: take explicit pointer
cp := @p.counter;
cp.inc();
if p.counter.value == 1 {
out("OK: workaround via pointer works\n");
}
}

View File

@@ -1,26 +0,0 @@
// Issue 0008: Chained ?? (null coalescing) doesn't work
//
// `a ?? b ?? c` where a: ?f32, b: ?f32, c: f32 fails with:
// "narrowing conversion from '?f32' to 'f32' requires explicit 'xx' cast"
//
// It parses as (a ?? b) ?? c, and the first ?? rejects ?f32 as the rhs.
//
// Expected: ?? should either be right-associative so it parses as a ?? (b ?? c),
// or allow ?T as the rhs (returning ?T when rhs is optional, T when rhs is concrete).
//
// Workaround: use parentheses — a ?? (b ?? c)
Foo :: struct {
x: ?f32;
y: ?f32;
}
main :: () -> void {
f := Foo.{ x = 1.0, y = 2.0 };
// This works:
ok := f.x ?? (f.y ?? 0.0);
// This should also work but fails:
bad := f.x ?? f.y ?? 0.0;
}

View File

@@ -1,20 +0,0 @@
// Issue 0009: Struct-level constant declarations
//
// Constants declared inside a struct body with `NAME :Type: value;` syntax
// fail with "expected field name in struct".
//
// Expected: structs should support constant declarations alongside fields and methods.
Foo :: struct {
x: f32;
// This method works:
get_x :: (self: *Foo) -> f32 { self.x; }
// This constant should work but fails:
DEFAULT_X :f32: 42.0;
}
main :: () -> void {
f := Foo.{ x = Foo.DEFAULT_X };
}

View File

@@ -1,38 +0,0 @@
// Issue 0010: inline if-else in struct literal field produces type error
// The `null` branch is typed as `*void` instead of being coerced to `?f32`
//
// Error: narrowing conversion from '*void' to 'f32' requires explicit 'xx' cast
#import "modules/std.sx";
Foo :: struct {
width: ?f32;
}
main :: () -> void {
x :f32: 10.0;
// null in then branch, value in else
f1 := Foo.{ width = if true then null else x };
print("{}\n", f1.width ?? 99.0);
// value in then branch, null in else
f2 := Foo.{ width = if true then x else null };
print("{}\n", f2.width ?? 99.0);
// both branches are values
f3 := Foo.{ width = if false then 5.0 else x };
print("{}\n", f3.width ?? 99.0);
// standalone variable, not just struct fields
val: ?f32 = if true then null else 42.0;
print("{}\n", val ?? 0.0);
val2: ?f32 = if false then null else 42.0;
print("{}\n", val2 ?? 0.0);
// negation in condition
cond := false;
val3: ?f32 = if !cond then null else 42.0;
print("{}\n", val3 ?? 0.0);
}

View File

@@ -1,41 +0,0 @@
#import "modules/std.sx";
// Forward references: types, struct fields, methods, and free functions
// can reference types declared later in the file.
// Free function referencing types declared later
make_frame :: (e: EdgeInsets) -> Frame {
Frame.{ x = e.left, y = e.top,
w = 100.0 - e.left - e.right,
h = 100.0 - e.top - e.bottom };
}
// Struct with a field whose type is declared later
Container :: struct {
frame: Frame;
insets: EdgeInsets;
}
Frame :: struct {
x, y, w, h: f32;
inset :: (self: Frame, insets: EdgeInsets) -> Frame {
Frame.{ x = self.x + insets.left, y = self.y + insets.top,
w = self.w - insets.left - insets.right,
h = self.h - insets.top - insets.bottom };
}
}
EdgeInsets :: struct {
top, left, bottom, right: f32;
}
main :: () {
e := EdgeInsets.{ top = 10.0, left = 10.0, bottom = 10.0, right = 10.0 };
f := make_frame(e);
r := f.inset(e);
c := Container.{ frame = f, insets = e };
print("{}", r.x);
print(" {}", r.w);
print(" {}", c.frame.x);
}

View File

@@ -1,6 +1,19 @@
OperatingSystem :: enum { macos; linux; windows; wasm; unknown; } OperatingSystem :: enum { macos; linux; windows; wasm; unknown; }
Architecture :: enum { aarch64; x86_64; wasm32; unknown; } Architecture :: enum { aarch64; x86_64; wasm32; wasm64; unknown; }
OS : OperatingSystem = .unknown; OS : OperatingSystem = .unknown;
ARCH : Architecture = .unknown; ARCH : Architecture = .unknown;
POINTER_SIZE : s64 = 8; POINTER_SIZE : s64 = 8;
BuildOptions :: struct {
add_link_flag :: (self: BuildOptions, flag: [:0]u8) {
// Compiler builtin — intercepted at compile time
}
set_output_path :: (self: BuildOptions, path: [:0]u8) {
// Compiler builtin — intercepted at compile time
}
}
build_options :: () -> BuildOptions {
return BuildOptions.{};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
#import "modules/std.sx";
Phys :: struct {
x, y: f32;
GRAVITY :f32: 9.81;
MAX_SPEED :: 100;
}
main :: () {
print("gravity: {}\n", Phys.GRAVITY);
print("max speed: {}\n", Phys.MAX_SPEED);
}

View File

@@ -1,80 +0,0 @@
#import "modules/std.sx";
main :: () {
n1 : s32 = 1;
n2 : s32 = 2;
n3 : s32 = 3;
f1 := closure((x: s32) -> s32 => x + n1);
f2 := closure((x: s32) -> s32 => x + n2);
f3 := closure((x: s32) -> s32 => x + n3);
print("f1: {}\n", f1(10));
print("f2: {}\n", f2(10));
print("f3: {}\n", f3(10));
// closure struct field
Button :: struct {
label: string;
on_press: Closure(s32) -> void;
}
btn_val := 99;
btn_cb := closure((id: s32) {
print("btn: {} {}\n", id, btn_val);
});
btn := Button.{ label = "OK", on_press = btn_cb };
btn.on_press(1);
// optional closure
f_none : ?Closure(s64) -> s64 = null;
if f_none != null { print("should not print\n"); }
else { print("opt-closure: none\n"); }
// closure factory
make_adder :: (n: s32) -> Closure(s32) -> s32 {
closure((x: s32) -> s32 => x + n);
}
add5 := make_adder(5);
add10 := make_adder(10);
print("factory: {} {}\n", add5(100), add10(100));
// HOF compose
compose :: (f: Closure(s32) -> s32, g: Closure(s32) -> s32) -> Closure(s32) -> s32 {
closure((x: s32) -> s32 => f(g(x)));
}
double :: (x: s32) -> s32 { return x * 2; }
cf := compose(add5, double);
print("compose: {}\n", cf(10));
// closure with array
sort_bubble :: (arr: [*]s32, cnt: s64, less: Closure(s32, s32) -> bool) {
i : s64 = 0;
while i < cnt {
j : s64 = 0;
while j < cnt - 1 {
if less(arr[j + 1], arr[j]) {
tmp := arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
j += 1;
}
i += 1;
}
}
sort_arr : [5]s32 = .[5, 3, 1, 4, 2];
sort_bubble(xx @sort_arr, 5, closure((a: s32, b: s32) -> bool {
return a < b;
}));
print("sort: {} {} {} {} {}\n", sort_arr[0], sort_arr[1], sort_arr[2], sort_arr[3], sort_arr[4]);
// Many closures with string captures
tag1 := "hello";
tag2 := "world";
sf1 := closure((x: s32) { print("sf1: {} {}\n", tag1, x); });
sf2 := closure((x: s32) { print("sf2: {} {}\n", tag2, x); });
sf1(1);
sf2(2);
print("=== DONE ===\n");
}

View File

@@ -1,8 +0,0 @@
#import "modules/std.sx";
main :: () {
n := 42;
f := closure((x: s64) -> s64 { x + n; });
r := f(10);
print("r: {}\n", r);
}

View File

@@ -1,5 +0,0 @@
#import "modules/std.sx";
greet :: () -> string { format("hello"); }
main :: () {
print("{}\n", greet());
}

View File

@@ -1,6 +0,0 @@
#import "modules/std.sx";
main :: () -> s32 {
v := Vector(3,f32).[1,2,3];
print("{}
", v);
}

View File

@@ -1,24 +0,0 @@
#import "modules/std.sx";
Point :: struct { x: s32; y: s32; }
Eq :: protocol {
eq :: (other: Self) -> bool;
}
impl Eq for Point {
eq :: (self: *Point, other: Point) -> bool {
self.x == other.x and self.y == other.y;
}
}
are_equal :: ($T: Type/Eq, a: T, b: T) -> bool {
a.eq(b);
}
main :: () {
p1 := Point.{ x = 1, y = 2 };
p2 := Point.{ x = 1, y = 2 };
p3 := Point.{ x = 3, y = 4 };
print("P6.1: {} {}\n", are_equal(p1, p2), are_equal(p1, p3));
}

View File

@@ -1,5 +0,0 @@
#import "modules/std.sx";
main :: () {
s := "";
print("{}\n", s);
}

View File

@@ -1,14 +0,0 @@
#import "modules/std.sx";
Vec2 :: union {
data: [2]f32;
struct { x, y: f32; };
}
main :: () {
uv : Vec2 = ---;
uv.x = 1.0;
uv.y = 2.0;
print("promoted-x: {}\n", uv.x);
print("promoted-data0: {}\n", uv.data[0]);
}

View File

@@ -1,4 +0,0 @@
#import c {
#include "vendors/test_c/test.h";
#source "vendors/test_c/test.c";
};

View File

@@ -1582,6 +1582,58 @@ response :: format("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", body.len,
This works for any function, not just `format`. The mechanism is general: the VM compiles the function body (including `#insert` directives, variadic `..Any` args, and calls to other functions) and executes it entirely at compile time. If the VM encounters something it cannot evaluate (e.g., foreign function calls, unsupported operations), it silently falls through to runtime codegen. This works for any function, not just `format`. The mechanism is general: the VM compiles the function body (including `#insert` directives, variadic `..Any` args, and calls to other functions) and executes it entirely at compile time. If the VM encounters something it cannot evaluate (e.g., foreign function calls, unsupported operations), it silently falls through to runtime codegen.
### Build Configuration
The `BuildOptions` struct (from `modules/compiler.sx`) provides compile-time build configuration via `#run`. Methods on `BuildOptions` are compiler builtins intercepted during compilation — they have no runtime cost.
```sx
#import "modules/compiler.sx";
configure_build :: () {
opts := build_options();
opts.add_link_flag("-lm");
opts.set_output_path("out/my_program");
inline if OS == .wasm {
opts.set_output_path("sx-out/wasm/app.html");
opts.add_link_flag("-sUSE_SDL=3");
opts.add_link_flag("-sALLOW_MEMORY_GROWTH=1");
}
}
#run configure_build();
```
**API:**
| Method | Description |
|--------|-------------|
| `build_options()` | Returns a `BuildOptions` value for the current compilation |
| `opts.add_link_flag(flag)` | Appends a linker flag (merged with CLI flags) |
| `opts.set_output_path(path)` | Sets the output binary path (overridden by CLI `-o`) |
Build flags from `add_link_flag` are merged with any flags passed on the command line. Duplicate library flags (e.g., `-lSDL3` from multiple imports) are automatically deduplicated.
### Compiler Constants
The `modules/compiler.sx` module provides compile-time constants set by the compiler based on the target:
| Constant | Type | Description |
|----------|------|-------------|
| `OS` | `OperatingSystem` | Target OS: `.macos`, `.linux`, `.windows`, `.wasm`, `.unknown` |
| `ARCH` | `Architecture` | Target arch: `.aarch64`, `.x86_64`, `.wasm32`, `.unknown` |
| `POINTER_SIZE` | `s64` | Pointer width in bytes (8 for 64-bit, 4 for wasm32) |
These are used with `inline if` for compile-time conditional compilation:
```sx
inline if OS == .wasm {
// Only compiled when targeting wasm
}
inline if POINTER_SIZE == 8 {
// Only compiled on 64-bit platforms
}
```
--- ---
## 9. Modules / Imports ## 9. Modules / Imports
@@ -1658,7 +1710,43 @@ main :: () -> s32 {
--- ---
## 10. Program Structure ## 10. CLI & Cross-Compilation
### Commands
```
sx run <file.sx> Compile and run
sx build <file.sx> Compile to binary
sx lsp Start language server (LSP)
```
### Options
| Flag | Description |
|------|-------------|
| `--target <target>` | Target triple or shorthand (default: host) |
| `--cpu <name>` | CPU name (default: generic) |
| `--opt <level>` | Optimization: `none`/`0`, `less`/`1`, `default`/`2`, `aggressive`/`3` |
| `-o <path>` | Output path (overrides `set_output_path`) |
### Target Shorthands
The `--target` flag accepts shorthand aliases for common targets:
| Shorthand | Expands to |
|-----------|-----------|
| `wasm`, `emscripten` | `wasm32-unknown-emscripten` |
| `macos`, `macos-arm` | `aarch64-apple-macos` |
| `macos-x86` | `x86_64-apple-macos` |
| `linux`, `linux-x86` | `x86_64-unknown-linux-gnu` |
| `linux-arm` | `aarch64-unknown-linux-gnu` |
| `windows` | `x86_64-windows-msvc` |
Full triples are also accepted and passed through as-is.
---
## 11. Program Structure
A program is a sequence of top-level declarations and `#import` directives. Execution begins at `main`. A program is a sequence of top-level declarations and `#import` directives. Execution begins at `main`.
@@ -1672,7 +1760,7 @@ main :: () {
--- ---
## 11. Grammar (informal) ## 12. Grammar (informal)
``` ```
program = top_level* program = top_level*
@@ -1731,7 +1819,7 @@ type = '$' IDENT | 's32' | 'f32' | 'f64' | 'bool' | 'string'
--- ---
## 12. Open Questions ## 13. Open Questions
- **Nested functions**: Can functions be defined inside other functions? - **Nested functions**: Can functions be defined inside other functions?
- **Operator overloading**: Not shown — presumably no. - **Operator overloading**: Not shown — presumably no.

View File

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

View File

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

View File

@@ -106,6 +106,18 @@ pub const Compilation = struct {
self.ir_emitter = emitter; 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. /// Collect C import source info from the resolved AST.
pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo { pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo {
const root = self.resolved_root orelse self.root orelse return &.{}; 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); const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator);
var module = ir.Module.init(self.allocator); var module = ir.Module.init(self.allocator);
//TODO: find a better place for this //TODO: find a better place for this
if (self.target_config.isWasm()) { if (self.target_config.isWasm32()) {
module.types.pointer_size = 4; module.types.pointer_size = 4;
} }
var lowering = ir.Lowering.init(&module); 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 configuration (stored for ABI decisions during emission)
target_config: TargetConfig, target_config: TargetConfig,
// Build configuration accumulated from #run blocks
build_config: interp_mod.BuildConfig,
const PendingPhi = struct { const PendingPhi = struct {
phi: c.LLVMValueRef, phi: c.LLVMValueRef,
block_id: BlockId, // the block this phi belongs to block_id: BlockId, // the block this phi belongs to
@@ -158,10 +161,12 @@ pub const LLVMEmitter = struct {
.closure_struct_type = null, .closure_struct_type = null,
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
.target_config = target_config, .target_config = target_config,
.build_config = .{},
}; };
} }
pub fn deinit(self: *LLVMEmitter) void { pub fn deinit(self: *LLVMEmitter) void {
self.build_config.deinit(self.alloc);
self.ref_map.deinit(); self.ref_map.deinit();
self.func_map.deinit(); self.func_map.deinit();
self.field_name_arrays.deinit(); self.field_name_arrays.deinit();
@@ -199,9 +204,9 @@ pub const LLVMEmitter = struct {
/// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types. /// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types.
fn verifySizes(self: *LLVMEmitter) void { 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. // 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); const dl = c.LLVMGetModuleDataLayout(self.llvm_module);
if (dl == null) return; if (dl == null) return;
const type_count = self.ir_mod.types.infos.items.len; 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 // Run the side-effect function via interpreter
const func_id = ir_inst.FuncId.fromIndex(@intCast(i)); const func_id = ir_inst.FuncId.fromIndex(@intCast(i));
var interp_inst = Interpreter.init(self.ir_mod, self.alloc); var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
_ = interp_inst.call(func_id, &.{}) catch {}; _ = interp_inst.call(func_id, &.{}) catch {};
// Write comptime output to stderr (same as old comptime VM) // Write comptime output to stderr (same as old comptime VM)
if (interp_inst.output.items.len > 0) { if (interp_inst.output.items.len > 0) {
@@ -263,6 +269,7 @@ pub const LLVMEmitter = struct {
// Evaluate comptime initializer if present // Evaluate comptime initializer if present
if (global.comptime_func) |func_id| { if (global.comptime_func) |func_id| {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc); 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 result = interp_inst.call(func_id, &.{}) catch .void_val;
const init_val = self.valueToLLVMConst(result, llvm_ty); const init_val = self.valueToLLVMConst(result, llvm_ty);
c.LLVMSetInitializer(llvm_global, init_val); 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()]; const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
if (callee_func.is_comptime and call_op.args.len == 0) { if (callee_func.is_comptime and call_op.args.len == 0) {
var interp_inst = Interpreter.init(self.ir_mod, self.alloc); var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
interp_inst.build_config = &self.build_config;
defer interp_inst.deinit(); defer interp_inst.deinit();
if (interp_inst.call(call_op.callee, &.{})) |result| { if (interp_inst.call(call_op.callee, &.{})) |result| {
if (result.asInt()) |v| { 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 raw_ptr = c.LLVMBuildExtractValue(self.builder, str_val, 0, "str.ptr");
const str_len = c.LLVMBuildExtractValue(self.builder, str_val, 1, "str.len"); const str_len = c.LLVMBuildExtractValue(self.builder, str_val, 1, "str.len");
// On wasm32, count param is i32 (size_t) // 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") c.LLVMBuildTrunc(self.builder, str_len, self.cached_i32, "len.tr")
else else
str_len; str_len;
@@ -2132,9 +2140,9 @@ pub const LLVMEmitter = struct {
return c.LLVMAddFunction(self.llvm_module, "free", fn_ty); 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 { 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 { fn getMallocType(self: *LLVMEmitter) c.LLVMTypeRef {
@@ -2540,7 +2548,7 @@ pub const LLVMEmitter = struct {
.string => self.getStringStructType(), .string => self.getStringStructType(),
.any => self.getAnyStructType(), .any => self.getAnyStructType(),
.noreturn => self.cached_void, .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), else => self.toLLVMTypeInfo(ty),
}; };
} }
@@ -2667,7 +2675,7 @@ pub const LLVMEmitter = struct {
// For now, use opaque ptr // For now, use opaque ptr
return self.cached_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). // WASM32: usize/isize are pointer-sized (i32 on wasm32).
// Other integer types (s64, u64) keep their declared size — they represent // Other integer types (s64, u64) keep their declared size — they represent
// genuinely 64-bit values (SDL_WindowFlags, timestamps, etc.). // 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; if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32;
return llvm_ty; return llvm_ty;
} }
@@ -3007,6 +3015,13 @@ pub const LLVMEmitter = struct {
return self.emitToFile(output_path, c.LLVMAssemblyFile); 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. /// Dump the LLVM IR to a file for debugging.
pub fn dumpIRToFile(self: *LLVMEmitter, path: [*:0]const u8) void { pub fn dumpIRToFile(self: *LLVMEmitter, path: [*:0]const u8) void {
_ = c.LLVMPrintModuleToFile(self.llvm_module, path, null); _ = c.LLVMPrintModuleToFile(self.llvm_module, path, null);

View File

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

View File

@@ -106,6 +106,18 @@ pub const InterpError = error{
Unreachable, 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 ───────────────────────────────────────────────────────── // ── Interpreter ─────────────────────────────────────────────────────────
pub const Interpreter = struct { pub const Interpreter = struct {
@@ -121,6 +133,9 @@ pub const Interpreter = struct {
// Global values: evaluated comptime globals, indexed by GlobalId // Global values: evaluated comptime globals, indexed by GlobalId
global_values: std.AutoHashMap(u32, Value), 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 { pub fn init(module: *const Module, alloc: Allocator) Interpreter {
return .{ return .{
.module = module, .module = module,
@@ -1242,6 +1257,30 @@ pub const Interpreter = struct {
const f = val.asFloat() orelse return error.TypeError; const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @floor(f) } }; 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 => { .cast, .type_of, .alloc, .dealloc => {
return error.CannotEvalComptime; 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"); const arch_name_id = self.module.types.internString("Architecture");
if (self.module.types.findByName(arch_name_id)) |arch_ty| { if (self.module.types.findByName(arch_name_id)) |arch_ty| {
const arch_info = self.module.types.get(arch_ty); const arch_info = self.module.types.get(arch_ty);
if (arch_info == .@"enum") { if (arch_info == .@"enum") {
const tag: u32 = if (tc.isWasm()) const tag: u32 = if (tc.isWasm32())
self.findVariantIndex(arch_info.@"enum".variants, "wasm32") self.findVariantIndex(arch_info.@"enum".variants, "wasm32")
else if (tc.isWasm64())
self.findVariantIndex(arch_info.@"enum".variants, "wasm64")
else if (tc.isAarch64()) else if (tc.isAarch64())
self.findVariantIndex(arch_info.@"enum".variants, "aarch64") self.findVariantIndex(arch_info.@"enum".variants, "aarch64")
else if (tc.isX86_64()) else if (tc.isX86_64())
@@ -221,8 +223,8 @@ pub const Lowering = struct {
} }
} }
// POINTER_SIZE: s64 (4 for wasm, 8 otherwise) // POINTER_SIZE: s64 (4 for wasm32, 8 for wasm64 and other 64-bit targets)
const ptr_size: i64 = if (tc.isWasm()) 4 else 8; const ptr_size: i64 = if (tc.isWasm32()) 4 else 8;
self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {}; 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 { fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
const header_bb = self.freshBlock("while.hdr"); const header_bb = self.freshBlock("while.hdr");
const body_bb = self.freshBlock("while.body"); const body_bb = self.freshBlock("while.body");
@@ -2240,6 +2288,14 @@ pub const Lowering = struct {
} }
fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { 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 is_type_match = isTypeCategoryMatch(me);
const subject = self.lowerExpr(me.subject); const subject = self.lowerExpr(me.subject);
@@ -3904,6 +3960,15 @@ pub const Lowering = struct {
// Try to resolve the method by struct type name // Try to resolve the method by struct type name
const struct_name = self.getStructTypeName(obj_ty); const struct_name = self.getStructTypeName(obj_ty);
if (struct_name) |sname| { 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 // Try direct qualified name: StructName.method
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; 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); const oi = self.module.types.get(obj_ty);
if (oi == .@"struct") { if (oi == .@"struct") {
const struct_name = self.module.types.getString(oi.@"struct".name); 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; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field;
if (self.resolveFuncByName(qualified)) |fid| { if (self.resolveFuncByName(qualified)) |fid| {
return self.module.functions.items[@intFromEnum(fid)].ret; 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/Orc.h");
@cInclude("llvm-c/Error.h"); @cInclude("llvm-c/Error.h");
@cInclude("llvm-c/BitReader.h"); @cInclude("llvm-c/BitReader.h");
@cInclude("llvm-c/BitWriter.h");
@cInclude("llvm-c/Linker.h"); @cInclude("llvm-c/Linker.h");
// Clang shim for C header parsing + source compilation // 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")) { if (std.mem.eql(u8, arg, "--target")) {
i += 1; i += 1;
if (i >= args.len) { std.debug.print("error: --target requires a value\n", .{}); return; } 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")) { } else if (std.mem.eql(u8, arg, "--cpu")) {
i += 1; i += 1;
if (i >= args.len) { std.debug.print("error: --cpu requires a value\n", .{}); return; } 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; break :blk base;
}; };
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch return; 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")) { } else if (std.mem.eql(u8, command, "ir")) {
emitIR(allocator, io, path, target_config) catch return; emitIR(allocator, io, path, target_config) catch return;
} else if (std.mem.eql(u8, command, "ir-dump")) { } 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. /// 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(); const c_infos = try comp.collectCImportSources();
if (c_infos.len == 0) return &.{}; if (c_infos.len == 0) return &.{};
// For Emscripten targets, use emcc to cross-compile C sources // For Emscripten targets, use emcc to cross-compile C sources
if (comp.target_config.isEmscripten()) { 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); 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 { fn parseOptLevel(s: []const u8) ?sx.target.TargetConfig.OptLevel {
@@ -252,7 +269,7 @@ fn printUsage() void {
\\ lsp Start language server (LSP) \\ lsp Start language server (LSP)
\\ \\
\\Options: \\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) \\ --cpu <name> CPU name (default: generic)
\\ --opt <level> Optimization: none/0, less/1, default/2, aggressive/3 \\ --opt <level> Optimization: none/0, less/1, default/2, aggressive/3
\\ -o <path> Output path \\ -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 root = comp.resolved_root orelse comp.root orelse return error.CompileError;
const libs = try extractLibraries(allocator, root); 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 // Cache: compute key and check for cached binary/.o
const key = computeCacheKey(source, &comp.import_sources, target_config); 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 // Compile C sources from #import c blocks to .o files
timer.mark(); 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", .{}); std.debug.print("error: C import compilation failed\n", .{});
return error.CompileError; return error.CompileError;
}; };
timer.record("c-import"); 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) // Link (sx .o + C .o files)
timer.mark(); 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", .{}); std.debug.print("error: linking failed\n", .{});
return error.CompileError; 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 {}; 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 {}; 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| { for (c_obj_paths) |cop| {
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {}; 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 { 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 { fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
var libs = std.ArrayList([]const u8).empty; 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| { for (root.data.root.decls) |decl| {
switch (decl.data) { 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| { .namespace_decl => |ns| {
for (ns.decls) |nd| { for (ns.decls) |nd| {
switch (nd.data) { 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 => {}, else => {},
} }
} }

View File

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

View File

@@ -58,6 +58,16 @@ pub const TargetConfig = struct {
return self.tripleHasPrefix("wasm32", "wasm64"); 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. /// Check if target triple indicates macOS/Darwin.
pub fn isMacOS(self: TargetConfig) bool { pub fn isMacOS(self: TargetConfig) bool {
return self.tripleContains("darwin") or self.tripleContains("macos"); 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 // Skip -l flags for Emscripten: libraries like SDL3 are provided via
// -sUSE_SDL=3, not -lSDL3. User provides everything via --lflags. // -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| { 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()) { } else if (target_config.isWindows()) {
// Windows: MSVC-style linker flags // 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})); 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| { 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>

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
build config: ok
pointer size: 8
os: macos
64-bit platform