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!

Macros

Weir’s macros are purely syntactic transformations — they rewrite S-expressions into other S-expressions without any knowledge of types. They are hygienic by default, preventing accidental variable capture.

Macros operate at a fixed point in the compilation pipeline:

parse → macro expand → type check → compile

No circular dependencies between expansion and type checking. This keeps the cascade model clean — re-expansion feeds into re-typechecking, which feeds into recompilation.

  • Transform and generate arbitrary S-expression code
  • Generate type definitions (deftype, defstruct)
  • Generate typeclass instances (derive-style)
  • Generate function definitions
  • Pattern-based or quasiquote-based code generation
(defmacro defsystem (name component-type &body body)
`(defn ,name ((entities : (List ,component-type))) : Unit
(for-each entities (fn (e) ,@body))))
;; Expands to a real function definition
(defsystem update-enemies Enemy
(set! (.pos e) (move (.pos e))))
  • Query the type of an expression during expansion
  • Adapt output based on types of arguments
  • Perform side effects during expansion (discouraged)

These use cases are better served by the type system — typeclasses, associated types, and generics.

Macro-generated code cannot accidentally shadow or capture variables from the call site. This is especially important in a live-reloading context where code is being swapped in and out — accidental capture across reload boundaries would produce the worst kind of intermittent bugs.

;; The macro's internal variable 'tmp' won't conflict with
;; a 'tmp' at the call site
(defmacro swap! (a b)
`(let ((tmp ,a))
(set! ,a ,b)
(set! ,b tmp)))

Explicit escape hatches for intentional capture are available when needed.

Macros can emit type definitions, which is essential for first-class macro citizenship:

;; A macro that generates a struct and its constructor
(defmacro defcomponent (name &rest fields)
`(defstruct ,name ,@fields))
;; A derive-style macro generating a typeclass instance
(defmacro derive-show (type-name field-names)
`(instance (Show ,type-name)
(defn show (x)
(str ,(str type-name "(")
,@(interpose ", "
(map (fn (f) `(str ,(str f "=") (show (. x ,f))))
field-names))
")"))))

The dependency tracker distinguishes between user-defined code and macro-generated code:

ChangeEffect
Edit a macro call siteRe-expand that call, cascade if output changed
Edit a macro definitionRe-expand all call sites of that macro
Macro output unchangedShort-circuit — skip downstream cascade

Most macro redefinitions don’t change most expansions. The dependency tracker compares expansion outputs structurally and skips the downstream cascade when output hasn’t changed. This is critical for performance.

When macro-generated code has a type error:

  1. Primary location: the macro call site (“error in expansion of defsystem at game.weir:15”)
  2. Secondary location: the specific point in the expansion that failed
  3. Expansion visibility: inspect expanded code with weir expand

During live reloading, the old working version keeps running while errors are reported.

Use the expand command to see what macros produce:

Terminal window
weir expand myfile.weir

This prints the source after all macros have been expanded — useful for debugging macro output.

Weir deliberately keeps macros simple (purely syntactic, no type awareness) and invests in typeclass expressiveness instead. A more expressive type system reduces the pressure on the macro system:

PatternWithout TypeclassesWith Typeclasses
Polymorphic printingMacro per type(Show 'a) instance
Generic mapMacro per container(Functor 'f) instance
Auto-derived equalityCode-walking macro(Eq 'a) derive macro (simpler)

The result is a macro system that’s easier to reason about, with fewer edge cases and better error messages.