Type system

Gradual typing: unannotated code runs silently; annotations are checked statically before execution.

Summary

Primitive types include int/i64, float/f64, str, bool, re, and any. Composite types cover arrays ([int]), tuples ((int, str)), optionals (int?), and function types (fn(int) -> int). The checker catches assignment mismatches, wrong argument types, return type errors, and unknown type names. Running with --strict requires annotations everywhere; --lenient downgrades errors to warnings. Use _ as a placeholder where you want inference. Type aliases are declared with type UserId = i64.

Canonical

XS has gradual typing. Code runs fine without any annotations. Add them where you want enforcement: the type checker only kicks in on annotated code and passes through everything else silently.

Type Annotations

Annotations go after a colon on variables, after parameter names, after -> for return types, and after struct field names.

-- variables
let count: int = 42
var name: str = "XS"
const MAX: i64 = 100

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

-- struct fields
struct Config {
    host: str,
    port: int,
    debug: bool
}

-- const with annotation
const PI: f64 = 3.14159

Primitive Types

AnnotationDescription
int / i6464-bit signed integer (default integer type)
i8, i16, i32Smaller signed integers
u8, u16, u32, u64Unsigned integers
float / f6464-bit float (default float type)
f3232-bit float
str / stringString
boolBoolean
charCharacter
byteAlias for u8
reRegex
any / dynAny type (disables checking)
void / unitNo value
neverFunction that never returns

Integer annotations are interchangeable for checking purposes: int, i32, and i64 all accept integer literals. Same for float, f32, f64.

Composite Types

-- array of ints
let nums: [int] = [1, 2, 3]

-- tuple
let pair: (int, str) = (42, "hello")

-- optional (nullable)
let maybe: int? = null
let found: str? = "yes"

-- function type
let transform: fn(int) -> int = fn(x) { x * 2 }

-- generic types
let items: array<int> = [1, 2, 3]
let lookup: map<str, int> = #{"a": 1}

-- nested
let grid: [[int]] = [[1, 2], [3, 4]]
let handlers: [fn(str) -> bool] = []

What Gets Checked

The type checker runs as part of semantic analysis (before execution). It catches:

Variable assignment mismatches:

let x: int = "hello"
-- error[T0001]: type mismatch: expected 'int', got 'str'
--   hint: use int() or float() to convert a string to a number

Function argument types:

fn greet(name: str) { println("hi {name}") }
greet(42)
-- error[T0001]: type mismatch: expected 'str', got 'i64'
--   hint: use to_str() to convert a number to a string

Return type mismatches:

fn double(x: int) -> int {
    return "oops"
-- error[T0001]: type mismatch: expected 'int', got 'str'
}

Struct field types:

struct Point { x: int, y: int }
let p = Point { x: "bad", y: 0 }
-- error[T0001]: type mismatch: expected 'int', got 'str'

Match arm consistency:

let r = match x {
    1 => "one"
    2 => 42        -- error: match arm type mismatch
}

Unknown type names:

let x: Foo = 42
-- error[T0011]: unknown type 'Foo'
--   hint: check spelling or define a struct/enum named 'Foo'

User-defined types work too: if you define a struct or enum, it becomes a valid type name:

struct Point { x: int, y: int }
let p: Point = Point { x: 1, y: 2 }  -- valid

What Doesn't Get Checked

Unannotated code passes through silently. The checker infers types where it can (literals, operators, function calls with known signatures) but never forces you to annotate.

-- no annotations, no errors, runs fine
let x = 42
let y = x + 1
fn foo(a, b) { return a + b }

Type Checking Modes

xs script.xs            -- normal: type check annotated code, then run
xs --check script.xs    -- check only, don't execute
xs --strict script.xs   -- require annotations on all variables, params, and return types
xs --lenient script.xs  -- downgrade type errors to warnings

Strict mode enforces annotations everywhere:

-- with --strict, this is an error:
let x = 42
-- error[S0010]: missing type annotation for 'x' in strict mode
--   hint: use 'let x: <type> = ...'

-- fix:
let x: int = 42

In strict mode, every let/var/const, every function parameter, and every function return type must have an annotation.

Type Aliases

type UserId = i64
type Handler = fn(str) -> bool

Generic Type Parameters

Functions can declare type parameters with optional bounds:

fn identity<T>(x: T) -> T {
    return x
}

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

-- with trait bounds
fn display<T: Describe>(item: T) -> str {
    return item.describe()
}

Structs and enums can also have type parameters (parsed but currently erased at runtime: the syntax is accepted for forward compatibility):

struct Pair<A, B> { first: A, second: B }
enum Option<T> { Some(T), None }

Inferred Placeholder

Use _ to let the checker infer a type in a position where you'd normally write one:

let x: _ = 42    -- inferred as int