A function with no explicit return type (arrow `=> expr`, or a block whose `return <v>` drives the type) has its return type inferred from the body — but the body references the function's own params. resolveReturnType ran that inference before the params were pushed into self.scope (they're bound later, at body lowering), so inferExprType couldn't resolve them and yielded .unresolved, which reached LLVM emission and panicked. It only worked when a same-named binding lingered in scope from earlier lowering (e.g. inside the big smoke file). Bind the function's plain annotated value params into a temporary scope during return-type inference. Resolve their types via resolveTypeWithBindings rather than resolveParamType — the latter does variadic/pack bookkeeping that must run exactly once, at body lowering; calling it here too corrupted the format/index path. Variadic/pack/comptime/unannotated params are skipped (no by-name return dependency; their types come from substitution). Regression: examples/0308-closures-arrow-inferred-return.sx (arrow + block inferred-return, top-level + local). Resolves issue 0059. Suite: 293 passed.
sx
An experimental systems programming language with Jai-inspired syntax, compile-time execution, generics, closures, protocols, and an LLVM backend.
Status: Highly experimental. The language and compiler are under active development.
At a Glance
#import "modules/std.sx";
Point :: struct {
x, y: s32;
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());
}
Key characteristics:
- Jai-inspired declaration syntax:
name :: valuefor constants,name := valuefor variables - Compiles to native code via LLVM 19
- Compile-time execution with
#run - Generics via monomorphization
- First-class closures with value capture
- Protocol-based polymorphism (traits)
- Pattern matching on enums, optionals, and type categories
- C interop via
#foreignand#import c - Targets: macOS (ARM64, x86_64), Linux (x86_64, ARM64), Windows (x86_64), WebAssembly
Building
Requires Zig 0.16+ and LLVM 19+.
zig build
On macOS with Homebrew LLVM:
# default path: /opt/homebrew/opt/llvm@19
zig build
Custom LLVM path:
zig build -Dllvm-prefix=/path/to/llvm
Usage
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 |
|---|---|
s8..s64, u8..u64 |
Signed/unsigned integers (default: s64) |
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 |
Declarations
// Constants (compile-time when possible)
PI :: 3.14159;
MAX : s32 : 100;
// Variables (mutable)
x := 42; // inferred type
y : s32 = 0; // explicit type
z : s32 = ---; // uninitialized
Structs
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)
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:
Perms :: enum flags { read; write; execute; }
rw := Perms.read | Perms.write;
Optionals
x: ?s32 = 42;
y: ?s32 = 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";
Generics
max :: (a: $T, b: T) -> T {
if a > b then a else b;
}
List :: struct ($T: Type) {
items: [*]T;
len: s64;
append :: (self: *List(T), item: T) { ... }
}
Generic constraints via protocols:
are_equal :: ($T: Type/Eq, a: T, b: T) -> bool { a.eq(b); }
Closures
make_adder :: (n: s64) -> Closure(s64) -> s64 {
closure((x: s64) -> s64 => x + n);
}
add5 := make_adder(5);
print("{}\n", add5(100)); // 105
Closures capture by value. Bare functions auto-promote to closures when needed.
Protocols
Drawable :: protocol {
draw :: (x: s32, y: s32);
}
impl Drawable for Circle {
draw :: (self: *Circle, x: s32, y: s32) { ... }
}
shape : Drawable = xx my_circle; // type erasure via xx
shape.draw(10, 20); // dynamic dispatch
#inline protocols store function pointers directly (no vtable indirection):
Allocator :: protocol #inline {
alloc :: (size: s64) -> *void;
dealloc :: (ptr: *void);
}
Pattern Matching
// 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
// Chained comparisons
if 0 <= x <= 100 { ... }
// While
while i < 10 { i += 1; }
// For (arrays and slices)
for items: (val) { print("{}\n", val); }
for items: (val, idx) { print("[{}] = {}\n", idx, val); }
// Defer
f := open("file.txt");
defer close(f);
// Multi-target assignment (atomic swap)
a, b = b, a;
Pipe Operator
result := data |> parse() |> transform() |> serialize();
// equivalent to: serialize(transform(parse(data)))
Compile-Time Execution
// Evaluate at compile time
FIBONACCI_10 :: #run fib(10);
// Generate code at compile time
#insert #run generate_lookup_table();
C Interop
Foreign functions:
libc :: #library "c";
printf :: (fmt: [:0]u8, args: ..Any) -> s32 #foreign libc;
write_fd :: (fd: s32, buf: [*]u8, count: u64) -> s64 #foreign libc "write";
Direct C header import:
#import c {
#include "vendors/mylib/api.h";
#source "vendors/mylib/impl.c";
};
Modules
#import "modules/std.sx"; // flat import
math :: #import "modules/math.sx"; // namespaced import
Implicit Context
Every program gets an implicit context with a default allocator:
// No boilerplate needed — context is auto-initialized
main :: () {
list := List(s64).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
#import "modules/std.sx";
quick_sort :: (items: []$T) {
partition :: (items: []T, lo: s64, hi: s64) -> s64 {
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: s64, hi: s64) {
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 : []s64 = .[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,float_to_string,cstring - Memory:
Allocatorprotocol,GPA(general purpose),Arena(bump allocator) - Math:
sqrt,sin,cos - Introspection:
type_of,type_name,field_count,field_name,field_value,size_of
Cross-Compilation
sx build app.sx --target linux # Linux x86_64
sx build app.sx --target macos-arm # macOS ARM64
sx build app.sx --target windows # Windows x86_64
sx build app.sx --target wasm # WebAssembly
Acknowledgments
- Jonathan Blow for Jai, the language that inspired this one
- Andrew Kelley for Zig, which made this compiler a joy to write
License
MIT