Files
sx/readme.md
agra b9a29c39c5 docs(lang): fix invalid protocol method-signature snippets — (self) -> () -> s64 [F0.6]
A protocol method signature omits the receiver; a bare `self` has no type, so
`protocol { … :: (self) … }` fails at parse with 'expected :'. Correct the three
member-exemption doc snippets (readme.md, specs.md, issues/0089) to the valid
signature form, matching examples/0158's `Speaker :: protocol { s2 :: () -> s64; }`.
2026-06-04 23:02:09 +03:00

436 lines
11 KiB
Markdown

# 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
```sx
#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 :: value` for constants, `name := value` for 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 `#foreign` and `#import c`
- Targets: macOS (ARM64, x86_64), Linux (x86_64, ARM64), Windows (x86_64), WebAssembly
## Building
Requires **Zig 0.16+** and **LLVM 19+**.
```sh
zig build
```
On macOS with Homebrew LLVM:
```sh
# default path: /opt/homebrew/opt/llvm@19
zig build
```
Custom LLVM path:
```sh
zig build -Dllvm-prefix=/path/to/llvm
```
## 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 |
|------|-------------|
| `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 |
**Numeric limits.** A field-like access on a builtin integer type name folds to
a compile-time constant of that type: `s64.max``9223372036854775807`,
`u8.min``0`, `s3.max``3`. It works for every width `s1`..`s64` / `u1`..`u64`
plus `usize`/`isize`, and is usable anywhere a constant of that type is — including
array dimensions (`[u8.max]T` is a 255-element array).
### Declarations
```sx
// 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
```
Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare*
spelling can't be used as an identifier at a **value-binding or declaration-name**
site — a value binding (`:=` / typed local / parameter), a `::` constant or
function declaration, an `impl` method definition, or a `::` type declaration
(`struct` / `enum` / `union` / alias / `protocol` / …) — each is an error
(`s2 :: 5` and `s2 :: (n) { … }` are rejected just like `s2 := 5`). **Member-name
positions are exempt**: a struct *field*, a union *tag*, and a protocol
*method-signature* may be a bare reserved spelling (`struct { s2: s64 }`,
`union { u8: … }`, `protocol { s2 :: () -> s64 }`) — they are reached via `obj.name`,
so they never mis-lower. The bare exemption covers only the identifier-classified
reserved names (`s1`..`s64`, `u1`..`u64`, `bool`, `string`, `void`, `usize`,
`isize`, `Any`); `f32` and `f64` are lexer keywords, so even in a member slot they
need the backtick (`` struct { `f32: s64 } ``). A leading backtick escapes one into
a **raw identifier**:
`` `name `` is the literal identifier `name` (the backtick drops out of the text),
usable in **every** position — value, declaration, and type, and optional in the
exempt member positions. It is the only way handwritten sx can spell a reserved
name in a binding or declaration site.
```sx
`s2 := 2.5; // identifier "s2", distinct from the s2 type
print("{}\n", `s2); // 2.5 (or bare `s2` in value position)
`s2 :: struct { x: s64; } // declare a type named with a reserved spelling
v : `s2 = ---; // and reference it as a type — resolves to the struct
x : s2 = 3; // bare `s2` in type position is still the int type
```
It works in every identifier position — local, global, parameter, struct field,
union tag, function name, type/alias/import name, a top-level or struct-body
constant, and the control-flow / capture / binding forms (destructure, `if`/`while`
binding, `for` capture, match capture, `catch`/`onfail` tag) — and a reserved-spelled
function is bare-callable (`s2(10)`). A backtick name used as a type resolves to a
`` `name ``-declared type — including a parameterized template (`` `s2(s64) ``) and
under pointer/optional wrappers — else a normal `unknown type` error.
Foreign declarations from `#import c { … }` are exempt automatically: C names that
collide with reserved type names (e.g. `s1`, `s2`) import unedited, and a foreign
reserved-name function is bare-callable by its C name.
### 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;
```
### Optionals
```sx
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
```sx
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:
```sx
are_equal :: ($T: Type/Eq, a: T, b: T) -> bool { a.eq(b); }
```
### Closures
```sx
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
```sx
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):
```sx
Allocator :: protocol #inline {
alloc :: (size: s64) -> *void;
dealloc :: (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 (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
```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
Foreign functions:
```sx
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:
```sx
#import c {
#include "vendors/mylib/api.h";
#source "vendors/mylib/impl.c";
};
```
### Modules
```sx
#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:
```sx
// 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
```sx
#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**: `Allocator` protocol, `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
```sh
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](https://en.wikipedia.org/wiki/Jonathan_Blow) for Jai, the language that inspired this one
- [Andrew Kelley](https://andrewkelley.me) for Zig, which made this compiler a joy to write
## License
MIT