Sans is a compiled programming language optimized for AI code generation. It compiles to native code via LLVM with performance comparable to C, Rust, and Go.
sans build hello.sans && ./hello
Sans compiles to native binaries via LLVM. Write a .sans file, build it, and run.
| Type | Short | Description |
|---|---|---|
Int | I | 64-bit signed integer |
Float | F | 64-bit floating point |
Bool | B | Boolean (true / false) |
String | S | UTF-8 string |
Array<T> | — | Dynamic growable array |
Map | M | Hash map with string keys |
Result<T> | R<T> | Success or error value |
JsonValue | — | Opaque JSON value |
HttpResponse | — | HTTP client response |
HttpServer | — | HTTP server socket |
HttpRequest | — | HTTP server request |
Sender<T> | — | Channel sender |
Receiver<T> | — | Channel receiver |
Mutex<T> | — | Mutual exclusion lock |
JoinHandle | — | Thread handle |
User-defined types: struct, enum, trait.
x = 42 // immutable (type inferred)
x := 0 // mutable
let x Int = 42 // explicit type (verbose, optional)
let mut x = 0 // verbose mutable (optional)
g counter = 0 // global mutable variable
inc() I {
counter = counter + 1
counter
}
Globals are mutable and accessible from any function. Declared at the top level with g.
// Full form
fn add(a Int, b Int) Int { a + b }
// Compact form (preferred)
add(a:I b:I) I = a + b
// Implicit return type (defaults to Int)
main() { 0 }
// Ternary
result = x > 0 ? x * 2 : x * -1
// If-else block
if condition {
body
} else {
body
}
// While loop
while condition { body }
// For-in loop
for item in array { body }
// Loop control
while cond {
if done { break } // exit loop immediately
if skip { continue } // skip to next iteration
}
for x in arr {
if x == 0 { continue } // works in for-in too
if x < 0 { break }
}
// Match expression
match value {
EnumName::Variant1 => expr1,
EnumName::Variant2(x) => x + 1,
}
Array methods return arrays, so they chain without .collect():
a.map(|x:I| I { x * 2 }).filter(|x:I| B { x > 3 })
// New methods
a.any(|x:I| B { x > 3 }) // B — true if any match
a.find(|x:I| B { x > 3 }) // first match or 0
a.enumerate() // [(I I)] index-value tuples
a.zip(b) // [(I I)] paired tuples
a.sort() // in-place sort (integers)
a.reverse() // in-place reverse
a.join(",") // "1,2,3" — join to string
a.slice(1, 3) // sub-array [start..end)
// String methods
s.upper() s.lower() // case conversion
s.index_of("sub") // position or -1
s.char_at(0) s.get(0) // single character as string
s.repeat(3) // "abcabcabc"
// Math & range
abs(-5) min(3, 7) max(3, 7)
range(5) // [0 1 2 3 4]
range(2, 5) // [2 3 4]
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, % |
| Comparison | ==, !=, <, >, <=, >= |
| Boolean | &&, ||, ! |
| Assignment | =, :=, +=, -=, *=, /=, %= |
| Special | ?: (ternary), ? (try/propagate), ! (postfix unwrap), [] (index) |
Hash map with string keys. Constructor: M() or map().
m = M()
m.set("x" 10)
m.set("y" 20)
m.get("x") // 10
m.has("z") // false
m.len() // 2
m.keys() // ["x" "y"]
| Method | Signature | Description |
|---|---|---|
set(key, val) | (S, I) -> I | Set key-value pair |
get(key) | (S) -> I | Get value, 0 if missing |
has(key) | (S) -> B | Check if key exists |
len() | () -> I | Number of entries |
keys() | () -> [S] | Array of all keys |
vals() | () -> [I] | Array of all values |
name = "Sans"
msg = "Hello {name}!" // "Hello Sans!"
Full expressions are supported inside {}:
x = 10
"result is {x + 1}" // "result is 11"
"len is {a.len()}" // method calls
"sum is {x * 2 + 3}" // arithmetic
Slice strings with [start:end] syntax (desugars to .substring()):
s = "hello world"
s[0:5] // "hello"
s[6:] // "world" (to end)
s[:5] // "hello" (from start)
Tuples are fixed-size, ordered collections of values that can have different types. No commas — space-separated like arrays.
// Tuple literal (no commas)
t = (1 "hello" true)
// Access by index
t.0 // 1
t.1 // "hello"
t.2 // true
// Tuple type annotation
pair(a:I b:I) (I I) = (a b)
// Multi-return via tuples
p = pair(10 20)
p.0 + p.1 // 30
Single expressions in parens are grouping, not tuples: (1 + 2) evaluates to 3, not a 1-tuple.
struct Point { x I, y I }
make_point(x:I y:I) Point = Point { x: x, y: y }
main() {
pt = Point { x: 10, y: 20 }
p(str(pt.x + pt.y))
0
}
enum Shape {
Circle(I),
Rect(I, I),
}
area(s Shape) I = match s {
Shape::Circle(r) => r * r * 3,
Shape::Rect(w h) => w * h,
}
trait Describable {
fn describe(self) I
}
impl Describable for Point {
fn describe(self) I { self.x + self.y }
}
identity<T>(x T) T = x
Lambda expressions are anonymous functions. They implicitly capture variables from their enclosing scope.
// Non-capturing lambda
f = |x:I| I { x + 10 }
f(5) // 15
// Multiple parameters
add = |a:I b:I| I { a + b }
// Used with map
nums = [1 2 3 4 5]
doubled = nums.map(|x:I| I { x * 2 })
// Implicit capture from enclosing scope
multiplier = 3
scaled = nums.map(|x:I| I { x * multiplier })
divide(a:I b:I) R<I> = b == 0 ? err("div/0") : ok(a / b)
main() {
r = divide(10 3)
r.is_ok ? r! : 0
}
? operator)The ? operator unwraps a Result<T> or early-returns the error:
safe_div(a:I b:I) R<I> {
b == 0 ? err("div by zero") : ok(a / b)
}
compute(x:I) R<I> {
r = safe_div(x 2)? // unwraps ok(5), or returns err early
ok(r + 1)
}
x? desugars to: if x.is_err() { return x } followed by x! (unwrap).
// math.sans
add(a:I b:I) = a + b
// main.sans
import "math"
main() {
result = math.add(3 4)
result
}
worker(tx Sender<Int>) I {
tx.send(42)
0
}
main() {
let (tx rx) = channel<I>()
spawn worker(tx)
val = rx.recv
val
}
| Function | Alias | Signature |
|---|---|---|
print(value) | p | (String|Int|Float|Bool) -> Int |
file_read(path) | fr | (String) -> String |
file_write(path, content) | fw | (String, String) -> Int |
file_append(path, content) | fa | (String, String) -> Int |
file_exists(path) | fe | (String) -> Bool |
| Function | Alias | Signature |
|---|---|---|
int_to_string(n) | str | (Int) -> String |
string_to_int(s) | stoi | (String) -> Int |
int_to_float(n) | itof | (Int) -> Float |
float_to_int(f) | ftoi | (Float) -> Int |
float_to_string(f) | ftos | (Float) -> String |
| Function | Alias | Signature |
|---|---|---|
json_object() | jo | () -> JsonValue |
json_array() | ja | () -> JsonValue |
json_string(s) | js | (String) -> JsonValue |
json_int(n) | ji | (Int) -> JsonValue |
json_bool(b) | jb | (Bool) -> JsonValue |
json_null() | jn | () -> JsonValue |
json_parse(s) | jp | (String) -> JsonValue |
json_stringify(v) | jfy | (JsonValue) -> String |
| Function | Alias | Signature |
|---|---|---|
http_get(url) | hg | (String) -> HttpResponse |
http_post(url, body, ct) | hp | (String, String, String) -> HttpResponse |
| Function | Alias | Signature |
|---|---|---|
http_listen(port) | listen | (Int) -> HttpServer |
https_listen(port, cert, key) | hl_s | (Int, String, String) -> HttpServer |
serve(port, handler) | — | (Int, Fn) -> Int |
serve_tls(port, cert, key, handler) | — | (Int, String, String, Fn) -> Int |
stream_write(writer, data) | — | (Int, String) -> Int |
stream_end(writer) | — | (Int) -> Int |
signal_handler(signum) | — | (Int) -> Int |
signal_check() | — | () -> Int |
spoll(fd, timeout_ms) | — | (Int, Int) -> Int |
ws_send(ws, msg) | — | (Int, String) -> Int |
ws_recv(ws) | — | (Int) -> String |
ws_close(ws) | — | (Int) -> Int |
serve_file(req, dir) | — | (HttpRequest, String) -> Int |
url_decode(s) | — | (String) -> String |
path_segment(path, idx) | — | (String, Int) -> String |
serve_file(req, dir) serves a static file from dir matching the request path. Handles content-type detection, 404 for missing files, and directory traversal protection.
url_decode(s) decodes a URL-encoded string (e.g. %20 to space, + to space).
path_segment(path, idx) extracts the segment at index idx from a URL path. path_segment("/api/users/42" 2) returns "42".
serve(port, handler) starts a production server with auto-threading, HTTP/1.1 keep-alive, automatic gzip compression, and graceful shutdown. Each connection spawns a thread. The handler receives an HttpRequest and should call respond or respond_stream. On SIGINT/SIGTERM, the server stops accepting new connections, lets in-flight requests finish, and exits cleanly.
req.respond_stream(status) sends chunked HTTP headers and returns a writer. Use stream_write(w, data) to send chunks and stream_end(w) to finalize.
signal_handler(signum) registers a signal handler that sets a global flag. signal_check() returns 1 if the signal was received. spoll(fd, timeout_ms) polls a file descriptor for readability with timeout, returning 1 if ready, 0 otherwise.
respond() automatically gzip-compresses response bodies when all conditions are met:
Accept-Encoding containing gzipX-No-Compress: 1 response header set by usertext/*, application/json, application/javascript, application/xml, image/svg+xml)No code changes needed — compression is transparent. Opt out with:
req.set_header("X-No-Compress" "1")
req.respond(200 large_body)
req.is_ws_upgrade() returns 1 if the request is a WebSocket upgrade request. req.upgrade_ws() performs the handshake and returns a WebSocket handle.
ws_send(ws, msg) sends a text frame. ws_recv(ws) receives the next text frame (handles ping/pong automatically, returns "" on close). ws_close(ws) sends a close frame and closes the socket.
handle(req:I) I {
req.is_ws_upgrade() ? {
ws = req.upgrade_ws()
msg := ws_recv(ws)
while slen(msg) > 0 {
ws_send(ws "echo: " + msg)
msg = ws_recv(ws)
}
ws_close(ws)
} : {
req.respond(200 "WebSocket server")
}
}
main() I {
serve(8080 fptr("handle"))
}
| Function | Signature | Description |
|---|---|---|
cors(req, origin) | (HttpRequest, String) -> Int | Set CORS headers with given origin. Call before respond. |
cors_all(req) | (HttpRequest) -> Int | Set CORS headers with wildcard origin (*). |
srv = listen(8080)
while true {
req = srv.accept
cors_all(req)
req.respond(200 "ok")
}
| Function | Alias | Signature |
|---|---|---|
log_debug(msg) | ld | (String) -> Int |
log_info(msg) | li | (String) -> Int |
log_warn(msg) | lw | (String) -> Int |
log_error(msg) | le | (String) -> Int |
log_set_level(n) | ll | (Int) -> Int |
Log levels: 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
These enable Sans to replace its own C runtime. Pointers are stored as Int (i64).
| Function | Signature | Description |
|---|---|---|
alloc(size) | (Int) -> Int | malloc, returns pointer |
dealloc(ptr) | (Int) -> Int | free |
ralloc(ptr, size) | (Int, Int) -> Int | realloc |
mcpy(dst, src, n) | (Int, Int, Int) -> Int | memcpy |
mcmp(a, b, n) | (Int, Int, Int) -> Int | memcmp |
slen(ptr) | (Int) -> Int | strlen on raw pointer |
load8(ptr) | (Int) -> Int | load byte (0-255) |
store8(ptr, val) | (Int, Int) -> Int | store byte |
load16(ptr) | (Int) -> Int | load 16-bit value |
store16(ptr, val) | (Int, Int) -> Int | store 16-bit value |
load32(ptr) | (Int) -> Int | load 32-bit value |
store32(ptr, val) | (Int, Int) -> Int | store 32-bit value |
load64(ptr) | (Int) -> Int | load 64-bit value |
store64(ptr, val) | (Int, Int) -> Int | store 64-bit value |
strstr(haystack, needle) | (Int, Int) -> Int | find substring, 0 if not found |
bswap16(val) | (Int) -> Int | byte-swap 16-bit (htons) |
exit(code) | (Int) -> Int | exit process |
system(cmd) / sys(cmd) | (String) -> Int | run shell command, return exit code |
gzip_compress(data, len) | (Int, Int) -> Int | gzip-compress data; returns ptr to [compressed_ptr, compressed_len] |
Phase-based bump allocator. All allocations between arena_begin() and arena_end() are freed at once. Arenas nest up to 8 deep.
| Function | Signature | Description |
|---|---|---|
arena_begin() | () -> Int | Push a new arena onto the stack |
arena_alloc(size) | (Int) -> Int | Bump-allocate from the current arena (8-byte aligned) |
arena_end() | () -> Int | Pop and free all memory in the current arena |
| Function | Signature | Description |
|---|---|---|
wfd(fd, msg) | (Int, String) -> Int | write string to file descriptor |
Low-level TLS/SSL bindings. For most use cases, prefer https_listen.
| Function | Signature | Description |
|---|---|---|
ssl_ctx(cert, key) | (String, String) -> Int | Create SSL context from cert/key file paths |
ssl_accept(ctx, fd) | (Int, Int) -> Int | Perform TLS handshake on accepted socket fd |
ssl_read(ssl, buf, len) | (Int, Int, Int) -> Int | Read bytes from TLS connection |
ssl_write(ssl, buf, len) | (Int, Int, Int) -> Int | Write bytes to TLS connection |
ssl_close(ssl) | (Int) -> Int | Shut down TLS connection and free SSL object |
| Function | Signature | Description |
|---|---|---|
sock(domain, type, proto) | (Int, Int, Int) -> Int | socket() |
sbind(fd, port) | (Int, Int) -> Int | bind to port |
slisten(fd, backlog) | (Int, Int) -> Int | listen() |
saccept(fd) | (Int) -> Int | accept() |
srecv(fd, buf, len) | (Int, Int, Int) -> Int | recv() |
ssend(fd, buf, len) | (Int, Int, Int) -> Int | send() |
sclose(fd) | (Int) -> Int | close() |
rbind(fd, addr, len) | (Int, Int, Int) -> Int | raw bind() |
rsetsockopt(fd, level, opt, val, len) | (Int, Int, Int, Int, Int) -> Int | raw setsockopt() |
| Function | Signature | Description |
|---|---|---|
cinit() | () -> Int | curl_easy_init |
csets(h, opt, val) | (Int, Int, String) -> Int | setopt with string |
cseti(h, opt, val) | (Int, Int, Int) -> Int | setopt with long |
cperf(h) | (Int) -> Int | curl_easy_perform |
cclean(h) | (Int) -> Int | curl_easy_cleanup |
cinfo(h, info, buf) | (Int, Int, Int) -> Int | curl_easy_getinfo |
curl_slist_append(slist, str) | (Int, Int) -> Int | append to curl header list |
curl_slist_free(slist) | (Int) -> Int | free curl header list |
| Function | Signature | Description |
|---|---|---|
fptr("name") | (String) -> Int | get pointer to named function |
fcall(ptr, arg) | (Int, Int) -> Int | call function pointer with 1 arg |
fcall2(ptr, a, b) | (Int, Int, Int) -> Int | call function pointer with 2 args |
fcall3(ptr, a, b, c) | (Int, Int, Int, Int) -> Int | call function pointer with 3 args |
| Function | Signature | Description |
|---|---|---|
ptr(s) | (String|Map|Array) -> Int | get raw i64 pointer of string, map, or array |
char_at(s, i) | (String, Int) -> Int | read byte at index i (shorthand for load8(ptr(s) + i)) |
Explicit Map built-ins. Use these when a Map is stored as Int (e.g. from load64) and method dispatch cannot determine the correct type.
| Function | Signature | Description |
|---|---|---|
mget(map, key) | (Int, String) -> Int | get value from Map by key (0 if not found) |
mset(map, key, val) | (Int, String, Int) -> Int | set key-value pair in Map |
mhas(map, key) | (Int, String) -> Int | check if Map contains key (1=yes, 0=no) |
| Function | Signature | Description |
|---|---|---|
read_file(path) | (String) -> String | read entire file to string |
write_file(path, content) | (String, String) -> Int | write string to file |
args() | () -> Array<String> | get command-line arguments |
| Function | Signature |
|---|---|
ok(value) | (T) -> Result<T> |
err(message) | (String) -> Result<_> |
| Method | Signature | Notes |
|---|---|---|
push(value) | (T) -> Int | Append element |
pop | () -> T | Remove and return last |
get(index) or [index] | (Int) -> T | Read element |
set(index, value) | (Int, T) -> Int | Write element |
len | () -> Int | Length |
remove(index) | (Int) -> T | Remove at index |
contains(value) | (T) -> Bool | Check membership |
map(fn) | ((T) -> U) -> Array<U> | Transform elements |
filter(fn) | ((T) -> Bool) -> Array<T> | Filter elements |
any(fn) | ((T) -> Bool) -> Bool | True if any element matches |
find(fn) | ((T) -> Bool) -> T | First match, or 0 |
enumerate | () -> Array<(Int, T)> | Index-value tuples |
zip(other) | (Array<U>) -> Array<(T, U)> | Paired tuples |
| Method | Alias | Signature |
|---|---|---|
len | () -> Int | |
substring(start, end) | (Int, Int) -> String | |
trim | () -> String | |
starts_with(prefix) | sw | (String) -> Bool |
ends_with(suffix) | ew | (String) -> Bool |
contains(needle) | (String) -> Bool | |
split(delimiter) | (String) -> Array<String> | |
replace(old, new) | (String, String) -> String |
| Method | Signature |
|---|---|
get(key) | (String) -> JsonValue |
get_index(index) | (Int) -> JsonValue |
get_string | () -> String |
get_int | () -> Int |
get_bool | () -> Bool |
len | () -> Int |
type_of | () -> String |
set(key, value) | (String, JsonValue) -> Int |
push(value) | (JsonValue) -> Int |
| Method | Signature |
|---|---|
status | () -> Int |
body | () -> String |
header(name) | (String) -> String |
ok | () -> Bool |
| Type | Method | Signature | Notes |
|---|---|---|---|
| HttpServer | accept | () -> HttpRequest | |
| HttpRequest | path | () -> String | |
| HttpRequest | method | () -> String | |
| HttpRequest | body | () -> String | |
| HttpRequest | header(name) | (String) -> String | Get request header value (case-insensitive) |
| HttpRequest | set_header(name, value) | (String, String) -> Int | Add custom response header (call before respond) |
| HttpRequest | query(name) | (String) -> String | Get query parameter value by name |
| HttpRequest | path_only | () -> String | Path without query string |
| HttpRequest | content_length | () -> Int | Get Content-Length as int |
| HttpRequest | cookie(name) | (String) -> String | Get cookie value from Cookie header |
| HttpRequest | form(name) | (String) -> String | Parse form field from POST body (URL-encoded or multipart) |
| HttpRequest | respond(status, body) | (Int, String) -> Int | Defaults to text/html content-type |
| HttpRequest | respond(status, body, content_type) | (Int, String, String) -> Int | Explicit content-type |
| HttpRequest | respond_json(status, body) | (Int, String) -> Int | JSON response (sets Content-Type: application/json) |
| HttpRequest | respond_stream(status) | (Int) -> Int | Chunked streaming response, returns writer handle |
| HttpRequest | is_ws_upgrade | () -> Int | Returns 1 if WebSocket upgrade request |
| HttpRequest | upgrade_ws | () -> Int | Perform WS handshake, return WebSocket handle |
| Method | Signature | Notes |
|---|---|---|
is_ok | () -> Bool | |
is_err | () -> Bool | |
unwrap or ! | () -> T | Exits on error |
unwrap_or(default) | (T) -> T | |
error | () -> String |
| Type | Method | Signature |
|---|---|---|
| Sender<T> | send(value) | (T) -> Int |
| Receiver<T> | recv | () -> T |
| Mutex<T> | lock | () -> T |
| Mutex<T> | unlock(value) | (T) -> Int |
| JoinHandle | join | () -> Int |
Sans is self-hosted: the compiler and runtime are both written in Sans.
All built-in capabilities are implemented in Sans using low-level primitives (alloc, load8/store8, mcpy, sockets, etc.). The runtime/ directory contains: server.sans, json.sans, string_ext.sans, array_ext.sans, map.sans, ssl.sans, http.sans, curl.sans, arena.sans, result.sans, functional.sans.
compiler/ contains a full Sans compiler (~11,600 LOC across 7 modules): lexer, parser, typeck, IR, codegen, main. It compiles to LLVM IR via llc, then links with clang.
Bootstrap stages: stage 0 (Rust-compiled) → stage 1 (self-compiled once) → stage 2 (self-compiled twice) → stage 3 (fixed point).
The compiler has builtin mappings for a large set of names. User-defined functions take precedence over builtins of the same name. Builtin names include:
p, print, str, stoi, itof, ftoi, ftos, fr, fw, fa, fe, file_read, file_write, file_exists, listen, serve, serve_file, serve_tls, alloc, dealloc, load8/16/32/64, store8/16/32/64, mcpy, slen, wfd, exit, system, ok, err, map, M, jp, jfy, jo, ja, sock, saccept, args, signal_handler, signal_check, and all others listed in the reference.
arena_begin/arena_end for phase-based bulk deallocation.