Skip to content

Hello! 👋 My name is Nathan Weir. This is a fun personal project for using AI to build a bespoke, domain-specific programming language. It is not a serious, professional project. This site and the language itself are largely generated via Claude Code. If you find yourself programming with Weir, have fun - but use at your own risk!

Control Flow

Weir provides several control flow forms. All are expressions — they return values. Looping is done via recursion (with tail-call optimization).

if takes a condition and two branches. When the result is used, the else branch is required:

;; As expression — else required
(let ((status (if alive? :active :dead)))
(println status))
;; As statement (result unused) — else optional, implicit Unit
(if (key-pressed? :escape)
(open-menu))

Both branches must have the same type when the result is used:

;; OK — both branches return String
(if (> x 0) "positive" "non-positive")
;; Compile error — type mismatch
(if (> x 0) "positive" 42)

cond is a multi-way if. The else branch is required — an unhandled case is a silent bug:

(cond
((< health 0) (die))
((< health 20) (flee))
((< health 50) (defend))
(else (attack)))

Conditions are tested in order. The first truthy condition’s body is evaluated and returned. All branches must return the same type when the result is used.

match dispatches on the structure of a value. See Pattern Matching for full details.

(match enemy-state
((Patrol start end) (move-between start end))
((Chase target-id) (pursue target-id))
(Idle (stand-still))
(Dead (remove-entity)))

Pattern matching is exhaustive by default — missing a variant is a compile error. Use _ as a wildcard to handle remaining cases.

when / unless — Side-effecting Conditionals

Section titled “when / unless — Side-effecting Conditionals”

Single-branch forms that always return Unit. No else branch — they make intent clear: “I only care about this one case.”

(when (key-pressed? :escape)
(open-menu)
(pause-game))
(unless (alive? entity)
(remove entity))

when executes its body when the condition is true. unless executes when the condition is false. Both support multiple expressions in the body.

Function bodies, let bodies, and lambda bodies implicitly sequence multiple expressions. The do block provides explicit sequencing where only a single expression is expected:

(if condition
(do (log "taking branch A")
(branch-a))
(branch-b))

The last expression in a do block is its return value.

Weir has no for or while loops. Iteration is expressed via recursion. Self-recursive calls in tail position are optimized into loops by the compiler, so there’s no stack overflow risk:

;; Count down from n — compiled as a loop
(defn count-down ((n : i64)) : i64
(if (= n 0) 0 (count-down (- n 1))))
;; Sum a list — compiled as a loop
(defn sum-list ((xs : (Vector i64)) (i : i64) (acc : i64)) : i64
(if (= i (len xs))
acc
(sum-list xs (+ i 1) (+ acc (nth xs i)))))

See Functions — Tail-Call Optimization for details on which positions qualify for optimization.

FormBranchesReturnsUse When
if2 (else optional as statement)Expression valueBinary choice
condN + required elseExpression valueMulti-way choice
matchN (exhaustive)Expression valueDispatching on data structure
when1UnitSide effect on true
unless1UnitSide effect on false
doN (sequenced)Last expressionExplicit multi-expression block