sm
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
45
examples/38-build-config.sx
Normal file
45
examples/38-build-config.sx
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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.{};
|
||||||
|
}
|
||||||
|
|||||||
1611
examples/smoke_a.sx
1611
examples/smoke_a.sx
File diff suppressed because it is too large
Load Diff
1931
examples/smoke_b.sx
1931
examples/smoke_b.sx
File diff suppressed because it is too large
Load Diff
2667
examples/smoke_c.sx
2667
examples/smoke_c.sx
File diff suppressed because it is too large
Load Diff
2343
examples/smoke_d.sx
2343
examples/smoke_d.sx
File diff suppressed because it is too large
Load Diff
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#import "modules/std.sx";
|
|
||||||
greet :: () -> string { format("hello"); }
|
|
||||||
main :: () {
|
|
||||||
print("{}\n", greet());
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#import "modules/std.sx";
|
|
||||||
main :: () -> s32 {
|
|
||||||
v := Vector(3,f32).[1,2,3];
|
|
||||||
print("{}
|
|
||||||
", v);
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#import "modules/std.sx";
|
|
||||||
main :: () {
|
|
||||||
s := "";
|
|
||||||
print("{}\n", s);
|
|
||||||
}
|
|
||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#import c {
|
|
||||||
#include "vendors/test_c/test.h";
|
|
||||||
#source "vendors/test_c/test.c";
|
|
||||||
};
|
|
||||||
94
specs.md
94
specs.md
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
14
src/core.zig
14
src/core.zig
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
80
src/main.zig
80
src/main.zig
@@ -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 => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
49
src/wasm_shell.html
Normal 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…</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>
|
||||||
1
tests/expected/38-build-config.exit
Normal file
1
tests/expected/38-build-config.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
4
tests/expected/38-build-config.txt
Normal file
4
tests/expected/38-build-config.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
build config: ok
|
||||||
|
pointer size: 8
|
||||||
|
os: macos
|
||||||
|
64-bit platform
|
||||||
Reference in New Issue
Block a user