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!

Functions

Functions are defined with defn. Parameter types and return type are always explicit:

(defn add ((x : i32) (y : i32)) : i32
(+ x y))

Functions without a meaningful return value use Unit:

(defn greet ((name : String)) : Unit
(println (str "Hello, " name "!")))

Function bodies implicitly sequence multiple expressions. The last expression is the return value:

(defn process ((x : i64)) : i64
(println (str "Processing: " x))
(let ((doubled (* x 2)))
(println (str "Doubled: " doubled))
doubled)) ;; return value

Callers can optionally use keyword syntax to name arguments. The definition is always positional:

(defn spawn-enemy ((pos : Vec2) (health : i32) (state : EnemyState)) : Enemy
...)
;; All valid call styles:
(spawn-enemy (Vec2 0.0 0.0) 50 Idle) ;; positional
(spawn-enemy :pos (Vec2 0.0 0.0) :health 50 :state Idle) ;; named
(spawn-enemy :health 50 :pos (Vec2 0.0 0.0) :state Idle) ;; named, any order
(spawn-enemy (Vec2 0.0 0.0) :health 50 :state Idle) ;; mixed

The compiler verifies keyword names match parameter names at compile time.

Lambda syntax mirrors defn without the name. Type annotations are optional (usually inferred):

;; Type-inferred (common)
(fn (x) (+ x 1))
;; Type-annotated
(fn ((x : i32)) : i32 (+ x 1))
;; Multi-arg
(fn ((x : i32) (y : i32)) (+ x y))
;; Multi-expression body
(fn (e)
(println (.name e))
(.health e))

Closures capture by reference — the closure shares the variable with its enclosing scope:

(let ((mut x 5))
(let ((f (fn () x)))
(set! x 10)
(f))) ;; => 10

Functions are first-class values with types:

(defn make-adder ((n : i64)) : (Fn [i64] i64)
(fn (x) (+ x n)))
(defn apply-twice ((f : (Fn [i64] i64)) (x : i64)) : i64
(f (f x)))
(defn main ()
(let ((add5 (make-adder 5)))
(println (apply-twice add5 10)))) ;; => 20

Functions can call themselves recursively:

(defn factorial ((n : i64)) : i64
(if (<= n 1)
1
(* n (factorial (- n 1)))))
(defn fib ((n : i64)) : i64
(if (<= n 1)
n
(+ (fib (- n 1)) (fib (- n 2)))))

Self-recursive calls in tail position are automatically optimized into loops. This means deep recursion doesn’t overflow the stack:

(defn count-down ((n : i64)) : i64
(if (= n 0) 0 (count-down (- n 1))))
(defn main ()
(println (count-down 1000000))) ;; works — no stack overflow

Tail positions are:

  • The body of a defn
  • The then/else branches of an if in tail position
  • The last expression in a let body in tail position
  • Branch results of cond/match in tail position

Weir is immutable by default. Use mut to make a binding reassignable:

;; Immutable (default)
(let ((x 5))
(+ x 1)) ;; x cannot be reassigned
;; Mutable — explicit opt-in
(let ((mut x 5))
(set! x (+ x 1))
x) ;; => 6

There are no mutable references — functions cannot modify the caller’s data. Use functional struct update to create modified copies:

(defn damage ((e : Enemy) (amount : i32)) : Enemy
(update e :health (- (.health e) amount)))

For complex signatures, use declare separately from the definition:

(declare transform
(=> (Functor 'f)
(Fn [(Fn ['a] 'b) ('f 'a)] ('f 'b))))
(defn transform (func container)
(map func container))

When both inline annotations and a declare are present, they must agree.

Functions are private by default. Use pub to make them accessible from other modules:

(pub defn spawn-enemy ((pos : Vec2) (health : i32)) : Enemy
...)