Skip to content

Language fundamentals

The src/basics/ directory contains 11 files, each isolating one language concept. This page summarizes what each file demonstrates.

Named dimensions and no implicit broadcasting

Section titled “Named dimensions and no implicit broadcasting”

Tensors carry named dimensions. Operations require explicit dimension alignment; the compiler rejects shape mismatches at type-check time rather than silently broadcasting.

let x: Tensor[Batch: 32, Features: 784] = ...
let w: Tensor[Features: 784, Hidden: 256] = ...
let y = x @ w // result is Tensor[Batch: 32, Hidden: 256]

Attempting to combine tensors along incompatible dimension names produces a compile-time error.

Algebraic data types pair with exhaustive match expressions. The compiler enforces that every variant is handled.

type Shape =
| Circle(radius: f64)
| Rect(w: f64, h: f64)
fn area(s: Shape) -> f64 =
match s
| Circle(r) => 3.14159 * r * r
| Rect(w, h) => w * h

Chelis supports qualified, selective, and glob imports:

import std.math // qualified: std.math.sqrt(x)
import std.math.{sqrt, sin, cos} // selective: sqrt(x)
import std.math.* // glob: everything into scope

Dim polymorphism with bracketed parameters

Section titled “Dim polymorphism with bracketed parameters”

Functions accept dimension parameters in brackets, enabling code that is generic over tensor shape:

fn normalize[D](x: Tensor[D]) -> Tensor[D] =
x / x.sum(dim=D)

The caller supplies the concrete dimension at the call site, or the compiler infers it.

Chelis never silently widens numeric types. Mixing f32 and f64 without an explicit cast is a type error:

let a: f32 = 1.0
let b: f64 = 2.0
// let c = a + b // ERROR: cannot add f32 and f64
let c = f64(a) + b // OK: explicit cast

Side effects are tracked in the type system. The Random effect requires an algebraic handler that supplies the implementation:

fn sample() -> f64 with Random =
random.uniform(0.0, 1.0)
let result = with seed(42) { sample() }

The handler (with seed(...)) provides deterministic randomness, making effectful code reproducible and testable.

Linearity: consume-by-default, explicit copy, borrow

Section titled “Linearity: consume-by-default, explicit copy, borrow”

Values are consumed on use. To use a value more than once, explicitly copy it or pass a borrow:

let x = Tensor.ones[N: 128]
let y = x + x // ERROR: x consumed on first use
let y = copy(x) + x // OK: copy then consume
fn peek(t: &Tensor[N]) -> f64 = t.sum() // borrow: no consumption

Linear types prevent accidental aliasing of mutable tensors and enable the compiler to reuse memory safely.

Automatic differentiation is a first-class transform. grad computes reverse-mode derivatives:

fn loss(w: Tensor[D]) -> f64 =
(w * w).sum()
let dw = grad(loss, wrt=w)

The wrt parameter names which argument to differentiate with respect to. Higher-order derivatives compose naturally.

vmap lifts a function written for a single example into one that operates over a batch:

fn predict(x: Tensor[Features: 784]) -> Tensor[Classes: 10] = ...
let batch_predict = vmap(predict, axis=Batch)
// batch_predict: Tensor[Batch, Features: 784] -> Tensor[Batch, Classes: 10]

No manual batch-dimension threading is required.

jit marks a function for trace-based compilation. Computation is deferred until realize forces evaluation:

let fast_predict = jit(predict)
let lazy_result = fast_predict(input) // traces, does not execute
let concrete = realize(lazy_result) // executes the traced graph

This separation allows the compiler to fuse operations and optimize the computation graph before execution.

Macros run at compile time and produce Surf AST nodes:

macro repeat_layer(n: int, layer: Expr) =
for i in 0..n:
emit layer
let stack = repeat_layer(6, TransformerBlock(hidden=512))

Macros have access to type information and can generate code based on compile-time constants.