Documentation

Installation

Install XS with a single command:

curl -fsSL xslang.org/install | sh

This installs the xs compiler, the VM, and all built-in tools. Supports Linux, macOS, and Windows (via WSL).

Or build from source:

git clone https://github.com/xs-lang0/xs
cd xs
make
make install

Hello world

hello.xs
fn main() {
  println("hello, world")
}

Run it with xs run hello.xs. The main() function is auto-called if defined.

Type system

XS has gradual typing. You can start without types and add them incrementally. The checker only enforces annotated code.

types.xs
-- untyped, works fine
let x = 42
let name = "xs"

-- typed, compiler checks these
let count: int = 42
var name: str = "xs"
const MAX: i64 = 100

-- function signatures
fn add(a: int, b: int) -> int {
  return a + b
}

-- generics
fn first<T>(arr: [T]) -> T {
  return arr[0]
}

-- composite types
let nums: [int] = [1, 2, 3]
let pair: (int, str) = (42, "hello")
let maybe: int? = null

Functions

Functions support overloading, default arguments, variadic params, and expression bodies.

functions.xs
-- expression body shorthand
fn double(x) = x * 2

-- implicit return (last expression)
fn square(x) { x * x }

-- overloading by arity
fn greet() { println("hello!") }
fn greet(name) { println("hello, {name}!") }

-- default parameters
fn connect(host, port = 8080) {
  println("connecting to {host}:{port}")
}

-- variadic
fn sum(...args) {
  var total = 0
  for a in args { total = total + a }
  return total
}

-- closures
fn make_counter() {
  var count = 0
  return fn() {
    count = count + 1
    return count
  }
}

-- arrow lambdas
let inc = (x) => x + 1

Pattern matching

match.xs
enum Shape {
  Circle(radius),
  Rect(w, h),
  Point
}

fn area(s) {
  match s {
    Shape::Circle(r) => 3.14159 * r * r
    Shape::Rect(w, h) => w * h
    Shape::Point => 0.0
  }
}

-- guards, ranges, regex, or-patterns
fn classify(data) {
  match data {
    n @ 1..=10          => "small: {n}"
    "a" | "e" | "i"    => "vowel"
    /^[0-9]+$/          => "number string"
    [first, ..rest]     => "list starting with {first}"
    (x, y)              => "pair: ({x}, {y})"
    _                   => "unknown"
  }
}

-- string prefix patterns
fn parse_url(url) {
  match url {
    "https://" ++ rest => "secure: {rest}"
    "http://" ++ rest  => "insecure: {rest}"
    _                  => "unknown protocol"
  }
}

Effects

Algebraic effects let you perform operations without knowing how they'll be handled. The handler decides. Think of it as exceptions you can resume from.

effects.xs
effect Ask {
  fn prompt(msg) -> str
}

fn greet() {
  let name = perform Ask.prompt("name?")
  return "Hello, {name}!"
}

-- the handler decides what prompt() returns
let result = handle greet() {
  Ask.prompt(msg) => resume("World")
}
println(result)  -- Hello, World!

-- effects with accumulation
effect Log {
  fn log(msg)
}

var logs = []
handle {
  perform Log.log("first")
  perform Log.log("second")
} {
  Log.log(msg) => {
    logs.push(msg)
    resume(null)
  }
}
println(logs)  -- ["first", "second"]

Structs and traits

structs.xs
struct Point { x, y }

impl Point {
  fn distance(self) {
    return sqrt(self.x * self.x + self.y * self.y)
  }

  fn translate(self, dx, dy) {
    return Point { x: self.x + dx, y: self.y + dy }
  }

  -- operator overloading
  fn +(self, other) {
    return Point { x: self.x + other.x, y: self.y + other.y }
  }
}

-- traits
trait Describe {
  fn describe(self) -> str
}

impl Describe for Point {
  fn describe(self) -> str {
    return "({self.x}, {self.y})"
  }
}

-- struct spread/update
let p = Point { x: 10, y: 20 }
let p2 = Point { ...p, y: 30 }

-- classes with inheritance
class Animal {
  name = ""
  sound = "..."

  fn init(self, name) {
    self.name = name
  }

  fn speak(self) {
    return "{self.name} says {self.sound}"
  }
}

class Dog : Animal {
  fn init(self, name) {
    super.init(name)
    self.sound = "woof"
  }
}

Concurrency

XS supports multiple concurrency models. Use whichever fits your problem.

concurrency.xs
-- spawn lightweight tasks
spawn { println("in background") }

-- async/await
async fn compute(x) {
  return x * 2
}
let r = await compute(21)  -- 42

-- channels
let ch = channel()
spawn {
  ch.send("ping")
  ch.send("pong")
}
println(ch.recv())  -- ping
println(ch.recv())  -- pong

-- nurseries (structured concurrency)
var results = []
nursery {
  spawn { results.push("a") }
  spawn { results.push("b") }
  spawn { results.push("c") }
}
-- all tasks complete before we get here

-- actors
actor Cache {
  var data = #{}

  fn set(key, val) { data[key] = val }
  fn get(key) { return data[key] }
}

let c = spawn Cache
c.set("x", 42)
println(c.get("x"))  -- 42

Interop

Inline C for performance-critical code. Use xs transpile to compile to C, JS, or WASM.

interop.xs
fn fast_hash(data) {
  inline c {
    uint64_t h = 0x525201;
    const char *s = xs_to_cstr(args[0]);
    while (*s) h = h * 31 + *s++;
    xs_return_int(h);
  }
  return 0  -- fallback for interpreter mode
}
xs transpile --target c    main.xs
xs transpile --target js   main.xs
xs transpile --target wasm main.xs

Tooling

xs run main.xs          -- run a file
xs build main.xs        -- compile to binary
xs test                 -- run tests
xs fmt                  -- format code
xs lint                 -- lint code
xs lsp                  -- start language server
xs debug main.xs        -- start debugger
xs --check main.xs      -- type check only
xs --strict main.xs     -- require all annotations