MIT licensedEarly prototypeOne Go binary, no dependencies

A language where tensors are the primitive.

twill is a small language where grad is built in and a shape mistake is an error you see before the program runs.

Most machine-learning code is a language plus a numeric framework bolted on top. twill goes the other way: differentiation is a language operation rather than a library call, a static checker reads your shapes before anything executes, and the compiler is written in the language it compiles.

$ go install github.com/twill-lang/twill/cmd/twill@latest

01Before anything runs

The most useful thing twill does is refuse to start

Change a dimension in the signature and watch the checker change its mind. The messages below are what the 1.19.0 binary prints for each of these programs, character for character, and the arithmetic deciding between them is the same one rule the checker uses: the inner dimensions of a matrix multiply have to agree.

matvec.twclick a dimension
fn matvec(A: [3, ], x: []) -> [3] {
  A @ x
}
twill check
$ twill check matvec.tw
matvec.tw:2: shape error: shape mismatch in @: [3, 2] @ [3] (inner 2 != 3)
  2 |   A @ x
$ echo $?
1

A dimension can be a literal, or a name. A name used more than once must be the same size, which is what lets the checker verify the return type of fn mm(A: [n, k], B: [k, m]) -> [n, m]. The checker only flags a mismatch when it is certain: code whose shapes depend on runtime values is left alone rather than guessed at, so a clean run means what it says. And a clean run exits 0, so twill check is a CI gate as it stands.

02The whole of it

Price a European call, then differentiate the pricer for its Greeks

No bumping, no second library. grad went through 200,000 simulated paths, a relu payoff and a mean, and landed on the closed-form Greeks.

examples/montecarlo_option.tw
# montecarlo_option.tw: price a European call by Monte Carlo, and get its
# Greeks (delta, vega) by differentiating the pricer. No finite differences,
# no extra libraries. `grad` does it.
#
# This is the finance beachhead: the payoff is parallel and simple, the result
# is reproducible (seeded RNG), and the sensitivities come straight from autodiff,
# something Python needs JAX or bumping-and-revaluing to match.

seed(42)

# Fix the standard-normal shocks once, so the price is a smooth, differentiable
# function of the inputs (common random numbers) and fully reproducible.
let paths = 200000
let Z = randn(paths)

# Terminal-value pricer for a call under geometric Brownian motion.
fn call_price(S0, K, r, sigma, T) {
  let drift = (r - 0.5 * sigma * sigma) * T
  let ST = S0 * exp(drift + sigma * sqrt(T) * Z)   # simulated terminal prices
  let payoff = relu(ST - K)                        # max(ST - K, 0)
  exp(-r * T) * mean(payoff)                       # discounted expected payoff
}

let S0 = 100.0
let K = 100.0
let r = 0.05
let sigma = 0.2
let T = 1.0

let price = call_price(S0, K, r, sigma, T)

# Greeks by autodiff: delta = dPrice/dS0, vega = dPrice/dSigma.
let delta = grad(fn(s) = call_price(s, K, r, sigma, T))(S0)
let vega = grad(fn(v) = call_price(S0, K, r, v, T))(sigma)

print("European call, S0=100 K=100 r=5% vol=20% T=1y, MC paths:", paths)
print("  price =", price, " (Black-Scholes 10.4506)")
print("  delta =", delta, " (Black-Scholes 0.6368)")
print("  vega  =", vega, "  (Black-Scholes 37.524)")
output
$ twill run examples/montecarlo_option.tw
European call, S0=100 K=100 r=5% vol=20% T=1y, MC paths: 200000
  price = 10.442696  (Black-Scholes 10.4506)
  delta = 0.636269  (Black-Scholes 0.6368)
  vega  = 37.488476   (Black-Scholes 37.524)

03Why

Three things fall out of building the language around it

01

Tensors are the primitive

Every number is a rank-0 tensor, vectors and matrices are literals, and @ is matrix multiply. Broadcasting follows NumPy rules, and the gradients broadcast back correctly.

02

grad is a builtin

Backed by a real reverse-mode engine that follows the structure of its argument. A model held in a list gets a list of gradients back; a model in a record gets a record. No tape, no requires_grad, no .backward().

03

Shapes and units are static

[2,3] @ [4] is an error you see before the program runs, not a stack trace forty minutes into training. Dollars plus shares is refused the same way, and units cost nothing at runtime.

04Units of measure

Price times quantity is money; dollars plus shares is refused

Declare base units, annotate quantities, and the checker tracks units through arithmetic. Units are erased at runtime and cost nothing.

notional.tw
unit USD
unit share

fn notional(px: USD/share, qty: share) -> USD { px * qty }

let price: USD/share = 150.0
let qty: share = 200.0
let value = notional(price, qty)   # USD
let bad = price + qty              # USD/share plus share
twill check
$ twill check notional.tw
notional.tw:9: shape error: unit mismatch: USD*share^-1 + share
  9 | let bad = price + qty              # USD/share plus share

05Self-hosting

twill is written in twill, and the claim is checked rather than asserted

The lexer, parser, checker, evaluator, tensor kernels, formatter and CLI are written in the language itself. What makes that a claim rather than a boast is the fourth line of this chain: the same corpus goes through both implementations and the bytes are compared.

01

The compiler, in twill

src/*.tw: lexer, parser, checker, formatter, evaluator, tensor kernels and CLI, all written in the language, all in the systems half of it.

Every file in src/, std/ and examples/ draws no diagnostic, and a test in the repository walks them to say so

02

Run by the Go bootstrap

The Go implementation plays the part OCaml played for Rust: it is the compiler that runs the compiler. It is interpreted and it is slow, and neither matters for the check being made.

Since v1.4.0

03

Compared against the reference, byte for byte

The same corpus goes through both implementations and the output is diffed. Not a sample of it, and not a summary: the bytes.

check is held to every .tw file under testdata/, 363 of them today; fmt to the 472 files its own corpus test globs. Both counted out of the repository when this page was built

04

A binary with no Go in it

The triple build: the compiler compiles itself, and the artifact its own output produces is compared with the one produced by that output. The fixed point is the proof, and it is the stage after this one.

Not done

06What shipped

Three walls the ecosystem kept hitting

Every entry here came from docs/roadmap.md, which ranks what the language is missing by how many of the independently written codebases hit each wall on their own. The list below is read out of the changelog when this page is built, so it is the releases the language actually has rather than the ones somebody remembered to type in.

  1. v1.19.0

    3 October 2026

    • Lazy iteration, for over an iterator
    • Named and optional arguments on user functions
    • record() and without_field, and the frame operations they unlock
  2. v1.18.5

    23 September 2026

    • v1.18.4

      22 September 2026

      Av1.19.0

      sort orders more than strings, and takes a comparison

      sort ordered strings and nothing else, so five codebases wrote their own. docs/roadmap.md counts eleven hand-written sorts and names every one: four in spool, three in twill itself, two in bobbin, one in loom and one in weft; and the same function, sort_strings, is written out under the same name in two files of the twill repository. skein needed the comparison most, because it sorts an index array by comparing through a second array the closure captures, and that was only expressible once function values landed in 1.7. Every form is stable, which here is correctness rather than a nicety: skein assigns token ids from a sorted vocabulary, so an unstable sort would make the ids a function of the sort's internals rather than of the corpus.

      sort_demo.tw
      mode systems
      
      struct Row { name: Str, n: I64 }
      
      fn main() {
        let xs: Arr[I64] = [3, 1, 2]
        print(sort(xs))                        # ascending, by the elements' own order
        print(sort(xs, true))                  # descending
      
        let rows: Arr[Row] = [
          Row { name: "c", n: 3 },
          Row { name: "a", n: 1 },
          Row { name: "b", n: 2 },
        ]
        for r in sort(rows, fn(a, b) = a.n < b.n) {   # by a comparison: does a come first?
          print(r.name, r.n)
        }
      }
      twill run
      $ twill run sort_demo.tw
      [1, 2, 3]
      [3, 2, 1]
      a 1
      b 2
      c 3

      Bv1.8.0

      A program can start another program, and never through a shell

      run(program, argv, dir) -> Res[Str, Str]. spool needed it to fetch a package at all: the package manager for a self-hosting language could not clone a repository. The program and its arguments stay separate values all the way to execve, so an argument reaches the program as text rather than as something a shell gets to read, which matters when the arguments are tags and URLs out of a manifest a stranger wrote. Ok carries stdout and only on an exit status of 0; stderr is the Err message and is never spliced into Ok. The blank line in the output below is git's own trailing newline, which arrives in stdout whole rather than being trimmed on the way. TWILL_NO_EXEC set to anything non-empty makes every call answer Err without starting anything, and an Err rather than an abort, so a program degrades to what it can still do.

      head.tw
      mode systems
      
      fn main() {
        let argv: Arr[Str] = ["rev-parse", "--abbrev-ref", "HEAD"]
        match run("git", argv, ".") {          # no shell: argv stays a vector
          Ok(branch) => print("checkout is on", branch),
          Err(e) => print("git said:", e),
        }
      }
      twill run
      $ twill run head.tw
      checkout is on main
      
      $ TWILL_NO_EXEC=1 twill run head.tw
      git said: run: refused to start "git" because TWILL_NO_EXEC is set

      Cv1.19.0

      A function of many settings takes only the ones that differ

      Named and optional arguments, the open half of the roadmap's entry 29. A parameter can carry a default and a call can name its arguments, so a function of a dozen settings is no longer twelve positions at every call site: the defaults fill what is not given, and a name picks out the one setting that differs without counting commas to reach it. Named arguments follow the positional ones and may come in any order. Builtins stay positional, since their arities are nameless word lists with no parameter to bind a name to, so a named argument to one is a checker error rather than a silently dropped word. The ordinary positional call is unchanged, down to its arity message, and both the Go bootstrap and the compiler written in twill bind the same way, which a conformance case checks.

      axes.tw
      mode systems
      
      # A parameter can carry a default, and a call can name its arguments, so a
      # function of many settings is called with only the ones that differ, in any
      # order. Builtins stay positional; this is for the functions you write.
      fn axis(label: Str, lo: I64 = 0, hi: I64 = 100, step: I64 = 10) {
        write_out(label)
        write_out(": ")
        write_out(str(lo))
        write_out("..")
        write_out(str(hi))
        write_out(" by ")
        write_out(str(step))
        write_out("\n")
      }
      
      fn main() {
        axis("x")                 # every optional takes its default
        axis("y", hi: 50)         # name just the one that differs
        axis("t", step: 5, lo: 1) # named, in any order
      }
      twill run
      $ twill run axes.tw
      x: 0..100 by 10
      y: 0..50 by 10
      t: 1..100 by 5

      07The language

      What a pattern is, and what can be generic

      Two things the language was missing from the middle, and both are checked by the Go bootstrap and by the compiler written in twill, which is the check this project exists to be able to make.

      expr.tw
      mode systems
      
      enum Expr { Num(F64), Neg(Expr), Mul(Pair[Expr, Expr]) }
      
      struct Pair[A, B] { left: A, right: B }      # a declaration of your own, generic
      
      fn describe(e: Expr) -> Str {
        match e {
          Num(v) if v < 0.0 => "a negative constant",   # a guard
          Num(0.0) => "zero",                           # a literal pattern
          Neg(Neg(inner)) => describe(inner),           # a nested pattern
          Mul(p) => "a product",
          other => "something else",                    # a catch-all with a name
        }
      }
      
      fn depth(e: Expr) -> I64 {
        match e {                                       # no catch-all, and two cases short
          Num(v) => 0,
        }
      }
      
      let mixed: Pair[F64, F64] = Pair { left: 1.0, right: "two" }
      twill check
      $ twill check expr.tw
      expr.tw:18: shape error: match on Expr is not exhaustive: missing Neg, Mul
        18 |   match e {                                       # no catch-all, and two cases short
      expr.tw:23: shape error: "mixed" is declared Pair[F64, F64] but the value is Pair[F64, Str]
        23 | let mixed: Pair[F64, F64] = Pair { left: 1.0, right: "two" }
      01

      A pattern is a tree

      Ok(Some(v)) takes a value apart in one place instead of a second match inside the arm; 3, "hi" and true match by ordinary equality, so a match over numbers needs no enum written around it; and Some(n) if n > 0 says the thing a shape cannot. A lower-case name binds rather than naming a case, so a catch-all can say what it caught.

      02

      Exhaustiveness recurses

      Some(Ok(v)), Some(Err(e)) and None cover an Opt[Res[..]], and dropping one names the value that gets through rather than passing. An arm counts only when nothing but the value's shape decides whether it runs, so a guarded arm and a narrower nested one prove nothing, and Some(v) if v > 3 together with None is reported as incomplete.

      03

      Anything can be generic

      struct Box[T], enum Tree[T] and fn first[T](xs: Arr[T]) -> T parse, check and run alongside the built-in Arr, Dict, Opt and Res. A Box[I64]'s field is an I64 rather than an unknown, substitution goes under the constructors a parameter is written inside, and a Box[Str] is refused where a Box[I64] was declared.

      04

      And no monomorphization, which turned out not to be needed

      The original plan assumed it. The runtime is a tree walker over dynamically typed values, so the same code runs whatever T is and specialising per instantiation would produce identical copies. The parameters have to reach exactly one place, the types the checker judges against, so generics here are substitution in about eighty lines per implementation.

      And the tooling around it

      twill lsp
      A language server: diagnostics republished as you type, formatting, and hover reporting the inferred type and shape. Hover is the one worth having, because in a tensor-first language the question you actually have is what shape something is, and it is answered from the checker without running anything.
      std/gradcheck
      A gradient checker. There is nothing about a wrong gradient that looks wrong: the model does not crash, it trains to a worse loss, and the search starts at the learning rate. Compares against a central difference quotient, deliberately not built out of grad.
      twill doctor
      Answers the question a bug report starts with, and finds what is wrong quietly: a stale binary earlier on PATH, a TWILL_STD pointing at last month's checkout, a standard library that will not load.
      :type and :shape
      Answer from the checker in the REPL without running anything. :shape randn(4096, 4096) @ w costs nothing here and a gigabyte there.
      The filesystem, finished
      path_exists, mkdir_all, remove_all, rename, mtime, temp_dir, cwd and the seven path operations. Plus read_file_at, a ranged read: read_file returns the whole file, so a reader following a growing log read all of it again on every poll.
      mono_ns()
      A clock that only goes forward. The wall clock steps when the system time is corrected, so a duration measured across one is wrong by the correction, which for a benchmark is the difference between a number and a fiction.

      08The ecosystem

      Ten repositories, nine of them written in twill

      Everything downstream of the compiler is written in twill itself, which is the same experiment run again: a real program against the subset, with its own list of what is missing. Those lists are what the roadmap ranks.

      09What is not done yet

      This is a prototype, and some of it is deliberately left for later

      It is interpreted. Tensor ops loop in Go, and there is no vectorized or GPU backend.
      Autodiff is reverse-mode and first-order. A gradient inside a gradient is refused wherever it is written, rather than answered with zeros; hessian and jacobian nest legitimately.
      The shape checker is best-effort, not a full type system. It flags a mismatch only where one is certain: code whose shapes depend on runtime values is left alone rather than guessed at.
      The self-hosted compiler runs on the Go bootstrap. The Go-free binary, and the triple build that would prove it, are the stage after this one.

      10Read on

      Documentation