Port library/modules/std/sched.sx to run on aarch64-linux alongside aarch64-macOS, validated byte-identical on both via Apple `container`. Per-OS bits are comptime-branched: - MAP_AP (mmap MAP_ANON flag): linux 0x22 / macOS 0x1002. - fd-readiness backend: epoll on linux, kqueue on darwin (epoll import scoped to the linux branch). block_on_fd, the run-loop Mode-2 drain, and cancel_io_waiter_for each branch; the epoll paths EPOLL_CTL_DEL on fire and on early-wake (EPOLLONESHOT only disables a registration; kqueue EV_ONESHOT auto-removes it). - first-entry trampoline: a per-OS hand-written global-asm symbol becomes a naked sx fn fib_tramp (mov x0,x19; br x20) + register-indirect dispatch (spawn presets regs[1] == x20 == &fib_dispatch), dropping the per-OS .global symbol entirely. Fixes issue 0193 Bug A: the trampoline redesign bus-errored on the go/wait/sleep capstone (1817) until `export "fib_dispatch"` was restored. Without the export, fib_dispatch reverts to sx's internal ABI (x0 = implicit context, first arg self shifted to x1) while the trampoline hands self over in x0 (C-ABI); on first entry the body runs (x1 happens to alias self) but the closure then loads regs[1] == &fib_dispatch as its first capture and re-invokes fib_dispatch forever -> stack overflow -> bus error. The export pins fib_dispatch to the C-ABI (self in x0), matching the trampoline. Root cause found via lldb on an AOT build; confirmed against the compiler source. Bug B (a top-level asm block wrapped in inline-if is dropped during the comptime-conditional flatten) is carved out to issue 0194 (OPEN) -- no live trigger remains, since the naked-fn trampoline sidesteps it. 1811/1814/1816/1817 run byte-identical on the aarch64-macOS host and in an aarch64-linux container; full suite green (817/0). Documents the fiber runtime in readme.md.
594 lines
18 KiB
Markdown
594 lines
18 KiB
Markdown
# sx
|
|
|
|
A programming language with compile-time execution, generics, closures,
|
|
protocols, and an LLVM backend — compiled to fast native code.
|
|
|
|
## At a Glance
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
Point :: struct {
|
|
x, y: i32;
|
|
magnitude :: (self: *Point) -> f32 { sqrt(self.x * self.x + self.y * self.y); }
|
|
}
|
|
|
|
main :: () {
|
|
p := Point.{ x = 3, y = 4 };
|
|
print("point: {}, magnitude: {}\n", p, p.magnitude());
|
|
}
|
|
```
|
|
|
|
**Highlights:**
|
|
|
|
- Clean declaration syntax: `name :: value` for constants, `name := value` for variables
|
|
- Compiles to native code via LLVM
|
|
- Compile-time execution with `#run` and code generation with `#insert`
|
|
- Generics via monomorphization
|
|
- First-class closures with value capture
|
|
- Protocol-based polymorphism (traits) with optional inline dispatch
|
|
- Pattern matching on enums, optionals, and type categories
|
|
- C interop via `extern` / `export` and `#import c`
|
|
- Inline assembly as a first-class expression
|
|
- Colorblind async via a pure-sx cooperative fiber runtime (no function coloring)
|
|
- Targets: macOS (ARM64, x86_64), Linux (x86_64, ARM64), Windows (x86_64), WebAssembly
|
|
|
|
## Usage
|
|
|
|
```sh
|
|
sx run file.sx # compile and run
|
|
sx build file.sx # compile to binary
|
|
sx build file.sx -o out # compile with output path
|
|
sx ir file.sx # emit LLVM IR
|
|
sx lsp # start language server
|
|
```
|
|
|
|
Options:
|
|
```
|
|
--target <triple> target platform (shortcuts: macos, linux, windows, wasm)
|
|
--opt <level> optimization: none, less, default, aggressive
|
|
--cpu <name> target CPU
|
|
-o <path> output path
|
|
```
|
|
|
|
## Language Overview
|
|
|
|
### Types
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `i8`..`i64`, `u8`..`u64` | Signed/unsigned integers (default: `i64`) |
|
|
| `f32`, `f64` | Floating point (default: `f32`) |
|
|
| `bool` | `true` / `false` |
|
|
| `string` | UTF-8 fat pointer `{ptr, len}` |
|
|
| `[N]T` | Fixed-size array |
|
|
| `[]T` | Slice (fat pointer) |
|
|
| `*T`, `[*]T` | Single / many pointer |
|
|
| `?T` | Optional |
|
|
| `struct`, `enum`, `union` | Composite types |
|
|
| `Closure(args) -> ret` | Closure type |
|
|
|
|
A fixed array `[N]T` coerces to a slice `[]T` (its length is known); a `[*]T`
|
|
many-pointer carries no length, so slice it explicitly with `ptr[0..len]`.
|
|
|
|
**Numeric limits.** A field access on a builtin integer type folds to a
|
|
compile-time constant: `i64.max`, `u8.min`, `[u8.max]T` (a 255-element array).
|
|
Floats expose `.min` / `.max` plus `.epsilon`, `.min_positive`, `.true_min`,
|
|
`.inf`, and `.nan`. See `specs.md` → Numeric Limits.
|
|
|
|
### Declarations
|
|
|
|
```sx
|
|
// Constants (compile-time when possible)
|
|
PI :: 3.14159;
|
|
MAX : i32 : 100;
|
|
|
|
// Variables (mutable)
|
|
x := 42; // inferred type
|
|
y : i32 = 0; // explicit type
|
|
z : i32 = ---; // uninitialized
|
|
```
|
|
|
|
A typed constant's initializer must be compatible with its annotation (checked at
|
|
compile time for both literals and constant expressions). Mixed int+float
|
|
arithmetic promotes to float in either operand order.
|
|
|
|
**Aggregate constants.** Array- and struct-typed `::` constants are immutable
|
|
globals — one storage, reads index directly, whole-value uses copy by value,
|
|
unused tables are dropped from the binary. `::` is the one and only const
|
|
spelling:
|
|
|
|
```sx
|
|
K : [4]i64 : .[11, 22, 33, 44]; // typed array const
|
|
A :: .[1, 2, 3]; // untyped — infers [3]i64
|
|
M :: .[1, 2.2, 3]; // numeric mix promotes — [3]f64
|
|
LIT :: Color.{ r = 255, g = 0, b = 0 }; // struct const
|
|
|
|
N :: K[0] + K[3]; // 55 — const element reads fold at compile time
|
|
D : [K.len]u8 = ---; // .len folds in dimensions too
|
|
K[0] = 5; // error: cannot assign through constant 'K'
|
|
```
|
|
|
|
Writes through a constant's name are compile errors; a local copy (`k := K`)
|
|
stays writable.
|
|
|
|
**Float → integer narrowing.** A float flowing into an integer binding without a
|
|
cast must be integral: an integral compile-time float folds to its integer, a
|
|
non-integral one is a compile error (`y : i64 = 4.0` → `4`; `y : i64 = 1.5`
|
|
errors). This is uniform across locals, defaults, arguments, constants, and array
|
|
dimensions. An explicit `xx` / `cast(i64)` is the escape hatch and always
|
|
truncates.
|
|
|
|
**Reserved names.** Builtin type names (`i32`, `u8`, `bool`, `string`, …) can't
|
|
be used bare as identifiers at value-binding or declaration sites. Member
|
|
positions (struct fields, union tags, protocol methods) are exempt, as is any
|
|
name after a leading `.`. A leading backtick escapes one into a raw identifier
|
|
(`` `i2 ``), usable in every position:
|
|
|
|
```sx
|
|
`i2 := 2.5; // identifier "i2", distinct from the i2 type
|
|
`i2 :: struct { x: i64; } // a type named with a reserved spelling
|
|
v : `i2 = ---; // referenced as a type
|
|
x : i2 = 3; // bare `i2` in type position is still the int type
|
|
```
|
|
|
|
### Structs
|
|
|
|
```sx
|
|
Vec3 :: struct {
|
|
x, y, z: f32;
|
|
|
|
length :: (self: *Vec3) -> f32 {
|
|
sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
|
|
}
|
|
}
|
|
|
|
v := Vec3.{ x = 1, y = 2, z = 3 };
|
|
v2 := Vec3.{ 1, 2, 3 }; // positional
|
|
print("{}\n", v.length());
|
|
```
|
|
|
|
Structs support field defaults, `#using` for composition, and methods defined in
|
|
the body.
|
|
|
|
### Enums (Tagged Unions)
|
|
|
|
```sx
|
|
Shape :: enum {
|
|
circle: f32;
|
|
rect: struct { w, h: f32; };
|
|
none;
|
|
}
|
|
|
|
area :: (s: Shape) -> f32 {
|
|
if s == {
|
|
case .circle: (r) => 3.14159 * r * r;
|
|
case .rect: (r) => r.w * r.h;
|
|
case .none: 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
Flag enums with power-of-2 values:
|
|
```sx
|
|
Perms :: enum flags { read; write; execute; }
|
|
rw := Perms.read | Perms.write;
|
|
```
|
|
|
|
Set a variant by construction (`s = .circle(2.0)`), which writes the tag and
|
|
payload together. Direct member assignment to a variant (`s.circle = 2.0`) is
|
|
rejected; mutating a sub-field of the active variant in place (`s.rect.w = 9.0`)
|
|
is fine.
|
|
|
|
### Optionals
|
|
|
|
```sx
|
|
x: ?i32 = 42;
|
|
y: ?i32 = null;
|
|
|
|
val := x ?? 0; // null coalescing
|
|
forced := x!; // force unwrap (traps on null)
|
|
|
|
if v := x { // safe unwrap
|
|
print("{}\n", v);
|
|
}
|
|
|
|
// Optional chaining
|
|
node: ?Node = get_node();
|
|
name := node?.name ?? "unknown";
|
|
|
|
// Flow-sensitive narrowing: a `!= null` guard proves the value present.
|
|
n: ?i32 = maybe();
|
|
if n != null { take_i32(n); } // `n` is i32 here
|
|
```
|
|
|
|
A `?T` never implicitly unwraps to `T` in a value position — a bare `take_i32(n)`
|
|
without a guard, `!`, `??`, or binding is a compile error.
|
|
|
|
### Generics
|
|
|
|
```sx
|
|
max :: (a: $T, b: T) -> T {
|
|
if a > b then a else b;
|
|
}
|
|
|
|
List :: struct ($T: Type) {
|
|
items: []T; // a slice; items.len is the live count, so a List is
|
|
cap: i64; // directly iterable: `for xs.items (e) { ... }`
|
|
|
|
append :: (self: *List(T), item: T) { ... }
|
|
|
|
// `#get` / `#set` property accessors: read/write via field syntax
|
|
// (`xs.len`, `xs.len = n`) rather than method calls.
|
|
len :: (self: *List(T)) -> i64 #get => self.items.len;
|
|
len :: (self: *List(T), v: i64) #set { self.items.len = v; }
|
|
}
|
|
```
|
|
|
|
Generic constraints via protocols:
|
|
```sx
|
|
are_equal :: ($T: Type/Eq, a: T, b: T) -> bool { a.eq(b); }
|
|
```
|
|
|
|
### Closures
|
|
|
|
```sx
|
|
make_adder :: (n: i64) -> Closure(i64) -> i64 {
|
|
closure((x: i64) -> i64 => x + n);
|
|
}
|
|
|
|
add5 := make_adder(5);
|
|
print("{}\n", add5(100)); // 105
|
|
```
|
|
|
|
Closures capture by value. Bare functions auto-promote to closures when needed.
|
|
|
|
### Protocols
|
|
|
|
```sx
|
|
Drawable :: protocol {
|
|
draw :: (self: *Self, x: i32, y: i32); // receiver is explicit + required
|
|
}
|
|
|
|
impl Drawable for Circle {
|
|
draw :: (self: *Circle, x: i32, y: i32) { ... }
|
|
}
|
|
|
|
shape : Drawable = xx my_circle; // type erasure via xx
|
|
shape.draw(10, 20); // dynamic dispatch
|
|
```
|
|
|
|
`#inline` protocols store function pointers directly (no vtable indirection):
|
|
```sx
|
|
Allocator :: protocol #inline {
|
|
alloc :: (self: *Self, size: i64) -> *void;
|
|
dealloc :: (self: *Self, ptr: *void);
|
|
}
|
|
```
|
|
|
|
### Pattern Matching
|
|
|
|
```sx
|
|
// On enums
|
|
if shape == {
|
|
case .circle: (r) => print("radius: {}\n", r);
|
|
case .rect: (r) => print("{}x{}\n", r.w, r.h);
|
|
case .none: print("nothing\n");
|
|
}
|
|
|
|
// On optionals
|
|
if opt == {
|
|
case .some: (val) => use(val);
|
|
case .none: fallback();
|
|
}
|
|
|
|
// On type categories (via Any)
|
|
if type_of(val) == {
|
|
case int: print("integer\n");
|
|
case string: print("string\n");
|
|
case struct: print("struct\n");
|
|
}
|
|
```
|
|
|
|
### Control Flow
|
|
|
|
```sx
|
|
// Chained comparisons
|
|
if 0 <= x <= 100 { ... }
|
|
|
|
// While
|
|
while i < 10 { i += 1; }
|
|
|
|
// For — collections, ranges, and parallel iteration
|
|
for items (val) { print("{}\n", val); }
|
|
for items, 0.. (val, idx) { print("[{}] = {}\n", idx, val); }
|
|
for 1..=5, 0.. (a, b) { print("{}:{}\n", a, b); } // a: 1..5, b follows
|
|
for items (val) => total += val; // arrow body
|
|
for 0<..<n (i) { } // bound markers: 1 .. n-1
|
|
sub := items[1..=3]; // slices take them too
|
|
|
|
// Defer
|
|
f := open("file.txt");
|
|
defer close(f);
|
|
|
|
// Multi-target assignment (atomic swap)
|
|
a, b = b, a;
|
|
```
|
|
|
|
### Pipe Operator
|
|
|
|
```sx
|
|
result := data |> parse() |> transform() |> serialize();
|
|
// equivalent to: serialize(transform(parse(data)))
|
|
```
|
|
|
|
### Compile-Time Execution
|
|
|
|
```sx
|
|
// Evaluate at compile time
|
|
FIBONACCI_10 :: #run fib(10);
|
|
|
|
// Generate code at compile time
|
|
#insert #run generate_lookup_table();
|
|
```
|
|
|
|
### C Interop
|
|
|
|
```sx
|
|
libc :: #library "c";
|
|
printf :: (fmt: [:0]u8, args: ..Any) -> i32 extern libc;
|
|
write_fd :: (fd: i32, buf: [*]u8, count: u64) -> i64 extern libc "write";
|
|
```
|
|
|
|
`extern` imports a symbol defined elsewhere; `export` is its dual — define a
|
|
function in sx and expose it under the C ABI so C can call back in. Both imply
|
|
`abi(.c)` and take an optional `[LIB] ["csym"]` rename tail:
|
|
|
|
```sx
|
|
abs :: (x: i32) -> i32 extern; // import an external C symbol
|
|
sx_square :: (x: i32) -> i32 export { x * x } // define + expose to C
|
|
__stdinp : *void extern; // extern data global
|
|
```
|
|
|
|
Direct C header import:
|
|
```sx
|
|
#import c {
|
|
#include "vendors/mylib/api.h";
|
|
#source "vendors/mylib/impl.c";
|
|
};
|
|
```
|
|
|
|
### Inline Assembly
|
|
|
|
`asm` is an expression. The body is a brace block: a template string first, then
|
|
operands and an optional `clobbers(.…)` clause. Each operand is
|
|
`[name]? "constraint" <role>`, where the role is `-> Type` (a value output) or
|
|
`= expr` (an input). It compiles to an LLVM inline-asm call (AT&T syntax).
|
|
|
|
```sx
|
|
// one value output, two register-class inputs
|
|
add :: (a: i64, b: i64) -> i64 {
|
|
return asm { "add %[out], %[a], %[b]", [out] "=r" -> i64, [a] "r" = a, [b] "r" = b };
|
|
}
|
|
```
|
|
|
|
Outputs decide the result: **0** → `void` (asm must be `volatile`); **1** → that
|
|
type; **N** → a destructurable `Tuple` named by each operand. A top-level
|
|
`asm { … }` block is global (module-level) assembly. See
|
|
[docs/inline-assembly.md](docs/inline-assembly.md) for the full guide.
|
|
|
|
### Modules
|
|
|
|
```sx
|
|
#import "modules/std.sx"; // flat import
|
|
math :: #import "modules/math"; // namespaced import (directory: all .sx files merged)
|
|
```
|
|
|
|
A flat import makes a module's top-level names bare-visible; a namespaced import
|
|
binds only its alias, reached as `m.name`. Visibility does **not** chain — a flat
|
|
import of a flat import is not bare-visible two hops away; qualify it or
|
|
`#import` the module directly. Bare names that two flat imports both provide are
|
|
ambiguous and must be qualified. When a module declares its own same-name symbol,
|
|
that wins over any import.
|
|
|
|
A facade can re-export another module's members as its own declarations
|
|
(ordinary aliases), which its direct importers then see bare:
|
|
|
|
```sx
|
|
// facade.sx
|
|
r :: #import "rich.sx";
|
|
helper :: r.helper; // fn re-export
|
|
Thing :: r.Thing; // struct re-export
|
|
Box :: r.Box; // generic head re-export — same template
|
|
```
|
|
|
|
The stdlib prelude uses exactly this: `std.sx` is a pure re-export facade, so
|
|
`#import "modules/std.sx"` gives every bare prelude name (`print`, `List`,
|
|
`Context`, …) plus carried namespaces (`mem`, `fs`, `process`, `socket`, `json`,
|
|
`cli`, `hash`, `xml`, `log`, `test`):
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
main :: () {
|
|
gpa := mem.GPA.init(); // mem :: #import — carried from std.sx
|
|
log.warn("count = {}", 3);
|
|
s := xml.escape("<a & b>");
|
|
}
|
|
```
|
|
|
|
### Implicit Context
|
|
|
|
Every program gets an implicit `context` with a default allocator:
|
|
|
|
```sx
|
|
// No boilerplate needed — context is auto-initialized
|
|
main :: () {
|
|
list := List(i64).create(); // uses context.allocator
|
|
list.append(42);
|
|
}
|
|
|
|
// Override allocator for a scope
|
|
push Context.{ allocator = my_arena } {
|
|
do_work(); // all allocations use my_arena
|
|
}
|
|
```
|
|
|
|
## Quick Sort Example
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
quick_sort :: (items: []$T) {
|
|
partition :: (items: []T, lo: i64, hi: i64) -> i64 {
|
|
pivot := items[hi];
|
|
i := lo - 1;
|
|
j := lo;
|
|
while j < hi {
|
|
if items[j] < pivot {
|
|
i += 1;
|
|
items[i], items[j] = items[j], items[i];
|
|
}
|
|
j += 1;
|
|
}
|
|
i += 1;
|
|
items[i], items[hi] = items[hi], items[i];
|
|
i;
|
|
}
|
|
|
|
sort :: (items: []T, lo: i64, hi: i64) {
|
|
if lo < hi {
|
|
pi := partition(items, lo, hi);
|
|
sort(items, lo, pi - 1);
|
|
sort(items, pi + 1, hi);
|
|
}
|
|
}
|
|
|
|
sort(items, 0, items.len - 1);
|
|
}
|
|
|
|
main :: () {
|
|
arr : []i64 = .[333, 2, 3, 5, 2, 2, 3, 4, 5, 6, 6, 1];
|
|
quick_sort(arr);
|
|
print("{}\n", arr);
|
|
// [1, 2, 2, 2, 3, 3, 4, 5, 5, 6, 6, 333]
|
|
}
|
|
```
|
|
|
|
## Standard Library
|
|
|
|
The standard library (`modules/std.sx`) provides:
|
|
|
|
- **I/O**: `print(fmt, args...)`, `out(str)`
|
|
- **Collections**: `List($T)` (dynamic array)
|
|
- **Strings**: `concat`, `substr`, `int_to_string`, `uint_to_string`, `float_to_string`, `cstring`
|
|
- **Memory**: `Allocator` protocol, `GPA` (general purpose), `Arena` (bump allocator)
|
|
- **Math**: `sqrt`, `sin`, `cos`
|
|
- **Introspection**: `type_of`, `type_name`, `size_of`, `align_of`, `field_count`, `field_name`, `field_value`, and more
|
|
|
|
### Atomics (`modules/std/atomic.sx`)
|
|
|
|
Opt-in import. `Atomic($T)` is a transparent wrapper over an integer/pointer-sized
|
|
`T`; the memory `Ordering` is an explicit compile-time value parameter:
|
|
|
|
```sx
|
|
#import "modules/std/atomic.sx";
|
|
|
|
counter : Atomic(i64) = .init(0);
|
|
counter.store(0, .relaxed);
|
|
n := counter.load(.acquire);
|
|
prev := counter.fetch_add(1, .seq_cst); // + fetch_sub/and/or/xor/min/max
|
|
old := counter.swap(42, .acq_rel);
|
|
|
|
// compare-exchange returns ?T — null = SUCCESS; a present value is the actual
|
|
// current value on failure (for a retry loop).
|
|
got := counter.compare_exchange(old, 99, .acq_rel, .acquire);
|
|
if got == null { /* swapped */ } else { /* retry with got! */ }
|
|
|
|
fence(.seq_cst); // standalone memory fence
|
|
```
|
|
|
|
`Ordering` = `relaxed`/`acquire`/`release`/`acq_rel`/`seq_cst`. Invalid
|
|
combinations are compile errors. The same operations run at compile time (`#run`)
|
|
under single-threaded semantics.
|
|
|
|
### Async / Concurrency (`modules/std/sched.sx`)
|
|
|
|
A pure-sx cooperative fiber runtime — **colorblind async**, with no `async` /
|
|
`await` keywords and no function coloring. Any function can suspend; a `Scheduler`
|
|
drives any number of stackful fibers, each on its own guard-paged stack. The
|
|
high-level API is `go` to spawn a task and `wait` to suspend until it completes:
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
sched :: #import "modules/std/sched.sx";
|
|
|
|
main :: () {
|
|
s := sched.Scheduler.init();
|
|
ps := @s; // closures capture by value — capture a pointer to the scheduler
|
|
|
|
// The coordinator runs as a fiber so `wait` has a fiber to park.
|
|
s.spawn(() => {
|
|
a := ps.go(() -> i64 => { ps.sleep(30); 100 }); // launch async tasks
|
|
b := ps.go(() -> i64 => { ps.sleep(10); 20 });
|
|
c := ps.go(() -> i64 => { ps.sleep(20); 3 });
|
|
|
|
sum := (a.wait() or 0) + (b.wait() or 0) + (c.wait() or 0); // 123
|
|
print("sum: {}\n", sum);
|
|
});
|
|
|
|
s.run(); // drive the scheduler until all fibers finish
|
|
}
|
|
```
|
|
|
|
Tasks complete in deadline order, not spawn or await order. The runtime offers:
|
|
|
|
- **`go(work) -> *Task($R)`** / **`wait() -> R !TaskErr`** / **`cancel()`** — the
|
|
task layer. `wait` rides the `!` error channel so a cancel surfaces as
|
|
`error.Canceled`.
|
|
- **`spawn`**, **`yield_now`**, **`suspend_self`**, **`wake`** — the raw fiber
|
|
primitives the task layer is built on.
|
|
- **`sleep(ms)`** / **`now_ms()`** — timer-driven suspension on a virtual clock
|
|
(deterministic, no real wall time).
|
|
- **`block_on_fd(fd, want_read)`** — suspend until a file descriptor is ready,
|
|
backed by kqueue (darwin) or epoll (linux).
|
|
|
|
It's an M:1 model (cooperative, no preemption — so no data races between fibers
|
|
and no atomics needed across them), built on `abi(.naked)` context switching over
|
|
guarded `mmap` stacks. Currently aarch64-pinned (macOS + Linux).
|
|
|
|
### Command-line interface (`modules/std/cli.sx`)
|
|
|
|
`std.cli` builds command-line front-ends over an explicit logical argv: `os_args`
|
|
reads the real process argv, and `parse(args, commands, diag)` does subcommand
|
|
dispatch + `--flag` parsing, with named exit codes (`EX_OK`, `EX_USAGE`,
|
|
`EX_UNAVAILABLE`) and a `--json` machine-output convention.
|
|
|
|
## Cross-Compilation
|
|
|
|
```sh
|
|
sx build app.sx --target linux # Linux x86_64 (glibc, dynamic)
|
|
sx build app.sx --target linux-musl # Linux x86_64 (musl, static)
|
|
sx build app.sx --target macos-arm # macOS ARM64
|
|
sx build app.sx --target windows # Windows x86_64 (MSVC)
|
|
sx build app.sx --target windows-gnu # Windows x86_64 (MinGW)
|
|
sx build app.sx --target wasm # WebAssembly
|
|
```
|
|
|
|
### Self-contained builds
|
|
|
|
sx can link with a bundled toolchain instead of the host's system linker — it
|
|
supplies lld, the CRT, and libc (musl/glibc/mingw), so no `cc`/SDK needs to be
|
|
installed. The default Linux output is statically-linked musl, which runs on any
|
|
Linux.
|
|
|
|
```sh
|
|
sx build app.sx --target linux-musl --self-contained # static, portable ELF
|
|
sx build app.sx --self-contained # host target, hermetic link
|
|
sx build app.sx --no-self-contained # force the system toolchain
|
|
```
|
|
|
|
## License
|
|
|
|
MIT
|