Anchor is a systems programming language with Lisp syntax that compiles to C.
Every value is an AnchorVal — a raw 64-bit quantity. Pointers and integers are
stored directly with no boxing or tagging visible to user code. Memory is managed
through arenas — bump-pointer regions that free all at once when the function returns.
There is no GC.
The macro system is hygienic syntax-rules plus macro-case, which runs arbitrary
Chez Scheme at expand time. This lets macros compute sizes, unroll loops, generate
families of functions, and define other macros — all before a line of C is emitted.
Requires Chez Scheme.
chez --script build.ss # → ./anchorc (standalone binary, no Chez needed to run)The Chez Scheme Windows installer puts scheme.exe and csv1030.dll in
C:\Program Files\Chez Scheme 10.3.0\bin\a6nt\. Both that directory and the
directory where you install anchorc.exe must be on your PATH.
# add once (run as Administrator, then restart your terminal)
[System.Environment]::SetEnvironmentVariable(
"PATH",
"C:\Program Files\Chez Scheme 10.3.0\bin\a6nt;" +
[System.Environment]::GetEnvironmentVariable("PATH","Machine"),
"Machine"
)
scheme --script build.ss # note: 'scheme', not 'chez' → anchorc.exeFor programs that link against external C libraries, install
MSYS2 and add C:\msys64\mingw64\bin to your PATH
as well. Pass include/lib paths via --cflags:
anchorc main.anc -o main --cflags "-IC:/msys64/mingw64/include -LC:/msys64/mingw64/lib -lsomelib"Stack size. Windows defaults to a 1 MB thread stack. Each with-arena scope
allocates a 1 MB buffer on the stack, so programs with more than one nested
arena scope will overflow immediately. Add -Wl,--stack,16777216 to --cflags
to raise the limit to 16 MB:
anchorc main.anc -o main --cflags "-Wl,--stack,16777216"Run a file:
./anchorc examples/hello.anc --run # compile + run
./anchorc examples/hello.anc -o hello # compile to binary
./anchorc examples/hello.anc -o hello.c # emit C only
./anchorc examples/hello.anc --emit-exp # print macro-expanded AST
./anchorc examples/hello.anc --multi-threaded -o hello # thread-safe arena pointer (uses _Thread_local)(include <stdio.h>)
(ffi printf (const char* ...) -> int)
(fn main ()
(printf "Hello, Anchor!\n"))
(include <stdio.h>)
(ffi printf (const char* ...) -> int)
; each: hygienic list iterator — _cur in the template never clashes with call-site vars
(define-syntax each
(syntax-rules ()
[(_ var lst body ...)
(do (let _cur lst)
(while (! (null? _cur))
(let var (car _cur))
body ...
(set! _cur (cdr _cur))))]))
; unroll: macro-case duplicates body N times at expand time — no loop in generated C
(define-syntax unroll
(macro-case ()
[(_ n body ...)
`(do ,@(apply append (map (lambda (_) body) (iota n))))]))
(fn fold (lst f init)
(let acc init)
(each x lst (set! acc (f x acc)))
(return acc))
(with-arena
(fn main ()
(let nums (cons 2 (cons 3 (cons 4 (cons 5 nil)))))
; closure captures base from the enclosing scope
(let base 10)
(let add-base (lambda ((base) (x)) (return (+ x base))))
; _cur here is a different variable from the one inside each —
; hygiene ensures they never collide
(let _cur 99)
(each n nums
(printf "%d " (cast int (add-base n))))
(printf "\n")
(let sum (fold nums (lambda (x acc) (return (+ x acc))) 0))
(printf "sum: %d\n" (cast int sum))
; unroll inlines 4 increments — no branch, no loop counter in the generated C
(let ticks 0)
(unroll 4 (set! ticks (+ ticks 1)))
(printf "ticks: %d\n" (cast int ticks))
(printf "_cur: %d\n" (cast int _cur))))
with-arena attaches a heap arena to the function. All alloc calls inside use it;
everything is freed on return. The default size is 1 MB.
(let x 10) ; declare
(set! x (+ x 1)) ; mutate
(if (> x 5)
(printf "big\n")
(printf "small\n"))
(while (< x 100)
(set! x (* x 2)))
(while #t
(if (== x 0) (break))
(if (== (% x 2) 0) (do (set! x (- x 1)) (continue)))
(set! x (- x 1)))
(block ; introduces a C scope — variables declared here are local to it
(let tmp x)
(set! x 0))
do sequences expressions and returns the last one; block does the same but wraps
in { } so let bindings inside don't escape. break and continue work exactly
as in C — they apply to the innermost enclosing while.
when and unless are one-armed conditionals:
(when (< x 10) (printf "small\n"))
(unless (> x 10) (printf "not large\n"))
cond chains tests with an optional else. Single-form clauses execute as
statements (useful for inline bindings):
(cond
[(< x 0) (printf "negative\n")]
[(let abs (- 0 x))]
[(< abs 10) (printf "small\n")]
[else (printf "big\n")])
(fn square (n)
(return (* n n)))
(fn abs-val (n)
(if (< n 0)
(return (* n -1))
(return n)))
All functions return AnchorVal. main is the exception — it returns int.
Tail calls are not optimized; use while for loops.
Anchor identifiers are mapped to valid C names: - → _, ! → _mut,
? → _p, . → _dot_, > → gt_, < → lt_, % → _pct_.
So string->symbol becomes string_gt_symbol in the generated C.
(+ a b) (- a b) (* a b) (/ a b) (% a b) ; signed integer
(f+ a b) (f- a b) (f* a b) (f/ a b) ; float64 (double)
(f32+ a b) (f32- a b) (f32* a b) (f32/ a b) ; float32
(u+ a b) (u- a b) (u* a b) (u/ a b) (u% a b) ; unsigned
(band x mask) (bor x y) (bxor x y) (bnot x)
(lshift x n) (rshift x n)
(== a b) (!= a b) (< a b) (> a b) (<= a b) (>= a b)
(f== a b) (f!= a b) (f< a b) (f> a b) (f<= a b) (f>= a b) ; float64
(f32== a b) (f32!= a b) (f32< a b) (f32> a b) (f32<= a b) (f32>= a b) ; float32
(u< a b) (u> a b) (u<= a b) (u>= a b) ; unsigned
Bare decimal literals like 0.5 produce f64 (double) values — the bit pattern of
the double is stored directly in the AnchorVal. Use f+, f-, f*, f/ and the
f==/f</etc. comparisons.
f32 (float) values store 32-bit IEEE 754 bits in the low 32 bits of the AnchorVal.
Create them with (f32 expr):
(let a (f32 3.14)) ; f32 literal
(let b (f32 2.0))
(let c (f32+ a b)) ; f32 arithmetic
(printf "%f\n" (cast double (f32->f64 c))) ; widen to f64, cast for printf
Conversions use the -> family, which actually converts between representations:
| Form | Description |
|---|---|
(f32 expr) |
Create f32 from literal or integer |
(int->f64 expr) |
Convert integer → f64 |
(int->f32 expr) |
Convert integer → f32 |
(f64->f32 expr) |
Narrow f64 → f32 |
(f32->f64 expr) |
Widen f32 → f64 |
(f64->int expr) |
Truncate f64 → integer |
(f32->int expr) |
Truncate f32 → integer |
cast vs ->: cast does not convert — it tells C what type the AnchorVal
already holds. The -> functions actually convert between representations:
; cast = "this is already a double, unwrap it for C"
(printf "%f\n" (cast double 3.14))
; -> = "convert this integer into f64 representation, then unwrap"
(printf "%f\n" (cast double (int->f64 42)))
FFI and floats: Both ffi and fn-c respect C types at the boundary. A float
param expects f32 bits; a double param expects f64 bits. The compiler does not
auto-convert — the AnchorVal must already hold the right representation:
(ffi sqrtf (float) -> float)
(ffi sqrt (double) -> double)
(sqrtf (f32 2.0)) ; f32 in → f32 out
(sqrtf (f64->f32 x)) ; narrow f64 to f32 before calling
(sqrt 2.0) ; f64 in → f64 out
Return values follow the same rule: -> float returns an f32 AnchorVal,
-> double returns an f64 AnchorVal.
Anchor uses Chez Scheme's #\ syntax. Character literals evaluate to their Unicode codepoint as an integer:
#\a ; 97
#\newline ; 10
#\space ; 32
#\tab ; 9
#\nul ; 0
#\x41 ; 65 (hex codepoint)
#\[ ; 91 (punctuation)
Split code across multiple files using (include "path/to/file.anc"). The compiler resolves includes at parse time and inlines them into the AST before expansion — so macros, structs, and functions defined in an included file are visible everywhere after the include point.
(include "math.anc")
(include "utils/string.anc")
Paths are relative to the file containing the include. You only pass one file to the compiler; everything else comes in through includes:
./anchorc main.anc --runDuplicate includes are silently ignored — each file is inlined at most once regardless of how many times it appears, so circular or redundant includes are safe.
C header includes pass through unchanged to the generated C file:
(include <stdio.h>)
(include "mylib.h")
Declare a C function once; call it directly. Fixed parameters are automatically cast
to the declared types. Variadic arguments (after ...) require explicit (cast TYPE arg).
(include <unistd.h>)
(include <string.h>)
(ffi write (int const void* size_t) -> int)
(ffi memcpy (void* (const void*) size_t) -> void*)
(ffi strlen ((const char*)) -> size_t)
; call — types applied automatically for fixed params
(write 1 buf (strlen buf))
; variadic — explicit cast required
(ffi printf (const char* ...) -> int)
(printf "%d items\n" (cast int count))
Use (c-const NAME) to pull in a C preprocessor constant at compile time:
(c-const STDOUT_FILENO) ; → anchor_int((intptr_t)(STDOUT_FILENO))
(c-const CLOCKS_PER_SEC)
Use fn-c when you need a function with a specific C signature — for callbacks,
qsort comparators, signal handlers, or any place the C ABI is fixed. Parameters
are automatically wrapped as AnchorVal inside the body so Anchor expressions work
normally; the return value is cast back to the declared C type.
(include <stdlib.h>)
(fn-c compare-ints ((const void* a) (const void* b)) -> int
(let av (deref (cast intptr_t* a)))
(let bv (deref (cast intptr_t* b)))
(return (- av bv)))
; qsort needs a C comparator — fn-c is it
(ffi qsort (void* size_t size_t void*) -> void)
(qsort arr n 8 (fn-ptr compare-ints))
No separate (ffi ...) declaration is needed for the fn-c function itself — the
compiler registers it in the extern table automatically.
Each parameter is a space-separated list ending with the parameter name:
(const void* a) → C type const void*, name a.
Because parameters are immediately re-boxed as AnchorVal, the declared C type is
only visible at the call boundary — inside the body every parameter is an AnchorVal
regardless of its declared type. To use a parameter as its original C type you must
cast it explicitly:
(fn-c greet ((const char* name)) -> void
(printf "Hello, %s\n" (cast char* name))) ; cast needed — name is AnchorVal
lambda creates an anonymous callable. Call it exactly like a named function — no
special syntax at the call site.
(global-arena pool (mb 1))
(fn main ()
(with-arena pool
; plain lambda — no captures
(let sq (lambda (x) (return (* x x))))
(sq 5) ; → 25
; closure — first arg is ((captures...) (params...))
(let base 10)
(let add-base (lambda ((base) (x)) (return (+ x base))))
(add-base 7) ; → 17
(sq 6) ; → 36
))
Captures are packed into a flat struct [fn-ptr, cap0, cap1, ...] allocated in the
current arena. Bit 63 is set to mark the pointer as a closure. Both live as long as the arena does — don't call a lambda after its arena is
reset.
Multiple captures work the same way:
(let a 3) (let b 4)
(let f (lambda ((a b) (x)) (return (+ x (+ a b)))))
(f 10) ; → 17
fn-ptr takes the address of any named function (fn, fn-c, or ffi) and boxes
it as an AnchorVal. The result uses the same calling convention as lambdas — you can
store it in a variable and call it directly:
(fn add (a b) (return (+ a b)))
(let fp (fn-ptr add))
(fp 3 4) ; → 7
; also works for passing callbacks to C
(ffi qsort (void* size_t size_t void*) -> void)
(qsort arr n 8 (fn-ptr compare-ints))
For calling C-typed function pointers (fn-c or ffi), use call-ptr-c with an
explicit signature:
(let fp (fn-ptr printf))
(call-ptr-c fp ((const char* ...) -> int) "x = %d\n" 42)
The signature ((param-types...) -> ret-type) matches the ffi declaration syntax.
Calling convention: fn-ptr and lambda values share a unified tagged-pointer
convention. Bit 63 flags whether the value is a closure: 0 means direct call (no
environment), 1 means the pointer addresses a flat struct [fn-ptr, caps...] and
the callee receives the captures as a hidden first argument. This means fn-ptr values,
plain lambdas, and closures are all interchangeable at call sites.
alloc bumps the current arena pointer — O(1), no malloc overhead.
kb, mb, and gb are built-in size macros that expand at compile time.
(with-arena (mb 4) ...)
(global-arena scratch (kb 64))
(alloc (kb 512))
(with-arena (mb 4) ; 4 MB arena
(fn process (n)
(let buf (alloc (* n 8))) ; slice of arena bytes
; buf freed automatically when process returns
))
global-arena declares a named arena whose backing buffer lives for the entire
program. Use it when allocations need to outlive the function that creates them —
linked lists, trees, or any per-request scratch buffer that gets rebuilt in a loop.
To attach a global arena to a function so all its allocations go there, wrap the
fn definition with with-arena:
(global-arena scratch (kb 64))
(with-arena scratch
(fn build-list (n) ...)) ; all allocs inside go into scratch
(global-arena scratch (kb 64)) ; 64 KiB, allocated once
(fn build-list (n)
(with-arena scratch ; directs allocations into scratch
(let result nil)
(while (> n 0)
(set! result (cons n result))
(set! n (- n 1)))
(return result))) ; safe — scratch is never freed
(fn main ()
(let lst (build-list 5))
; ... use lst ...
(arena-reset! scratch) ; reclaim all allocations in O(1)
(let lst2 (build-list 3)) ; reuse same backing memory
)
Returning a list from a with-arena scratch block is safe because the backing
memory is permanent. Contrast with anonymous with-arena scopes, where returning
a linked structure would dangle — only flat values copy out correctly on return.
with-parent-arena temporarily redirects allocations into the arena one level up
the stack. Use it to deep-copy a data structure out of a scope that is about to end:
(fn list-copy (lst)
(if (null? lst)
(return nil))
(with-parent-arena
(return (cons (car lst) (list-copy (cdr lst))))))
(with-arena
(fn main ()
(let original nil)
(with-arena
(set! original (cons 1 (cons 2 (cons 3 nil))))
(set! original (list-copy original))) ; copied into outer arena before inner dies
; original is safe to use here
))
Nesting with-parent-arena climbs one level each time. Global arenas behave the
same as local ones — the parent of whatever arena is currently active is used.
arena-reset! reclaims arena allocations in O(1). With a name it resets that global
arena to zero; with no arguments it resets the current arena to its checkpoint floor
(or zero if no checkpoint is active):
(arena-reset! scratch) ; reset named global arena to zero
(arena-reset!) ; reset current arena to checkpoint floor (or 0)
with-arena-checkpoint saves the current allocation point and restores it on scope
exit — everything allocated inside the checkpoint is reclaimed automatically. Use it
for scratch work in a loop without resetting the entire arena:
(global-arena pool (mb 10))
(with-arena pool
(let permanent (alloc 1024)) ; lives beyond checkpoints
(while (< i n)
(with-arena-checkpoint
;; all scratch allocations here are reclaimed at end of each iteration
(let tmp (alloc (* size 8)))
(process tmp))
(set! i (+ i 1)))
;; permanent is still valid here
(use permanent))
arena-reset! inside a checkpoint resets to the checkpoint's start, not to zero —
preserving allocations made before the checkpoint:
(with-arena-checkpoint
(let a (alloc 64))
(let b (alloc 128))
(arena-reset!) ; reclaims a and b, resets to checkpoint start
(let c (alloc 64))) ; reuses the same memory
Checkpoints nest — each level saves and restores independently. arena-reset! always
targets the innermost checkpoint. The checkpoint value is stored in the arena struct
at runtime, so it works correctly across function boundaries:
(fn do-scratch ()
(let tmp (alloc 512))
(arena-reset!) ; resets to caller's checkpoint floor, not zero
)
(with-arena-checkpoint
(do-scratch)) ; checkpoint protects pre-existing allocations
(arena-remaining) returns the number of bytes still available in the current arena
as a signed integer. Useful for debugging or capacity checks:
(let free (arena-remaining))
(if (< free (mb 1))
(printf "arena nearly full\n"))
(arena-in? ptr) returns 1 if ptr falls within the current arena's buffer, 0
otherwise. Useful for asserting that a pointer was allocated from the expected arena:
(let node (alloc (sizeof Node)))
(if (! (arena-in? node))
(printf "unexpected allocation source\n"))
(ref expr) takes the address of a value; (deref ptr) reads through one. When
the argument is a plain variable, ref takes its address directly (no copy). For
complex expressions, a temporary is created.
(let n 0)
(some-c-fn (ref n)) ; passes &n directly
(let result (deref ptr))
; useful for treating a scalar as a C string (e.g. packed symbols)
(let s (sym hello))
(printf "%s\n" (cast char* (ref s))) ; prints "hello"
Fields default to 8 bytes. Specify smaller sizes explicitly (e.g. 4 for int,
1 for char, 2 for short).
(struct Point (x 8) (y 8))
(let p (alloc (sizeof Point)))
(set! p Point x 100)
(set! p Point y 200)
(let px (get p Point x)) ; → anchor_int(100)
get, aref, and set! take the pointer first, then the struct type, then fields. Nest structs inline using (sizeof Name) as the field size — chain field names to navigate without an intermediate variable. Use -> to follow a stored pointer instead of navigating into inline bytes:
(struct AABB
(min (sizeof Point))
(max (sizeof Point)))
(let b (alloc (sizeof AABB)))
;; chained — embedded: navigate into inline Point bytes
(set! b AABB min Point x 0)
(set! b AABB min Point y 0)
(set! b AABB max Point x 800)
(set! b AABB max Point y 600)
(let x0 (get b AABB min Point x))
;; pointer field — node.next stores an address to another Node
;; (struct Node (val 8) (next 8))
(get n Node nxt -> Node val) ; -> signals pointer dereference
(set! n Node nxt -> Node val 99)
aref is the address-returning mirror of get — same navigation syntax, but stops at the address rather than reading the value. Use it wherever you need a pointer to a field rather than its contents:
(aref b AABB min Point x) ; address of x inside the embedded min Point
(aref b AABB min Point) ; address of the embedded min Point itself
(aref b AABB min) ; address of the min field
aref supports the full navigation chain including -> pointer follows and byte-offset steps, so it composes naturally with array indexing.
A bare number or expression in get/set! is a byte offset into the buffer. For element i with element size sz, the offset is (* i sz):
(let arr (alloc (* n 8)))
(set! arr 0 42) ; write to byte offset 0
(set! arr 8 99) ; write to byte offset 8
(let v (get arr (* i 8))) ; scalar read at offset i*8
For sub-8-byte elements, pass a byte size after the offset:
(let buf (alloc (* n 3))) ; array of 3-byte integers
(set! buf 0 3 100) ; write 3 bytes at offset 0
(set! buf 3 3 200) ; write 3 bytes at offset 3
(let v (get buf (* i 3) 3)) ; read 3 bytes at offset i*3
Literal sizes are checked at compile time (must be ≤ 8). Runtime sizes
emit a trap guard; use memcpy directly to avoid the check.
Array of structs — byte offset then named chain in one form:
(let pts (alloc (* n (sizeof Point))))
(let sz (sizeof Point))
(set! pts 0 Point x 10)
(set! pts 0 Point y 20)
(let x0 (get pts 0 Point x))
(let x1 (get pts sz Point x))
(let x2 (get pts (* 2 sz) Point x))
;; aref for a pointer to element i
(let p (aref pts (* i sz) Point)) ; address of element i
(let px (aref pts (* i sz) Point x)) ; address of field x in element i
Array of pointers — -> dereferences a stored pointer:
(let arr (alloc (* n 8)))
(set! arr 0 p0)
(let x (get arr 0 -> Point x))
(set! arr 0 -> Point x 99)
Pointer-to-array in a struct field — follow with -> then navigate by byte offset:
(struct Bag (len 8) (items 8)) ; items stores a raw pointer
;; get items base pointer, then index into it
(let base (get bag Bag items)) ; follow the items pointer
(let x (get base (* i sz) Point x)) ; element i, field x
(let addr (aref base (* i sz) Point x)) ; address of element i, field x
All fields share offset 0. Total size is the largest field.
(union Val
(as-byte 1)
(as-int 4)
(as-ptr 8)) ; total size = 8 (largest field)
(let u (alloc (sizeof Val)))
(set! u Val as-ptr 0x0102030405060708)
(get u Val as-ptr) ; 0x0102030405060708
(get u Val as-int) ; 0x05060708 — lower 32 bits
(get u Val as-byte) ; 0x08 — lowest byte
Emit #define constants. Access them with (c-const Name_Variant).
(enum Direction
(North 0) (East 1) (South 2) (West 3))
(if (== dir (c-const Direction_North))
(printf "heading north\n"))
Auto-incrementing (omit the value):
(enum Color Red Green Blue) ; Red=0, Green=1, Blue=2
tiny-struct packs named bit fields into a single AnchorVal scalar — no allocation,
no pointer, just inline bit operations. Field sizes are in bits. Total must be ≤ 64.
(tiny-struct Color (r 8) (g 8) (b 8) (a 8)) ; 32 bits = 4 bytes
(let c (Color 255 128 64 200)) ; constructor — packed into one integer
(Color-get c r) ; → 255 (first field: mask only, no shift)
(Color-get c g) ; → 128 (shift + mask)
(Color-get c a) ; → 200 (last field: shift only, no mask)
(let c2 (Color-set c g 42)) ; returns NEW value — no mutation
(Color-get c2 g) ; → 42
(Color-get c g) ; → 128 (original unchanged)
Value semantics: Color-set returns a new packed integer. The original is unchanged.
This is different from regular structs, which mutate through a pointer.
sizeof works — returns the byte count (bits rounded up):
(sizeof Color) ; → 4
Tiny structs compose with regular structs for storage:
(struct Pixel (color (sizeof Color)) (x 4) (y 4))
(let p (alloc (sizeof Pixel)))
(set! p Pixel color (Color 255 0 0 255))
(let c (get p Pixel color))
(Color-get c r) ; → 255
Fields can be any bit width — not just byte-aligned:
(tiny-struct Flags (read 1) (write 1) (exec 1) (mode 5)) ; 8 bits = 1 byte
(let f (Flags 1 1 0 3))
(Flags-get f mode) ; → 3
(let f2 (Flags-set f exec 1)) ; flip exec on
(global count 0) ; mutable global AnchorVal
(set! count (+ count 1))
(global buf (array-of (kb 64))) ; static 64 KB buffer
(const max-size 4096) ; immutable — compiler may fold it
(const keys (array "Am" "F" "C" "G")) ; static array of strings
cons, car, cdr, set-car!, set-cdr!, nil, and null? are built into the language.
cons allocates a two-slot cell from the current arena.
nil is the zero value — a null pointer. It is identical to integer 0 at the bit
level; null? simply tests whether the value is zero.
(let lst (cons 1 (cons 2 (cons 3 nil))))
(let cur lst)
(while (! (null? cur))
(printf "%d\n" (cast int (car cur)))
(set! cur (cdr cur)))
set-car! and set-cdr! mutate a cons cell in place:
(set-car! lst 99) ; replace head value
(set-cdr! lst (cons 5 nil)) ; replace tail
for is a unified loop macro with range and each forms. continue and break work correctly — the increment is auto-inserted before continue.
;; range
(for range (i 10) (printf "%lld " i)) ; 0 to 9
(for range (i 3 8) (printf "%lld " i)) ; 3 to 7
;; untyped array
(for each (val arr len) (printf "%lld " val))
(for each ((val i) arr len) (printf "%lld:%lld " i val))
;; typed struct array
(for each (Point p points count)
(printf "(%lld,%lld) " (get p Point x) (get p Point y)))
(for each (Point (p i) points count)
(set! p Point x (* i 10)))
;; cons list
(for each (v lst ->) (printf "%lld " v))
(for each ((v i) lst ->) (printf "%lld:%lld " i v))
Limits, lengths, and counts are evaluated once. Untyped array iteration uses an offset-bump pattern that keeps the base pointer loop-invariant, enabling SIMD auto-vectorization in the generated C.
Symbols pack short strings (up to 7 bytes UTF-8) into a single AnchorVal integer.
Comparison is a single integer == — no strcmp, no hashing. The 8th byte is always
zero, so (ref s) yields a valid null-terminated C string for free.
(let status (sym ready)) ; compile-time constant — zero cost
(let other (sym waiting))
(if (== status (sym ready)) ; single integer comparison
(printf "go!\n"))
(printf "status: %s\n" (cast char* (ref status))) ; prints "ready"
sym is a macro that runs at compile time. The identifier is converted to a UTF-8
byte sequence and packed into a 64-bit integer literal — no function call in the
generated C.
For runtime conversion from a C string, use string->symbol:
(let s (string->symbol "hello")) ; runtime — packs bytes via memcpy
(== s (sym hello)) ; → 1 (same integer)
Symbols longer than 7 bytes are truncated by string->symbol (returns 0 from sym
if the guard fails at compile time). The 7-byte limit covers most identifiers and
keywords: default, private, include, message, request, display, etc.
| Flag | Effect |
|---|---|
--run |
Compile and execute immediately |
-o FILE |
Output binary (or .c if FILE ends in .c) |
--emit-exp |
Print macro-expanded AST |
--multi-threaded |
Use _Thread_local for the arena pointer. Required for programs that use threads. Without this flag, the arena pointer is a plain global — faster (no TLS overhead) but not thread-safe. |
Hygienic: names introduced in a template (like _tmp) are automatically gensymmed
so they never clash with variables at the call site.
; when / unless — one-armed conditionals
(define-syntax when
(syntax-rules ()
[(_ cond body ...)
(if cond (do body ...))]))
(define-syntax unless
(syntax-rules ()
[(_ cond body ...)
(if (! cond) (do body ...))]))
; for loop — block scopes the variable, so it doesn't leak after the loop
(define-syntax for
(syntax-rules (to)
[(_ var from to limit body ...)
(block ; C scope: var not visible after the loop
(let var from)
(while (< var limit)
body ...
(set! var (+ var 1))))]))
; swap! — _tmp is gensymmed, so (let _tmp 99) in caller is safe
(define-syntax swap!
(syntax-rules ()
[(_ a b)
(do (let _tmp a) (set! a b) (set! b _tmp))]))
Recursive patterns — my-and rewrites itself until base cases apply:
(define-syntax my-and
(syntax-rules ()
[(_) 1]
[(_ e) e]
[(_ e rest ...) (if e (my-and rest ...) 0)]))
Literal keyword in pattern — else is matched exactly, not as a pattern variable:
(define-syntax my-cond
(syntax-rules (else)
[(_ (else body ...)) (do body ...)]
[(_ (test body ...) clause ...) (if test (do body ...) (my-cond clause ...))]))
Templates are plain Chez Scheme code. Pattern variables bind to the matched Anchor
AST values. This lets you run arbitrary computation — length, map, iota, string
manipulation — before emitting a single line of C.
Three template styles are available inside macro-case clause bodies:
| Style | Ellipsis vars | Ellipsis in template |
|---|---|---|
` (Chez quasiquote) |
plain Chez lists | ,@var to splice |
#' (syntax template) |
plain Chez lists | var ... via pattern engine |
#` (quasisyntax) |
plain Chez lists | var ... via pattern engine, #,expr for escapes |
With backtick, body ... in the template is a literal symbol pair — use ,@body to splice.
With #' or #`, the pattern engine handles var ... expansion directly.
arena-array — size computed at expand time, indices are literals:
(define-syntax arena-array
(macro-case ()
[(_ name val ...)
(let* ([n (length val)]
[size (* n 8)])
`(do
(let ,name (alloc ,size))
,@(let loop ([i 0] [vs val])
(if (null? vs) '()
(cons `(set! ,name ,(* i 8) ,(car vs))
(loop (+ i 1) (cdr vs)))))))]))
(arena-array primes 2 3 5 7 11 13)
; expands to: (let primes (alloc 48))
; (set! primes 0 2)
; (set! primes 8 3) ...
unroll — loop body inlined N times, enforced by guard:
(define-syntax unroll
(macro-case ()
[(_ n body ...)
(number? n)
`(do ,@(apply append (map (lambda (_) body) (iota n))))]))
(unroll 4 (set! ticks (+ ticks 1)))
; expands to four sequential set! calls — no loop, no branch
syntax-rules cannot write macros whose inner templates contain ... because the
outer instantiator would try to expand them. macro-case with quasiquote treats
the inner template as plain data — r, ..., x are just symbols being consed
into a list:
(define-syntax define-fold-op
(macro-case ()
[(_ name op identity)
`(define-syntax ,name
(syntax-rules ()
[(_) ,identity]
[(_ x) x]
[(_ x r ...) (,op x (,name r ...))]))]))
(define-fold-op my-add + 0)
(define-fold-op my-mul * 1)
(my-add 1 2 3 4) ; → 10
(my-mul 2 3 4) ; → 24
A macro that returns (do ...) at the top level has its children spliced as
separate top-level forms. This lets one call site emit a struct definition, a
constructor, and accessor functions:
(define-syntax define-struct
(macro-case ()
[(_ name (field size) ...)
(let* ([sname (id-sym name)]
[cname (string->symbol (string-append "make-" (symbol->string sname)))]
[pnames (map (lambda (f) (string->symbol (string-append "p_" (symbol->string (id-sym f))))) field)]
[anames (map (lambda (f) (string->symbol (string-append (symbol->string sname) "-" (symbol->string (id-sym f))))) field)])
`(do
(struct ,name ,@(map list field size))
(fn ,cname (,@pnames)
(let _ptr (alloc (sizeof ,name)))
,@(map (lambda (f p) `(set! _ptr ,name ,f ,p)) field pnames)
(return _ptr))
,@(map (lambda (aname f)
`(fn ,aname (s) (return (get s ,name ,f))))
anames field)))]))
(define-struct Vec2 (x 8) (y 8))
; Generated at compile time:
; (struct Vec2 (x 8) (y 8))
; (fn make-Vec2 (p_x p_y) ...)
; (fn Vec2-x (s) ...)
; (fn Vec2-y (s) ...)
By default macros are hygienic: names introduced in a template never clash with
names at the call site. For deliberately anaphoric macros (e.g. aif, which
binds it for the user to reference), use datum->syntax to place a name in the
call-site scope. Name the keyword position in the pattern (rather than _) to
get a handle carrying the call-site marks:
(define-syntax aif
(macro-case ()
[(self test then else-clause)
`(block
(let ,(datum->syntax self 'it) ,test)
(if ,(datum->syntax self 'it) ,then ,else-clause))]))
(aif (find-item key table)
(printf "found: %d\n" (cast int it))
(printf "not found\n"))
it inside then refers to the macro-introduced binding, not any outer it.
The block scope means any outer it is simply shadowed, not renamed.
Place (define name body) forms after the literals list and before the first
pattern clause. They become internal definitions scoped to that macro — shared
across all branches, invisible outside.
(define-syntax for
(macro-case (to)
(define walk-continue
(lambda (stmts incr)
(map (lambda (s)
(let walk ([form s])
(cond
[(not (pair? form)) form]
[(memq (id-sym (car form)) '(fn lambda while)) form]
[(eq? (id-sym (car form)) 'continue)
`(do ,incr (continue))]
[else (map walk form)])))
stmts)))
(define expand-body
(lambda (body)
(let ([expanded (local-expand `(do ,@body))])
(if (and (pair? expanded) (eq? (id-sym (car expanded)) 'do))
(cdr expanded)
(list expanded)))))
[(_ i to limit body ...)
(let* ([incr `(set! ,i (+ ,i 1))]
[stmts (expand-body body)]
[walked (walk-continue stmts incr)])
`(block (let ,i 0)
(while (< ,i ,limit)
,@walked
,incr)))]))
Local helpers have access to the same functions as macro-case templates:
id-sym, anchor-error, anchor-gensym, datum->syntax, is-struct?,
local-expand, filter-map, and syntax record accessors (stx?, stx-sym,
stx-marks, stx-src, make-stx).
| File | What it shows |
|---|---|
examples/hello.anc |
Hello World, with-arena, basic FFI |
examples/fizzbuzz.anc |
Functions, while, conditionals, % |
examples/array.anc |
alloc, get/set!, bubble sort, for macro |
examples/linked_list.anc |
cons/car/cdr/nil/null?, list operations |
examples/global_arena.anc |
global-arena, arena-reset!, lists escaping function scope |
examples/structs.anc |
Structs, nested structs, unions, enums, array-of-structs |
examples/macros_showcase.anc |
Full macro spectrum: syntax-rules → macro-case → macros defining macros |
examples/fn_pointers.anc |
fn-ptr, call-ptr, fn-c, call-ptr-c, passing callbacks to qsort |
examples/get_set_chains.anc |
get/aref/set! edge cases: array-of-structs, array-of-pointers, -> pointer chaining, embedded struct chains, inline array fields, byte-offset navigation |
Uniform value type. Every value is an AnchorVal — a raw 64-bit quantity that
holds either a pointer or an integer/float unboxed. Everything flows through the same
ABI — no overloaded calling conventions, no special-casing for primitives, no GC
write barriers.
Arenas, not GC. alloc bumps a pointer. Anonymous with-arena scopes free all
allocations when the block exits. global-arena declares a named arena with permanent
backing memory — reset it explicitly with arena-reset! when you want to reclaim.
with-arena-checkpoint marks a restore point for scratch work within a loop or
function call — everything allocated inside is reclaimed at scope exit, while earlier
allocations survive. Arenas nest and stack; cons and alloc always use the innermost
active arena.
C as the backend. The compiler emits a single .c file with no dependencies
beyond anchor.h (included from anchor/runtime/). You can inspect, modify, or
link the C output directly. cc is invoked automatically with --run or when
compiling to a binary.
Hygiene without a runtime. Macros use KFFD mark-based hygiene. Each macro application gets a fresh mark; user-provided identifiers cancel (XOR) while template-introduced names keep their mark and become global references after resolution. No syntax objects, no scope chains — Anchor has no module system or runtime environments, so the flat-namespace model is sufficient.