Sans Language Reference

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.

Getting Started

sans build hello.sans && ./hello

Sans compiles to native binaries via LLVM. Write a .sans file, build it, and run.

Types

TypeShortDescription
IntI64-bit signed integer
FloatF64-bit floating point
BoolBBoolean (true / false)
StringSUTF-8 string
Array<T>Dynamic growable array
Map<K,V>MGeneric hash map (default M<S,I>)
Option<T>O<T>Optional value: Some or None
Result<T>R<T>Success or error value
JsonValueJOpaque JSON value
HttpResponseHTTP client response (opaque handle)
HttpServerHTTP server socket (opaque handle)
HttpRequestHTTP server request
Sender<T>Channel sender (opaque handle)
Receiver<T>Channel receiver (opaque handle)
Mutex<T>Mutual exclusion lock (opaque handle)
JoinHandleThread handle (opaque handle)

User-defined types: struct, enum, trait. Trait objects: dyn TraitName.

Variable Declaration

x = 42              // immutable (type inferred)
x := 0              // mutable
let x Int = 42      // explicit type (verbose, optional)
let mut x = 0       // verbose mutable (optional)

Global Variables

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.

Function Definition

// 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 }

// Default parameters (trailing params only)
greet(name:S greeting:S="Hello") S = "{greeting} {name}!"
greet("Alice")            // "Hello Alice!"
greet("Bob" "Hi")        // "Hi Bob!"

Control Flow

// 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,
}

// Match with guards
match x {
    n if n > 0 => "positive",
    n if n < 0 => "negative",
    _ => "zero",
}

// Struct destructuring in match
match pt {
    Point { x, y } => x + y,
}

// Tuple destructuring in match
match pair {
    (a, b) => a + b,
}

// For-loop destructuring
for (k v) in m.entries() {
    p("{k} = {str(v)}")
}

Iterator Chains

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)
a.sum()  a.min()  a.max()      // aggregate operations
a.flat()                       // flatten nested arrays
a.reduce(0, |a:I b:I| I { a + b })  // fold
a.each(|x:I| I { p(x); 0 })    // side-effect iteration
a.flat_map(|x:I| I { range(x) }) // map + flatten

// Lazy iterators (Iter<T>) — no intermediate allocations
a.iter()                        // It<T> — lazy iterator over array
iter(10)                         // It<I> — range 0..10 (no alloc)
iter(2, 5)                       // It<I> — range 2..5 (no alloc)

// Lazy combinators (return Iter)
a.iter().map(|x:I| I { x * 2 })  // transform
.filter(|x:I| B { x > 3 })   // keep matches
.take(5).skip(2)                 // first 5, skip 2
.collect()                       // materialize to array

// Consumers (terminal)
a.iter().find(|x:I| B { x > 3 })    // O<T> — first match
a.iter().any(|x:I| B { x > 3 })     // B — true if any match
a.iter().all(|x:I| B { x > 0 })     // B — true if all match
iter(5).reduce(|a:I b:I| I { a + b }, 0)  // fold
iter(10).count()                   // 10
a.iter().for_each(|x:I| I { p(x); 0 })  // side effects

// For-loop with lazy iterators
for x in iter(10) { p(x) }

// 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"
s.pad_left(5, "0")              // "00042"
s.pad_right(6, ".")             // "hi...."
s.bytes()                       // [65, 66, 67] — byte values
s.to_int()                      // "42".to_int() = 42

// Unicode / UTF-8
char_count(s) ccount(s)         // UTF-8 codepoint count
chars(s)                        // split into UTF-8 char array
is_ascii(s)                     // 1 if all bytes < 128
utf8_valid(s)                   // 1 if valid UTF-8
string_reverse(s) srev(s)      // UTF-8 aware reverse
42.to_str()                     // 42.to_str() = "42"

// Math & range
abs(-5)  min(3, 7)  max(3, 7)
range(5)                          // [0 1 2 3 4]
range(2, 5)                       // [2 3 4]

// Float math
floor(3.7)  ceil(3.2)  round(3.5)  // 3.0, 4.0, 4.0
sqrt(4.0)  pow(2.0, 3.0)            // 2.0, 8.0
sin(0.0)  cos(0.0)  tan(0.0)     // trig (radians)
log(1.0)  exp(1.0)  fabs(-5.5)  // 0.0, 2.718..., 5.5
PI()  E_CONST()                     // constants
sleep(1000)  time()  random(100)  // system
stof("3.14")                       // string to float

// Date/time (all operate on unix timestamps)
t = tnow()
tfmt(t "%Y-%m-%d %H:%M:%S")       // format with strftime
tyear(t) tmon(t) tday(t)         // year, month 1-12, day 1-31
thour(t) tmin(t) tsec(t)         // hour 0-23, min 0-59, sec 0-59
twday(t)                          // weekday 0=Sun..6=Sat
tadd(t 3600)                      // add seconds
tdiff(a b)                        // a - b in seconds

// Match expressions
match x { 1 => "one", 2 => "two", _ => "other" }
match cmd { "build" => 1, "run" => 2, _ => 0 }

// Tuple destructuring
let (a, b) = (10 20)

// Map entries
m.entries()                        // [(key, val), ...]
m.delete("key")                    // remove key

Operators

CategoryOperators
Arithmetic+, -, *, /, %
Comparison==, !=, <, >, <=, >=
Boolean&&, ||, !
Assignment=, :=, +=, -=, *=, /=, %=
Special?: (ternary), ? (try/propagate), ! (postfix unwrap), [] (index)

Maps

Generic hash map. The type parameter specifies key and value types. Bare M() defaults to M<S,I> (string keys, integer values). Supported key types: S (String), I (Int). Float keys are not allowed.

m = M()                   // M<S I> — default
m = M<S S>()              // string→string
m = M<I I>()              // int→int
m.set("x" 10)
m.get("x")!              // 10 (unwrap Option)
m.get("z").unwrap_or(0) // 0 (missing key)
m.has("z")               // false
m.len()                  // 1
m.keys()                 // ["x"]

Breaking change (v0.7.1): m.get(key) now returns Option<V> instead of a raw value. Use !, .unwrap(), or .unwrap_or(default) to extract the value.

Map Methods

MethodSignatureDescription
set(key, val)(K, V) -> ISet key-value pair
get(key)(K) -> Option<V>Get value — Some(v) or None
has(key)(K) -> BCheck if key exists
len()() -> INumber of entries
keys()() -> [K]Array of all keys
vals()() -> [V]Array of all values
delete(key)(K) -> IRemove key
entries() -> [(K V)]Key-value tuples

String Interpolation & Slicing

name = "Sans"
msg = "Hello {name}!"    // "Hello Sans!"

Expression Interpolation

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

String Slicing

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

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.

Structs

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
}

Generic Structs

Structs can have type parameters, enabling reusable data structures:

struct Pair<A B> { first A, second B }

main() {
    p = Pair<I S>{ first: 1, second: "hello" }
    p(str(p.first))     // 1
    p(p.second)          // "hello"
    0
}

// Multiple type params
struct Triple<A B C> { a A, b B, c C }
t = Triple<I S B>{ a: 42, b: "hi", c: true }

Enums

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,
}

Traits & Generics

trait Describable {
    fn describe(self) I
}

impl Describable for Point {
    fn describe(self) I { self.x + self.y }
}

identity<T>(x T) T = x

Trait Objects (dyn Trait)

Trait objects enable dynamic dispatch through a vtable. Use dyn TraitName as a type and expr as dyn TraitName to coerce a concrete struct. Runtime layout: 16-byte heap-allocated fat pointer (data ptr + vtable ptr).

trait Valued {
    fn value(self) I
}

struct Num { n I }
impl Valued for Num {
    fn value(self) I { self.n }
}

x = Num{ n: 42 }
v = x as dyn Valued   // coerce to trait object
v.value()               // 42 — vtable dispatch

// dyn Trait as parameter type
show(v dyn Valued) I { v.value() }
show(x as dyn Valued)  // 42

// Polymorphic collections
items = [x1 as dyn Valued  x2 as dyn Valued]
for item in items { p(str(item.value())) }

Limitations: no trait inheritance, no default implementations, no associated types, no generic bounds.

Lambdas & Closures

Lambda expressions are anonymous functions. They implicitly capture variables from their enclosing scope (up to 8 captured variables per closure).

// 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 })

Error Handling

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
}

Error Codes

err() accepts an optional integer error code as the first argument:

err("not found")           // error, code defaults to 0
err(404 "not found")       // error with code 404
r.code()                    // get error code (0 if not set)

Error Propagation (? operator)

The ? operator unwraps a Result<T> or early-returns the error. On Option<T>, it unwraps Some or early-returns none().

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) for Result; and to early-return none() for Option.

Result Combinators

Chain Result operations without manual error checks:

parse(s:S) R<I> = s == "" ? err("empty") : ok(stoi(s))

parse("42").map(|n:I| I { n * 2 })         // ok(84)
parse("10").and_then(|n:I| R<I> { n > 0 ? ok(n) : err("neg") })
parse("").map_err(|e:S| S { "parse failed: {e}" })
parse("").or_else(|e:S| R<I> { ok(0) })   // ok(0) fallback

Option

Option<T> (short: O<T>) represents an optional value. Used by Map.get and Array.find. Runtime layout: 16 bytes (tag@0, value@8).

x = some(42)                 // Some(42)
y = none()                    // None
x.is_some                     // true
x!                             // 42 (unwrap, exits on None)
x.unwrap_or(0)               // 42
none().unwrap_or(99)          // 99

// Map.get returns Option
m = M()
m.set("x" 10)
m.get("x")!                  // 10
m.get("z").unwrap_or(0)     // 0 (missing)

// ? propagation
lookup(m:M<S I> k:S) O<I> {
    v = m.get(k)?              // returns none() early if missing
    some(v * 2)
}

Modules

// math.sans
add(a:I b:I) = a + b

// main.sans
import "math"

main() {
    result = math.add(3 4)
    result
}

Module Re-exports

Use pub import to re-export all public symbols from another module. Downstream consumers see a single clean API.

// impl.sans
pub add(a:I b:I) I = a + b
pub sub(a:I b:I) I = a - b
helper() I = 42        // not pub — not re-exported

// facade.sans
pub import "impl"      // re-exports add and sub

// main.sans
import "facade"
main() I {
  p(facade.add(1 2))   // 3
  p(facade.sub(5 3))   // 2
  0
}

Only pub symbols from the source module are re-exported. Non-pub symbols remain private.

Package Manager

Sans includes a built-in package manager. Packages are git repositories fetched by version tag.

sans.json

{
  "name": "my-project",
  "version": "0.1.0",
  "deps": {
    "github.com/user/repo": "v1.0.0"
  }
}

Commands

CommandDescription
sans pkg initCreate sans.json
sans pkg add <url> [tag]Add dependency
sans pkg installInstall all dependencies
sans pkg remove <url>Remove dependency
sans pkg listList dependencies
sans pkg update <url> [tag]Update dependency version
sans pkg search <query>Search package index

Short aliases: sans pkg i (install), sans pkg ls (list), sans pkg rm (remove).

Packages are cached at ~/.sans/packages/. Dependencies are resolved transitively via BFS.

Linter

sans lint runs static analysis (parse + type check) without building.

sans lint foo.sans                        // lint single file
sans lint compiler/                       // lint all .sans files recursively
sans lint .                               // lint current directory
sans lint --error=unused-imports foo.sans  // promote rule to error
sans lint --quiet foo.sans                // suppress warnings

Rules

RuleDefaultDescription
unused-importswarnImported module never referenced
unreachable-codewarnCode after return statement
empty-catchwarnResult value silently discarded
shadowed-varswarnInner scope redeclares outer variable
unnecessary-mutwarnVariable declared := but never reassigned

Configuration

Rules can be configured in sans.json:

{
  "lint": {
    "unused-imports": "error",
    "shadowed-vars": "off"
  }
}

Valid severities: "error", "warn", "off". CLI --error=<rule> overrides config. Exit code 0 if no errors, 1 if any error-severity diagnostics.

Concurrency

worker(tx Sender<Int>) I {
    tx.send(42)
    0
}

main() {
    let (tx rx) = channel<I>()
    spawn worker(tx)
    val = rx.recv
    val
}

Built-in Functions

I/O

FunctionAliasSignature
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
read_lines(path)rl(String) -> Array<String>
write_lines(path, lines)wl(String, Array<String>) -> Int
append_line(path, line)al(String, String) -> Int
read_line(prompt)--(String) -> String

Buffered I/O

Line-oriented file I/O for common read/write patterns:

// Write lines to a file (each line terminated by \n)
wl("/tmp/data.txt", ["hello" "world" "sans"])

// Read all lines from a file
lines = rl("/tmp/data.txt")
p(lines[0])  // "hello"

// Append a single line (with trailing \n)
al("/tmp/data.txt", "extra")

// Interactive prompt (prints prompt, reads line, trims)
name = read_line("Enter name: ")

read_lines strips the trailing newline before splitting, so a file with content "a\nb\n" returns ["a", "b"]. write_lines adds a trailing newline after each line. append_line appends the line followed by a newline.

Type Conversion

FunctionAliasSignature
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

JSON

FunctionAliasSignature
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) -> Result<JsonValue> — parses int, float, string, bool, null, object, array. Returns error on invalid JSON or depth > 512. Breaking (v0.8.1): was JsonValue; add ! to unwrap.
json_stringify(v)jfy(JsonValue) -> String

HTTP Client

FunctionAliasSignature
http_get(url)hg(String) -> HttpResponse
http_post(url, body, ct)hp(String, String, String) -> HttpResponse

HTTP Server

FunctionAliasSignature
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. Uses a bounded thread pool (default 256 workers). Requests are read incrementally until headers are complete, then body is read based on Content-Length. On SIGINT/SIGTERM, the server stops accepting new connections, drains in-flight workers, and exits cleanly.

Server Configuration

Configure the HTTP server before calling serve() or serve_tls(). All settings have sensible defaults.

FunctionDefaultDescription
set_max_workers(n)256Max concurrent worker threads. 503 at capacity.
set_read_timeout(s)30Read timeout per recv call (seconds).
set_keepalive_timeout(s)60Timeout for next request on keep-alive (seconds).
set_drain_timeout(s)5Shutdown drain timeout (seconds).
set_max_body(n)1048576 (1MB)Max request body bytes. 413 if exceeded.
set_max_headers(n)8192 (8KB)Max total header size bytes. 431 if exceeded.
set_max_header_count(n)100Max number of headers. 431 if exceeded.
set_max_url(n)8192 (8KB)Max URL length bytes. 414 if exceeded.
set_compress_min_size(bytes)1024Min response body size in bytes to trigger gzip compression.
main() I {
  set_max_workers(128)
  set_read_timeout(10)
  set_max_body(4096)
  serve(8080 fptr("handle"))
}

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.

Automatic Gzip Compression

respond() automatically gzip-compresses response bodies when all conditions are met:

No code changes needed — compression is transparent. Opt out with:

req.set_header("X-No-Compress" "1")
req.respond(200 large_body)

WebSocket

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"))
}

WebSocket Extensions

ws_send_binary(ws, data, len) sends a binary WebSocket frame. ws_ping(ws) sends a ping frame. stream_write_json(w, data) writes a JSON-formatted SSE chunk to a streaming response.

File Uploads

req.file(name) returns an uploaded file's content (multipart/form-data). req.files(name) returns all files with the given field name.

handle(req:HR) I {
  data = req.file("upload")
  if slen(data) == 0 { return req.respond(400 "no file") }
  fw("/tmp/upload.bin" data)
  req.respond(200 "saved")
}

Static File Serving

serve_file(req, dir) serves a static file from dir matching the request path. Handles content-type detection for 24+ MIME types, 404 for missing files, and directory traversal protection.

set_index_file(name) sets the default file for directory requests (default: index.html).

FunctionSignatureDescription
serve_file(req, dir)(HttpRequest, String) -> IntServe static file from directory
set_index_file(name)(String) -> IntSet default index filename

Router

Pattern-based request routing with path parameters (:name) and wildcards (*).

FunctionSignatureDescription
router()() -> IntCreate router
rget(r, pattern, handler)(Int, String, Fn) -> IntRegister GET route
rpost(r, pattern, handler)(Int, String, Fn) -> IntRegister POST route
rput(r, pattern, handler)(Int, String, Fn) -> IntRegister PUT route
rdelete(r, pattern, handler)(Int, String, Fn) -> IntRegister DELETE route
route(r, method, pattern, handler)(Int, String, String, Fn) -> IntRegister any-method route
handle(r, req)(Int, HttpRequest) -> IntDispatch request through router
set_not_found(r, handler)(Int, Fn) -> IntCustom 404 handler
serve_static(r, prefix, dir)(Int, String, String) -> IntServe static files under prefix
param(req, name)(HttpRequest, String) -> StringGet path parameter
g r = 0

users_get(req:HR) I {
  id = param(req "id")
  req.respond(200 "{\"id\":{id}}")
}

dispatch(req:HR) I { handle(r req) }

main() I {
  r = router()
  rget(r "/users/:id" fptr("users_get"))
  serve_static(r "/" "./public")
  serve(8080 fptr("dispatch"))
}

TCP/UDP Networking

TCP

FunctionAliasSignatureDescription
tcp_connect(host, port)(String, Int) -> IntConnect to host:port, returns fd or -1
tcp_listen(port)tl(Int) -> IntListen on port, returns server fd
tcp_accept(fd)ta(Int) -> IntAccept connection, returns client fd
tcp_read(fd, size)tr(Int, Int) -> StringRead up to size bytes
tcp_write(fd, data)tw(Int, String) -> IntWrite data
tcp_close(fd)tc(Int) -> IntClose fd
tcp_set_timeout(fd, ms)(Int, Int) -> IntSet recv timeout (ms)

UDP

FunctionAliasSignatureDescription
udp_bind(port)ub(Int) -> IntBind UDP socket to port
udp_sendto(sock, host, port, data)(Int, String, Int, String) -> IntSend datagram
udp_recvfrom(sock, size)(Int, Int) -> IntReceive datagram
udp_close(sock)(Int) -> IntClose socket

CORS

FunctionSignatureDescription
cors(req, origin)(HttpRequest, String) -> IntSet CORS headers with given origin. Call before respond.
cors_all(req)(HttpRequest) -> IntSet CORS headers with wildcard origin (*).
srv = listen(8080)
while true {
    req = srv.accept
    cors_all(req)
    req.respond(200 "ok")
}

Logging

FunctionAliasSignature
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

Assertions

Built-in assertion functions for testing. Each prints a diagnostic with the source line number on failure and exits with code 1.

FunctionSignatureDescription
assert(cond)(Bool) -> IntFail if cond is false (zero)
assert_eq(a, b)(Int, Int) -> IntFail if a != b, prints expected vs got
assert_ne(a, b)(Int, Int) -> IntFail if a == b, prints the equal value
assert_ok(r)(Result<T>) -> IntFail if r is an err
assert_err(r)(Result<T>) -> IntFail if r is ok
assert_some(o)(Option<T>) -> IntFail if o is none
assert_none(o)(Option<T>) -> IntFail if o is some
assert(1 == 1)
assert_eq(42, 42)
assert_ne(1, 2)
assert_ok(ok(42))
assert_err(err("bad"))
assert_some(some(1))
assert_none(none())

Path Manipulation

FunctionAliasSignatureDescription
path_join(a, b)pjoin(S, S) -> SJoin two path segments with /. If b is absolute, returns b.
path_dir(p)pdir(S) -> SDirectory component (before last /). Returns "." if none.
path_base(p)pbase(S) -> SFilename component (after last /).
path_ext(p)pext(S) -> SFile extension including .. Returns "" if none.
path_stem(p)pstem(S) -> SFilename without extension.
path_is_abs(p)pabs(S) -> IReturns 1 if path starts with /, else 0.
path_join("foo" "bar")            // "foo/bar"
path_dir("/home/user/file.sans")  // "/home/user"
path_base("/home/user/file.sans") // "file.sans"
path_ext("file.sans")             // ".sans"
path_stem("file.sans")            // "file"
pabs("/home")                     // 1

Encoding

FunctionAliasSignatureDescription
base64_encode(s)b64e(S) -> SBase64 encode a string.
base64_decode(s)b64d(S) -> SBase64 decode a string.
url_encode(s)urle(S) -> SPercent-encode for URLs.
url_decode(s)urld / ud(S) -> SDecode percent-encoded string (+ becomes space).
hex_encode(s)hexe(S) -> SHex encode (each byte → 2 lowercase hex chars).
hex_decode(s)hexd(S) -> SHex decode (2 hex chars → byte).
base64_encode("Hello, World!")  // "SGVsbG8sIFdvcmxkIQ=="
base64_decode("SGVsbG8sIFdvcmxkIQ==")  // "Hello, World!"
url_encode("hello world&foo=bar")  // "hello%20world%26foo%3Dbar"
url_decode("hello%20world")  // "hello world"
hex_encode("ABC")  // "414243"
hex_decode("414243")  // "ABC"

Crypto

Hash functions, HMAC, and cryptographic random bytes via OpenSSL. All return lowercase hex-encoded strings.

FunctionAliasSignatureDescription
sha256(s)(S) -> SSHA-256 hash (64-char hex).
sha512(s)(S) -> SSHA-512 hash (128-char hex).
md5(s)(S) -> SMD5 hash (32-char hex).
hmac_sha256(key, msg)hmac256(S, S) -> SHMAC-SHA256 (64-char hex).
random_bytes(n)randb(I) -> Sn crypto random bytes as hex (2*n chars).
sha256("hello")              // "2cf24dba5fb0a30e..."
md5("hello")                 // "5d41402abc4b2a76..."
hmac_sha256("secret" "msg")  // HMAC-SHA256 hex digest
hmac256("key" "data")       // alias
r = random_bytes(16)        // 32-char random hex string
r = randb(8)                 // 16-char random hex string

Regex (POSIX ERE)

FunctionAliasSignatureDescription
regex_match(pattern, text)rmatch(S, S) -> IReturns 1 if text matches pattern, 0 otherwise.
regex_find(pattern, text)rfind(S, S) -> SFirst match substring, or "" if none.
regex_replace(pattern, text, replacement)rrepl(S, S, S) -> SReplace first match with replacement.
regex_match("[0-9]+" "hello123")    // 1
rmatch("[0-9]+" "hello")            // 0
regex_find("[0-9]+" "hello123world") // "123"
rfind("[a-z]+" "123abc456")         // "abc"
regex_replace("[0-9]+" "hello123world" "XXX")  // "helloXXXworld"
rrepl("[0-9]+" "no digits" "XXX")              // "no digits"

Unicode / UTF-8

Sans strings are byte arrays. These functions interpret bytes as UTF-8 without changing the underlying representation. slen(s) returns byte length; use char_count(s) for codepoint count.

FunctionAliasSignatureDescription
char_count(s)ccount(S) -> ICount UTF-8 codepoints (not bytes).
chars(s)(S) -> Array<S>Split string into array of UTF-8 characters.
is_ascii(s)(S) -> IReturns 1 if all bytes are ASCII (< 128).
utf8_valid(s)(S) -> IReturns 1 if string is valid UTF-8.
string_reverse(s)srev(S) -> SUTF-8 aware reverse (preserves multi-byte chars).
char_count("hello")         // 5
is_ascii("hello")           // 1
utf8_valid("hello")         // 1
string_reverse("hello")    // "olleh"
srev("abc")                // "cba"
c = chars("hi")            // ["h" "i"]

Filesystem & Process

FunctionAliasSignatureDescription
getenv(name)genv(String) -> StringRead environment variable. Returns "" if not set.
mkdir(path)(String) -> IntCreate directory and parents (mkdir -p). Returns 1 on success, 0 on error.
rmdir(path)(String) -> IntRemove empty directory. Returns 1 on success, 0 on error.
remove(path)rm(String) -> IntDelete a file. Returns 1 on success, 0 on error.
listdir(path)ls(String) -> Array<String>List directory contents. Returns empty array on error.
is_dir(path)(String) -> BoolCheck if path is a directory.
sh(cmd)shell(String) -> StringExecute command and capture stdout. Returns "" on failure.
// Environment
home = getenv("HOME")

// Filesystem
mkdir("build/output")
is_dir("build/output")       // true
files = listdir("src/")
remove("old.txt")
rmdir("build/output")

// Process
output = sh("uname -s")

Low-Level Primitives

These enable Sans to replace its own C runtime. Pointers are stored as Int (i64).

Memory

FunctionSignatureDescription
alloc(size)(Int) -> Intmalloc, returns pointer
dealloc(ptr)(Int) -> Intfree
ralloc(ptr, size)(Int, Int) -> Intrealloc
mcpy(dst, src, n)(Int, Int, Int) -> Intmemcpy
mcmp(a, b, n)(Int, Int, Int) -> Intmemcmp
slen(ptr)(Int) -> Intstrlen on raw pointer (byte length — use char_count() for UTF-8)
load8(ptr)(Int) -> Intload byte (0-255)
store8(ptr, val)(Int, Int) -> Intstore byte
load16(ptr)(Int) -> Intload 16-bit value
store16(ptr, val)(Int, Int) -> Intstore 16-bit value
load32(ptr)(Int) -> Intload 32-bit value
store32(ptr, val)(Int, Int) -> Intstore 32-bit value
load64(ptr)(Int) -> Intload 64-bit value
store64(ptr, val)(Int, Int) -> Intstore 64-bit value
strstr(haystack, needle)(Int, Int) -> Intfind substring, 0 if not found
bswap16(val)(Int) -> Intbyte-swap 16-bit (htons)
bxor(a, b)(Int, Int) -> Intbitwise XOR (native 64-bit)
band(a, b)(Int, Int) -> Intbitwise AND (native 64-bit)
bor(a, b)(Int, Int) -> Intbitwise OR (native 64-bit)
bshl(a, b)(Int, Int) -> Intbitwise shift left (native 64-bit)
bshr(a, b)(Int, Int) -> Intbitwise shift right (native 64-bit)
pmutex_init(ptr)(Int) -> IntInitialize a raw pthread mutex
pmutex_lock(ptr)(Int) -> IntLock a raw pthread mutex
pmutex_unlock(ptr)(Int) -> IntUnlock a raw pthread mutex
exit(code)(Int) -> Intexit process
system(cmd) / sys(cmd)(String) -> Intrun shell command, return exit code
gzip_compress(data, len)(Int, Int) -> Intgzip-compress data; returns ptr to [compressed_ptr, compressed_len]

Arena Allocator

Phase-based bump allocator. All allocations between arena_begin() and arena_end() are freed at once. Arenas nest up to 8 deep.

FunctionSignatureDescription
arena_begin()() -> IntPush a new arena onto the stack
arena_alloc(size)(Int) -> IntBump-allocate from the current arena (8-byte aligned)
arena_end()() -> IntPop and free all memory in the current arena

I/O

FunctionSignatureDescription
wfd(fd, msg)(Int, String) -> Intwrite string to file descriptor

SSL (Advanced)

Low-level TLS/SSL bindings. For most use cases, prefer https_listen.

FunctionSignatureDescription
ssl_ctx(cert, key)(String, String) -> IntCreate SSL context from cert/key file paths
ssl_accept(ctx, fd)(Int, Int) -> IntPerform TLS handshake on accepted socket fd
ssl_read(ssl, buf, len)(Int, Int, Int) -> IntRead bytes from TLS connection
ssl_write(ssl, buf, len)(Int, Int, Int) -> IntWrite bytes to TLS connection
ssl_close(ssl)(Int) -> IntShut down TLS connection and free SSL object

Sockets

FunctionSignatureDescription
sock(domain, type, proto)(Int, Int, Int) -> Intsocket()
sbind(fd, port)(Int, Int) -> Intbind to port
slisten(fd, backlog)(Int, Int) -> Intlisten()
saccept(fd)(Int) -> Intaccept()
srecv(fd, buf, len)(Int, Int, Int) -> Intrecv()
ssend(fd, buf, len)(Int, Int, Int) -> Intsend()
sclose(fd)(Int) -> Intclose()
rbind(fd, addr, len)(Int, Int, Int) -> Intraw bind()
rsetsockopt(fd, level, opt, val, len)(Int, Int, Int, Int, Int) -> Intraw setsockopt()

Curl

FunctionSignatureDescription
cinit()() -> Intcurl_easy_init
csets(h, opt, val)(Int, Int, String) -> Intsetopt with string
cseti(h, opt, val)(Int, Int, Int) -> Intsetopt with long
cperf(h)(Int) -> Intcurl_easy_perform
cclean(h)(Int) -> Intcurl_easy_cleanup
cinfo(h, info, buf)(Int, Int, Int) -> Intcurl_easy_getinfo
curl_slist_append(slist, str)(Int, Int) -> Intappend to curl header list
curl_slist_free(slist)(Int) -> Intfree curl header list

Function Pointers

FunctionSignatureDescription
fptr("name")(String) -> Intget pointer to named function
fcall(ptr, arg)(Int, Int) -> Intcall function pointer with 1 arg
fcall2(ptr, a, b)(Int, Int, Int) -> Intcall function pointer with 2 args
fcall3(ptr, a, b, c)(Int, Int, Int, Int) -> Intcall function pointer with 3 args

Pointer Access

FunctionSignatureDescription
ptr(s)(String|Map|Array) -> Intget raw i64 pointer of string, map, or array
char_at(s, i)(String, Int) -> Intread byte at index i (shorthand for load8(ptr(s) + i))

Map Operations

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.

FunctionSignatureDescription
mget(map, key)(Int, String) -> Intget value from Map by key (0 if not found)
mset(map, key, val)(Int, String, Int) -> Intset key-value pair in Map
mhas(map, key)(Int, String) -> Intcheck if Map contains key (1=yes, 0=no)

File I/O

FunctionSignatureDescription
read_file(path)(String) -> Stringread entire file to string
write_file(path, content)(String, String) -> Intwrite string to file
args()() -> Array<String>get command-line arguments

Error Handling

FunctionSignature
ok(value)(T) -> Result<T>
err(message)(String) -> Result<_>
err(code, message)(Int, String) -> Result<_>

Methods by Type

Array<T>

MethodSignatureNotes
push(value)(T) -> IntAppend element
pop() -> TRemove and return last
get(index) or [index](Int) -> TRead element
set(index, value)(Int, T) -> IntWrite element
len() -> IntLength
remove(index)(Int) -> TRemove at index
contains(value)(T) -> BoolCheck membership
map(fn)((T) -> U) -> Array<U>Transform elements
filter(fn)((T) -> Bool) -> Array<T>Filter elements
any(fn)((T) -> Bool) -> BoolTrue if any element matches
find(fn)((T) -> Bool) -> Option<T>First match, or None
enumerate() -> Array<(Int, T)>Index-value tuples
zip(other)(Array<U>) -> Array<(T, U)>Paired tuples

String

MethodAliasSignature
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

JsonValue

MethodSignature
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

HttpResponse

MethodSignature
status() -> Int
body() -> String
header(name)(String) -> String
ok() -> Bool

HttpServer / HttpRequest

TypeMethodSignatureNotes
HttpServeraccept() -> HttpRequest
HttpRequestpath() -> String
HttpRequestmethod() -> String
HttpRequestbody() -> String
HttpRequestheader(name)(String) -> StringGet request header value (case-insensitive)
HttpRequestset_header(name, value)(String, String) -> IntAdd custom response header (call before respond)
HttpRequestquery(name)(String) -> StringGet query parameter value by name
HttpRequestpath_only() -> StringPath without query string
HttpRequestcontent_length() -> IntGet Content-Length as int
HttpRequestcookie(name)(String) -> StringGet cookie value from Cookie header
HttpRequestform(name)(String) -> StringParse form field from POST body (URL-encoded or multipart)
HttpRequestfile(name)(String) -> StringGet uploaded file contents (multipart/form-data)
HttpRequestfiles(name)(String) -> Array<String>Get all uploaded files for field name
HttpRequestrespond(status, body)(Int, String) -> IntDefaults to text/html content-type
HttpRequestrespond(status, body, content_type)(Int, String, String) -> IntExplicit content-type
HttpRequestrespond_json(status, body)(Int, String) -> IntJSON response (sets Content-Type: application/json)
HttpRequestrespond_stream(status)(Int) -> IntChunked streaming response, returns writer handle
HttpRequestis_ws_upgrade() -> IntReturns 1 if WebSocket upgrade request
HttpRequestupgrade_ws() -> IntPerform WS handshake, return WebSocket handle

Option<T>

MethodSignatureNotes
is_some() -> Bool
is_none() -> Bool
unwrap or !() -> TExits on None
unwrap_or(default)(T) -> T

Result<T>

MethodSignatureNotes
is_ok() -> Bool
is_err() -> Bool
unwrap or !() -> TExits on error
unwrap_or(default)(T) -> T
error() -> StringError message
code() -> IntError code (0 if not set)
map(fn)((T) -> U) -> Result<U>Transform ok value
and_then(fn)((T) -> Result<U>) -> Result<U>Chain fallible operations
map_err(fn)((String) -> String) -> Result<T>Transform error message
or_else(fn)((String) -> Result<T>) -> Result<T>Recover from error

Concurrency Types

TypeMethodSignature
Sender<T>send(value)(T) -> Int
Receiver<T>recv() -> T
Mutex<T>lock() -> T
Mutex<T>unlock(value)(T) -> Int
JoinHandlejoin() -> Int

Runtime Safety

Array Bounds Checking

Array GET (a[i]) and SET (a[i] = v) are bounds-checked at runtime. Out-of-bounds access prints an error and exits:

a = [1 2 3]
a[10]     // error: index out of bounds: index 10 but length is 3
a[10] = 5 // error: index out of bounds: index 10 but length is 3

String Bounds Checking

char_at() is bounds-checked at runtime:

s = "hi"
s.char_at(99)  // error: string index out of bounds: index 99 but length is 2

JSON Parse Returns Result (v0.8.1 Breaking Change)

json_parse(s) / jp(s) now returns Result<JsonValue> instead of JsonValue. On invalid input, it returns an error Result with a descriptive message instead of a null JsonValue.

// Before (v0.8.0):
j = jp("{\"name\":\"Alice\"}")    // JsonValue (null on error)

// After (v0.8.1+):
j = jp("{\"name\":\"Alice\"}")!   // unwrap Result to get JsonValue
// or handle the error:
r = jp(input)
if r.is_err { p("bad json") } else { r!.get("name") }

Migration: Add ! after every json_parse() / jp() call, or use ? to propagate the error.

JSON Recursion Depth Limit

JSON parsing enforces a maximum nesting depth of 512 levels. Inputs with deeper nesting return an error Result:

r = jp(deeply_nested_string)
// r.is_err == true, r.error() == "JSON parse error: maximum nesting depth exceeded"

Scope GC Walks JSON Types

Returning nested JSON values from functions no longer causes use-after-free. The scope-based garbage collector now walks JSON object and array trees, promoting all referenced memory to the caller's scope on return.

SIGPIPE Handling

HTTP and HTTPS servers automatically ignore SIGPIPE so that client disconnects during a write do not crash the server process.

Panic Recovery

Panic recovery allows programs to catch unwrap failures (! on Err/None) and recover instead of exiting. Implemented with setjmp/longjmp; primarily intended for use in server request handlers.

buf := panic_get_buf()
rv := setjmp(buf)
if rv != 0 {
    req.respond(500 "internal error")
    panic_disable()
    0
} else {
    panic_enable()
    result = risky_op()!  // longjmp on Err/None instead of exit
    req.respond(200 str(result))
    panic_disable()
    0
}
FunctionSignatureDescription
setjmp(buf)(Int) -> IntSet jump point. Returns 0 initially, non-zero on longjmp
longjmp(buf, val)(Int, Int) -> IntJump to setjmp point with value
panic_enable()() -> IntEnable panic recovery (unwrap uses longjmp instead of exit)
panic_disable()() -> IntDisable panic recovery
panic_is_active()() -> IntReturns 1 if recovery is active, 0 otherwise
panic_get_buf()() -> IntGet the jmp_buf pointer
panic_fire()() -> IntCall longjmp to panic buf manually

Internals

Runtime Modules

The standard library is implemented across 28 modules in runtime/: arena.sans, array_ext.sans, bitwise.sans, curl.sans, encoding.sans, fs.sans, functional.sans, http.sans (HTTP client), io.sans, iter.sans, json.sans (JSON), log.sans, map.sans, math.sans, net.sans, option.sans, path.sans, process.sans, rc.sans (scope GC), result.sans, router.sans, server.sans (HTTP server, WebSocket), sock.sans, ssl.sans, static_file.sans, string_ext.sans, unicode.sans, websocket.sans.

Compiler Modules

The compiler is 7 modules in compiler/ (~11,600 LOC): lexer, parser, typeck, constants, IR, codegen, main. Compiles to LLVM IR via llc, links with clang.

Builtin Names

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, rl, wl, al, read_lines, write_lines, append_line, read_line, 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, tnow, tfmt, tyear, tmon, tday, thour, tmin, tsec, twday, tadd, tdiff, and all others listed in the reference.

Compiler Diagnostics

The Sans compiler reports errors with source location, the offending source line, and a caret pointing to the token:

file.sans:12:5: error: undefined variable 'foo'
    foo + 1
    ^

The compiler collects multiple errors before exiting — all errors in a file are reported in a single pass. Warnings are also emitted for common issues and do not prevent compilation.

Warnings

The compiler emits warnings for:

Known Limitations