Skip to content

Macros

Omega has a hygienic macro system. Macros are first-class values of type MacroTransformer (tag: "MacroTransformer"); they're registered against names in a macro environment and applied during the expansion pass before evaluation.

The pipeline

source text

  ▼ tokenize → parse → Datum

  ▼ toSyntax            (attach lexical info)

  ▼ macro/expander      (recursively expand registered macros, preserve hygiene)

  ▼ Expr                (the AST evaluated by the CESK machine)

Files: core/reader/tokenize.ts, core/reader/parse.ts, core/reader/toSyntax.ts, core/macro/expander.ts. The pipeline/compileText.ts chains them.

Defining a macro

The simplest path is register-macro:

lisp
(register-macro 'when
  (lambda (form)
    (let ((cond (cadr form))
          (body (cddr form)))
      `(if ,cond (begin ,@body) nil))))

(when (> x 0)
  (print "positive")
  (* x x))
;; expands to: (if (> x 0) (begin (print "positive") (* x x)) nil)

register-macro takes a name and a transformer procedure. The procedure receives the full call form as a list and returns the rewritten form.

make-transformer

For more structured rewrites, make-transformer creates a MacroTransformerVal directly:

ts
{ tag: "MacroTransformer", proc: <Val>, name: <string> }

The value can be passed around like any other procedure but is dispatched by the expander instead of by the evaluator.

Hygiene

The expander tracks lexical info on Syntax objects (core/syntax/syntax.ts). When a macro introduces an identifier, the identifier is "marked" so it doesn't accidentally collide with a binding at the use site. Conversely, identifiers from the use site keep their original marks so they refer to the user's bindings, not the macro's.

core/macro/hygiene.ts implements the marking and renaming. The two operations exposed are:

OperationPurpose
freshMark()a unique mark for one expansion
bindIntroduced(stx, mark)tag identifiers introduced by this expansion

This is the same model as syntax-rules / syntax-case, with marks instead of explicit syntax objects in user code.

Quasiquote inside macros

Macros heavily use quasiquote. \`` is quasiquote, ,isunquote, ,@isunquote-splicing`.

lisp
`(if ,cond (begin ,@body) nil)
;; with cond = (> x 0), body = ((print "yes") (do-thing))
;;   reads as: (if (> x 0) (begin (print "yes") (do-thing)) nil)

Semantic macros

core/macro/semantic.ts is for macros that need evaluator state — for example, a macro that compiles down to a different evaluator's primitives. These are rarer; they're how make-evaluator and language-building primitives interact with the expander.

Expansion order

Macros are expanded outermost first. A nested macro call inside the expansion of an outer macro is expanded after the outer one rewrites. This matches Scheme's semantics.

See also

Source: projects/omega-lisp/src/core/macro/expander.ts (the expander), hygiene.ts (mark machinery), semantic.ts (semantic transformers), types.ts (type definitions), projects/omega-lisp/src/core/eval/values.ts:788-792 (MacroTransformerVal).