Language tour

Surf first, Deep when tools need the tree

Chelis has two syntaxes for one language. Surf is what people read and write: functions, named dimensions, pipes, algebraic data types, and typed tensor programs. Deep is the canonical representation that tools can inspect after Surf has done its job.

01 Functions are the everyday unit

Surf is the human-facing syntax. A function declares the tensor shape it returns, so callers and compiler passes agree on the shape contract.

Surf Complete source: chelis/examples/hello_tensor.ch
module HelloTensor
def main() -> tensor[n, f32] = {
  a = to_tensor([1.0, 2.0, 3.0])
  b = to_tensor([4.0, 5.0, 6.0])
  out = add(a, b)
  out
}

02 Named dimensions travel through transforms

Dimensions are not comments. process works on a feature vector, and batch_process lifts it over the batch axis while preserving the feature axis in the type.

Surf Complete source: chelis/examples/vmap_relu.ch
def process(x: tensor[features, f32]) -> tensor[features, f32] = relu(x)
def batch_process(xs: tensor[batch, features, f32]) -> tensor[batch, features, f32] = xs |> vmap(process, axis=0)

03 Pipes keep tensor flow readable

The pipe operator passes the value on the left into each stage. That makes small activation chains and larger model blocks read in execution order.

Surf Source slice: basics/pipeandmatch.ch
def pipeline(x: &tensor[n, f32]) -> tensor[n, f32] = x |> relu |> sigmoid
Surf Source slice: capstone/transformerblock.ch
attn_out = probs |> matmul(v) |> matmul(wo)
resid1 = add(x, attn_out)
ff_out = matmul(matmul(resid1, ff1), ff2)
add(resid1, ff_out)

04 ADTs and match make cases explicit

Surf supports algebraic data types and pattern matching. The compiler sees both variants of Activation, and match arms make the control flow visible in the source.

Surf Source slice: basics/pipeandmatch.ch
type Activation =
  | Relu
  | Sigmoid
def activate(act: Activation, x: &tensor[n, f32]) -> tensor[n, f32] = {
  match act with {
    | Relu => relu(x)
    | Sigmoid => sigmoid(x)
  }
}

05 The type system rejects shape drift

Tensor widths, axes, and scalar element types are part of signatures and intermediate expressions. A wrong matmul width or bias shape is a type error instead of a backend crash.

Surf Source slice: capstone/linreg.ch
def predict(x: tensor[64, 64, f32], w: tensor[64, 1, f32], b: tensor[1, f32]) -> tensor[64, 1, f32] = x |> matmul(w) |> add(expand(b, 0, 64))
Surf Source slice: capstone/transformerblock.ch
def block(x: tensor[seq, 256, f32], wq: tensor[256, 64, f32], wk: tensor[256, 64, f32], wv: tensor[256, 64, f32], wo: tensor[64, 256, f32], ff1: tensor[256, 1024, f32], ff2: tensor[1024, 256, f32]) -> tensor[seq, 256, f32] = {
  q = matmul(x, wq)
  k = matmul(x, wk)
  v = matmul(x, wv)
  scores = matmul(q, permute(k, 1, 0))
  probs = softmax(scores, 1)
  attn_out = probs |> matmul(v) |> matmul(wo)
  resid1 = add(x, attn_out)
  ff_out = matmul(matmul(resid1, ff1), ff2)
  add(resid1, ff_out)
}

06 Deep is the canonical machine form

After Surf is parsed, Chelis can lower to Deep: a stable s-expression tree for compiler tooling, shell integration, tests, and round-tripping back to Surf.

Surf Complete source: chelis-surf/tests/fixtures/pipe.ch
def process(x: tensor[features, f32]): tensor[features, f32] =
  x |> normalize |> relu |> softmax
Deep Complete source: chelis-deep/tests/fixtures/pipeline.dp
(def {}
  process
  (fn {}
    (params {} x)
    (pipe {} (var {} x) (var {} normalize) (var {} relu) (var {} softmax))))

07 Automatic differentiation is a language primitive

grad takes a scalar-valued function and the parameter to differentiate with respect to. The result is the adjoint, computed by reverse-mode AD through the tensor graph.

Surf Source slice: basics/gradbasic.ch
def loss(w: tensor[f32], x: tensor[f32], y: tensor[f32]) -> tensor[f32] =
  sub(mul(w, x), y) |> fn (e) -> mul(e, e)

def dloss_dw(w: tensor[f32], x: tensor[f32], y: tensor[f32]) -> tensor[f32] =
  grad(loss, wrt=w)(w, x, y)

08 Effects track what touches the outside world

The type system surfaces randomness, I/O, and device placement as effects. A function that uses dropout carries Random in its type; a handler like with seed(...) makes the randomness deterministic.

Surf Source slice: basics/effectsrandom.ch
def train_step(x: tensor[batch, features, f32]) -> tensor[batch, features, f32] ! { Random } =
  dropout(x, 0.5)

def deterministic_step(x: tensor[batch, features, f32]) -> tensor[batch, features, f32] =
  with seed(42) { dropout(x, 0.5) }

09 Linear ownership prevents accidental aliasing

Tensors are consumed by default. Passing the same value to two call sites requires explicit copy(x) or read-only &borrow. The compiler rejects ambiguous ownership at check time.

Surf Source slice: basics/linearity.ch
def use_twice(x: tensor[n, f32]) -> tensor[n, f32] =
  add(copy(x), copy(x))

def read_only(x: &tensor[n, f32]) -> tensor[n, f32] =
  relu(x)

These snippets are static page examples. For larger listings, use the examples page; for the full reference, use the docs.