# 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 target platform (shortcuts: macos, linux, windows, wasm) --opt optimization: none, less, default, aggressive --cpu target CPU -o 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<.. 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" `, 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(""); } ``` ### 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