A stack-based programming language with static type inference and linear types. Single-file C99 interpreter (~3000 lines).
brew install surprisetalk/tap/slapOr build from source:
make slapecho '2 3 plus print' | slap # → 5
slap < examples/euler/1.slap # → 233168For the SDL graphics build:
make slap-sdl # requires SDL2
slap-sdl < examples/life.slapCLI:
slap [--check] [--headless] [args...] < file.slap
--check type-check only, no execution
--headless (SDL) run without a window, tick loop continues indefinitely
System primitives:
args— pushes list of CLI positional args (strings)isheadless— pushes 1 if--headless, else 0cwd— pushes current working directory as string
Values go on a stack. Words consume and produce values.
2 3 plus -- 5
10 3 sub -- 7
4 5 mul -- 20
15 4 div -- 3
15 4 mod -- 3
Stack manipulation:
3 dup -- 3 3
4 1 drop -- 4 (drop 1 item)
6 5 swap -- 5 6
7 8 (1 plus) dip -- 8 8 (apply under top)
(2 3 plus) apply -- 5 (execute a tuple literal)
2.0 3.0 plus -- 5.0
9.0 fsqrt -- 3.0
42 itof -- 42.0
3.7 ftoi -- 3
2.0 3.0 fpow -- 8.0
1.0 flog -- 0.0
Atomic identifiers. Prefixed with '.
'hello -- 'hello
'hello 'hello eq -- 1
'hello 'world eq -- 0
true is 1, false is 0.
1 1 and -- 1
0 1 or -- 1
1 not -- 0
3 5 lt -- 1
5 3 gt -- 1
3 3 eq -- 1
let takes value-then-name. On lookup, tuples auto-execute; other values push.
-- tuple binding: auto-executes on lookup (for functions)
(2 mul) 'double let
5 double -- 10
-- scalar binding: pushes on lookup
42 'answer let
answer -- 42
To bind a literal tuple as data (so lookup pushes it without running), wrap in extra parens — the outer tuple auto-execs and pushes the inner:
((1 2 3)) 'foo let
foo -- (1 2 3) on stack
To thread a bound closure through a nested call without auto-executing it, use quote:
'foo quote -- pushes foo's raw value (no auto-exec)
quote is mainly used when passing a closure parameter into a recursive call — see closures.
-- if: condition then two branches
10 dup 5 lt (2 mul) (3 mul) if -- 30
-- case: multi-way conditional with default (also dispatches on tagged values)
10 0 {(5 lt) (2 mul) (20 lt) (3 mul)} case -- 30
-- while: loop
1 (dup 100 lt) (2 mul) while -- 128
A name is self-visible inside its own body, so recursion needs no keyword:
(dup 1 le (drop 1) (dup 1 sub factorial mul) if) 'factorial let
5 factorial -- 120
(dup 1 le () (dup 1 sub fib swap 2 sub fib plus) if) 'fib let
10 fib -- 55
(dup 0 eq (drop) (swap over mod gcd) if) 'gcd let
12 8 gcd -- 4
Functions capture their defining scope:
('n let (n plus)) 'make-adder let
5 make-adder 'add5 let
3 add5 -- 8
7 add5 -- 12
('lo let 'hi let (dup lo le not swap hi lt and)) 'make-between let
10 1 make-between 'in-range let
5 in-range -- 1
15 in-range -- 0
When a closure is passed as a parameter and needs to be handed down to a recursive call, use quote to push it as data rather than running it:
-- 'pred let binds a predicate; recursion passes it down via 'pred quote
('pred let dup 0 gt (dup pred drop 1 sub 'pred quote countdown) () if) 'countdown let
5 (iseven) countdown -- applies pred at each step, terminates at 0
(2 mul) (1 plus) compose
3 swap apply -- 7
-- chain multiple
(1 plus) (2 mul) compose (3 sub) compose (sqr) compose
5 swap apply -- 81
-- creation
list -- []
[1 2 3] -- [1 2 3]
0 5 range -- [0 1 2 3 4]
-- mutation (pop/get/set return tagged; must unwraps or panics on no)
list 10 push 20 push -- [10 20]
[10 20 30] pop must -- 30 [10 20]
[10 20 30] 1 get must -- 20 (consumes the list)
[10 20 30] 1 peek must -- 20, with [10 20 30] still under it
[10 20 30] 1 99 set must -- [10 99 30]
[1 2] [3 4] cat -- [1 2 3 4]
-- slicing (clamped; no tagged result)
[1 2 3 4 5] 3 take-n -- [1 2 3]
[1 2 3 4 5] 2 drop-n -- [3 4 5]
-- higher-order
[1 2 3] (2 mul) each -- [2 4 6] (map is spelled `each`)
[1 2 3 4 5] (2 mod 1 eq) filter -- [1 3 5]
[1 2 3] 0 (plus) fold -- 6
[1 2 3] (plus) reduce -- 6
-- sorting and searching
[3 1 2] sort -- [1 2 3]
[1 2 3] reverse -- [3 2 1]
[1 2 2 3 3] dedup -- [1 2 3]
[10 20 30] 20 index-of must -- 1
[10 20 30] -1 (15 lt not) find -- 20 (default -1 if no match)
-- structural
[1 2 3] [4 5 6] zip -- [[1 4] [2 5] [3 6]]
[1 2 3 4] 2 windows -- [[1 2] [2 3] [3 4]]
[1 2 3 4 5] 2 rotate -- [4 5 1 2 3]
[[1 2] [3 4]] flatten -- [1 2 3 4]
-- analysis
[30 10 20] rise -- [1 2 0] (ascending rank)
[30 10 20] fall -- [0 2 1] (descending rank)
[1 2 1 3 2] classify -- [0 1 0 2 1]
[5 3 8 1 7] (4 lt) where -- [1 3] (indices)
[10 20 30 40] [0 2 3] select -- [10 30 40]
Immutable sequences. Parenthesized. Also used as code blocks. Access via destructuring (apply) or len.
(10 20 30) apply -- pushes 10 20 30
(1 2 3) len -- 3
Key-value maps keyed by symbols.
{'x 10 'y 20} -- record
{'x 10 'y 20} 'x at must -- 10 (at returns tagged; must unwraps)
{'x 10 'y 20} 30 'x into -- {'x 30 'y 20}
rec 10 'x into 20 'y into -- {'x 10 'y 20}
Strings are lists of Unicode codepoints. String literals are UTF-8 decoded at lex time — len counts characters, not bytes.
"hello" len -- 5
"hello" 0 get -- 104
"héllo" len -- 5 (not 6)
"héllo" 1 get must -- 233 (U+00E9)
"ab" "cd" cat -- "abcd"
For byte-oriented I/O (file reads, TCP, binary formats) use utf8-encode / utf8-decode to convert between codepoints and UTF-8 bytes explicitly.
Tag a value with a symbol to create a sum type. Use ok/no for result types, or 'sym tag for custom tags.
-- creating tagged values
123 ok -- 123 'ok tagged
"oops" no -- "oops" 'no tagged
42 'custom tag -- 42 'custom tagged
-- pattern matching with case on a tagged value
123 'foo tag 0 {'foo (1 plus) 'bar (2 mul)} case -- 124
123 'zzz tag 0 {'foo (1 plus) 'bar (2 mul)} case -- 0 (default fires; unmatched tag)
-- monadic chaining with then/default
('d let 'n let
d 0 eq ("division by zero" no) (n d div ok) if
) 'safe-div let
10 2 safe-div (3 mul) then -1 default -- 15
10 0 safe-div (3 mul) then -1 default -- -1
then chains operations on 'ok values — non-ok values pass through unchanged:
123 ok (1 plus) then (2 mul) then -1 default -- 248
"fail" no (1 plus) then (2 mul) then -1 default -- -1
Boxes wrap a value in a linear container that must be consumed exactly once.
42 box free -- box then immediately free
42 box (21 mul) lend -- 882 (borrow a snapshot)
free -- must free when done
42 box (1 plus) mutate -- modify in place
() lend 43 eq assert -- verify
free
42 box clone -- two independent boxes
free free -- each must be freed
Realistic example — a mutable counter:
{'count 0 'total 0} box
('count (1 plus) edit) mutate
('count (1 plus) edit) mutate
('total (100 plus) edit) mutate
() lend
dup 'count at 2 eq assert
'total at 100 eq assert
free
All code is type-checked before execution. Types are inferred — no annotations required.
| Category | Types | Rules |
|---|---|---|
| Stackable | Int, Float, Symbol, Tuple, Record, List, String, Tagged, Dict | Freely dup and drop |
| Linear | Box | Must consume exactly once |
Boxes must be consumed via free, lend, mutate, or clone. lend borrows a stackable snapshot from a box. Dicts are stackable: use dup to branch and drop to discard. free/clone reject dicts at type time — boxes only.
A dict is the one stackable type that cannot be let-bound. It is a heap object, and a binding would alias it rather than copy it, so dropping either copy would leave the other reading freed memory. Thread it on the stack (see examples/kv-server.slap) or use a record if you need something bindable.
Optional type annotations declare stack effects:
(2 mul) [int lent in int move out] effect 'double let
(dup mul) [int lent in int move out] effect 'square let
Forward declarations register a signature against a name defined later (used when a function needs to reference itself through a mutually-recursive helper):
'xml-render-pretty [int lent in rec own in list move out] effect
-- ...later...
(... xml-render-pretty ...) 'xml-render-pretty let
Ownership modes: lent (borrowed/copyable), move (consumed), own (linear ownership).
Built-in protocols group types by capability. Use in effect annotations:
(len) ['a sized lent in int move out] effect 'my-len let
(sort) ['a ord seq own in 'a ord seq move out] effect 'my-sort let
| Protocol | Keyword | Types | Operations |
|---|---|---|---|
| Sized | sized |
list, tuple, record, dict | len |
| Seq | seq |
list | get, peek, set, push, pop, cat |
| Eq | eq |
all stackable | eq |
| Ord | ord |
int, float | lt, sort |
| Num | num |
int, float | plus, sub, mul, div |
| Integral | integral |
int | mod, divmod, wrap, bitwise |
| Semigroup | semigroup |
list, tuple, record | cat |
Additional keywords recognized in annotations: functor (required by each), monad (required by then), dict (for the dict type), linear (alias for Box). Symbols are comparable with eq but not orderable.
~70 definitions in slap itself, loaded at startup.
| Word | Effect | Example |
|---|---|---|
over |
a b → a b a | 1 2 over → 1 2 1 |
nip |
a b → b | 1 2 nip → 2 |
rot |
a b c → b c a | 1 2 3 rot → 2 3 1 |
tuck |
a b → b a b | 1 2 tuck → 2 1 2 |
not |
n → n==0 | 1 not → 0 |
repeat |
x n f → f^n(x) | 1 10 (2 mul) repeat → 1024 |
| Word | Effect | Example |
|---|---|---|
inc |
n → n+1 | 5 inc → 6 |
dec |
n → n-1 | 5 dec → 4 |
neg |
n → -n | 5 neg → -5 |
abs |
n → |n| | -3 abs → 3 |
sqr |
n → n*n | 5 sqr → 25 |
cube |
n → n*n*n | 3 cube → 27 |
max |
a b → max | 3 5 max → 5 |
min |
a b → min | 3 5 min → 3 |
sign |
n → -1/0/1 | -3 sign → -1 |
clamp |
lo hi n → clamped | 1 10 5 clamp → 5 |
| Word | Effect | Example |
|---|---|---|
neq |
a b → a!=b | 3 5 neq → 1 |
gt |
a b → a>b | 5 3 gt → 1 |
ge |
a b → a>=b | 3 3 ge → 1 |
le |
a b → a<=b | 3 5 le → 1 |
| Word | Effect | Example |
|---|---|---|
iszero |
n → n==0 | 0 iszero → 1 |
ispos |
n → n>0 | 5 ispos → 1 |
iseven |
n → even? | 4 iseven → 1 |
isodd |
n → odd? | 3 isodd → 1 |
divides |
a b → b%a==0 | 3 9 divides → 1 |
isbetween |
n lo hi → in range? | 5 1 10 isbetween → 1 |
| Word | Effect | Example |
|---|---|---|
sum |
list → total | [1 2 3] sum → 6 |
product |
list → product | [1 2 3 4] product → 24 |
max-of |
list → max | [5 1 3] max-of → 5 |
min-of |
list → min | [5 1 3] min-of → 1 |
first |
list → elem | [1 2 3] first → 1 |
last |
list → elem | [1 2 3] last → 3 |
member |
list val → bool | [1 2 3] 2 member → 1 |
couple |
a b → [a b] | 1 2 couple → [1 2] |
flatten |
nested → flat | [[1 2] [3 4]] flatten → [1 2 3 4] |
table |
list f → [[x f(x)]...] | [1 2 3] (sqr) table → [[1 1] [2 4] [3 9]] |
select |
list indices → sublist | [10 20 30] [0 2] select → [10 30] |
reduce |
list f → result | [1 2 3] (plus) reduce → 6 |
keep-mask |
list mask → filtered | [10 20 30] [1 0 1] keep-mask → [10 30] |
| Word | Effect | Example |
|---|---|---|
rotate |
list n → rotated | [1 2 3 4 5] 2 rotate → [4 5 1 2 3] |
zip |
a b → pairs | [1 2 3] [4 5 6] zip → [[1 4] [2 5] [3 6]] |
windows |
list n → sublists | [1 2 3 4] 2 windows → [[1 2] [2 3] [3 4]] |
classify |
list → indices | [1 2 1 3 2] classify → [0 1 0 2 1] |
| Word | Effect | Example |
|---|---|---|
ok |
x → x 'ok tagged | 42 ok → 42 'ok tagged |
no |
x → x 'no tagged | "err" no → "err" 'no tagged |
tag |
x 'sym → tagged | 1 'foo tag → 1 'foo tagged |
then |
tagged body → tagged | 42 ok (inc) then → 43 'ok tagged |
default |
tagged fallback → value | 42 ok -1 default → 42 |
| Word | Effect | Example |
|---|---|---|
fneg |
f → -f | 3.0 fneg → -3.0 |
fabs |
f → |f| | -2.5 fabs → 2.5 |
frecip |
f → 1/f | 4.0 frecip → 0.25 |
fsign |
f → -1.0/0.0/1.0 | -3.0 fsign → -1.0 |
fclamp |
lo hi f → clamped | 1.0 10.0 5.0 fclamp → 5.0 |
lerp |
a b t → interpolated | 0.0 10.0 0.5 lerp → 5.0 |
| Word | Value |
|---|---|
pi |
3.14159265... |
tau |
6.28318530... |
e |
2.71828182... |
Two clocks, because they answer different questions and neither substitutes for the other.
| Word | Effect | Example |
|---|---|---|
millis |
→ milliseconds | millis → 1837402 |
datetime |
→ 9-element list | datetime → [2026 6 25 19 59 9 6 205 1] |
millis is monotonic and has no epoch — only differences between two reads mean anything. Use it for timing.
datetime is local wall clock, already broken down, in this order:
| # | field | range |
|---|---|---|
| 0 | year | full, e.g. 2026 |
| 1 | month | 0–11 |
| 2 | day | 1–31 |
| 3 | hour | 0–23 |
| 4 | minute | 0–59 |
| 5 | second | 0–60 (60 is a leap second) |
| 6 | day of week | 0–6, 0 = Sunday |
| 7 | day of year | 0–365 |
| 8 | is DST | 0 or 1 |
Fields are broken down in C because day-of-week and DST need the timezone database, which a Slap program cannot reach. The order is exactly Varvara's Datetime ports, which is what examples/uxn.slap indexes it as.
String primitives plus library helpers. Strings are lists of Unicode codepoints; utf8-encode/utf8-decode convert to/from UTF-8 byte lists. Higher-level helpers (int-str, str-join, crlf, http-request) live in examples/lib/strings.slap — cat it with your program: cat examples/lib/strings.slap myprog.slap | slap.
| Word | Effect | Example |
|---|---|---|
str-find |
haystack needle → index ok or none |
"hello world" "world" str-find must → 6 |
str-split |
str delim → list of substrings | "a,b,c" "," str-split → ["a" "b" "c"] |
utf8-encode |
codepoints → bytes | |
utf8-decode |
bytes → codepoints |
From examples/lib/strings.slap:
| Word | Effect | Example |
|---|---|---|
str-join |
parts sep → joined | ["a" "b" "c"] "," str-join → "a,b,c" |
int-str |
n → decimal string | 42 int-str → "42" |
crlf |
→ "\r\n" as byte list |
|
http-request |
method host path headers body → request-bytes |
| Word | Effect | Example |
|---|---|---|
byte-mask |
n → n & 0xFF | 300 byte-mask → 44 |
byte-bits |
byte → 8-bit list | 5 byte-bits → [0 0 0 0 0 1 0 1] |
bits-byte |
8-bit list → byte | [0 0 0 0 0 1 0 1] bits-byte → 5 |
chunks |
list n → sublists of size n | [1 2 3 4 5 6] 2 chunks → [[1 2] [3 4] [5 6]] |
Decoders/encoders for compact binary formats. These live in examples/lib/ as loadable libraries (not prelude) — cat the file with your program: cat examples/lib/icn.slap myprog.slap | ./slap. Each pairs a *-decode/*-encode that round-trip with the corresponding byte layout. Useful for tile graphics, tilemaps, fonts, and lightweight compression.
| File | Format |
|---|---|
examples/lib/icn.slap |
1-bit 8×8 tiles |
examples/lib/chr.slap |
2-bit 8×8 tiles (two planes) |
examples/lib/nmt.slap |
Nametable cells (addr + color per 3 bytes) |
examples/lib/tga.slap |
Uncompressed true-color TGA images |
examples/lib/gly.slap |
ASCII-inline 1-bit glyphs |
examples/lib/ufx.slap |
Proportional bitmap fonts (requires icn.slap) |
examples/lib/ulz.slap |
LZ-compressed byte stream (decode only) |
examples/lib/parse.slap |
parse-int/parse-float/parse-exact/parse-spaces/parse-while/parse-until |
examples/lib/xml.slap |
Elm-style XML decoder |
examples/lib/rss.slap |
RSS/Atom feed parser (requires xml.slap) |
examples/lib/json.slap |
Elm-style JSON decoder (requires parse.slap and strings.slap for int-str) |
examples/lib/strings.slap |
crlf, int-str, str-join, http-request (formerly prelude) |
Built on tcp-connect/tcp-send/tcp-recv/tcp-close primitives plus parse-http. http-request lives in examples/lib/strings.slap.
| Word | Effect |
|---|---|
parse-http |
raw bytes → status headers body |
http-request |
method host path headers body → request-bytes (from strings.slap) |
Build with make slap-sdl. Opens a 640x480 canvas with 2-bit grayscale (4 shades: 0=black, 1=dark, 2=light, 3=white).
| Word | Effect |
|---|---|
clear |
Fill canvas with color (0-3) |
pixel |
x y color pixel — set one pixel |
fill-rect |
x y w h color fill-rect — fill a rectangle |
on |
'event (handler) on — register event callback |
show |
(render) show — start event loop with render function |
| Event | Stack on callback |
|---|---|
tick |
frame-count |
keydown |
SDL keycode |
keyup |
SDL keycode |
mousedown |
x y |
mouseup |
x y |
mousemove |
x y |
160 'W let 120 'H let W H mul 'N let 4 'S let
-- wrapped cell lookup; the grid stays let-bound and nth reads it by symbol
(H plus H mod W mul swap W plus W mod plus nth must) 'cell let
('cy let 'cx let 'gs let
gs cx 1 sub cy 1 sub cell
gs cx cy 1 sub cell plus
gs cx 1 plus cy 1 sub cell plus
gs cx 1 sub cy cell plus
gs cx 1 plus cy cell plus
gs cx 1 sub cy 1 plus cell plus
gs cx cy 1 plus cell plus
gs cx 1 plus cy 1 plus cell plus
) 'neighbors let
('g let list 0
(dup N lt) (
dup W divmod 'y let 'x let
'g x y neighbors 'n let
'g over nth must 1 eq (n 2 eq n 3 eq or) (n 3 eq) if
(1) (0) if
swap (push) dip 1 plus
) while drop
) 'step let
list N (2 random push) repeat
'tick (drop step) on
('sg let 0 clear
0 (dup N lt) (
dup 'i let
'sg i nth must 1 eq (i W mod S mul i W div S mul S S 3 fill-rect) () if
1 plus
) while drop
) show
52 Project Euler solutions in examples/euler/:
-- Euler #1: sum of multiples of 3 or 5 below 1000
1 1000 range
(dup 3 mod 0 eq swap 5 mod 0 eq or) filter
sum
print -- 233168
-- Euler #6: sum-square difference for 1-100
1 101 range dup
(sqr) map sum 'sum-of-sq let
sum sqr 'sq-of-sum let
sq-of-sum sum-of-sq sub print -- 25164150
Interactive SDL demos in examples/:
| File | Description |
|---|---|
life.slap |
Conway's Game of Life with mouse drawing |
flock.slap |
Boids flocking with mouse attraction and predator |
ant.slap |
Langton's ant cellular automaton |
snake.slap |
Snake game with arrow key controls |
chip8.slap |
CHIP-8 emulator: runs real ROMs, 16-key hex keypad, sound-timer border flash |
uxn.slap |
Uxn/Varvara emulator: full 32-opcode CPU with all mode flags, System/Console/Screen/Controller/Mouse |
maze.slap |
Aldous-Broder maze generation and a BFS solve, both on one flat int list |
raycast.slap |
Grid raycaster: DDA per screen column, wall slices drawn as 1-pixel fill-rects |
dots.slap, fish.slap, gradient.slap, zoom.slap |
More graphics demos |
make slap-sdl
./slap-sdl roms/pong.ch8 < examples/chip8.slap # or no arg for the built-in demo ROM
./slap --headless < examples/chip8.slap # run the opcode self-test (no SDL needed)
./slap-sdl game.rom < examples/uxn.slap # likewise for the uxn emulatorThe whole machine — 4 KB of memory, registers, call stack, keypad, and the 64×32 display — is one flat int list threaded on the stack, with no boxes. set is an in-place O(1) store and peek an O(1) non-consuming read, so a cycle decodes and executes one instruction against the live state without ever copying it. That is what keeps the per-cycle cost independent of how big the machine is. The full opcode set (including the COSMAC shift/FX55/FX65 quirks and DXYN sprite collision) is covered by an in-language self-test that runs on the plain terminal build.
uxn.slap is the same idea at 16× the scale: a 220191-cell machine holding uxn's full 64 KB address space, both 256-byte stacks, the device page, and two 320×240 screen layers. All 32 base opcodes are written once each — keep, return and short modes are handled by six state cells set during decode, so one ADD body serves all eight encodings. Varvara pixels are already 2-bit palette indices, which is exactly the canvas depth, so nothing is lost but hue; the palette is mapped by rank rather than absolute luminance so that four shades of one colour stay distinguishable. Roughly 290k instructions/sec. Audio and File are stubbed — the file header says why.
It is checked against another implementation rather than only against itself. ./slap --headless game.rom [frames] < examples/uxn.slap boots a ROM with no window and writes the composited canvas to stdout as one palette index per pixel, with the four palette entries on a PAL line. Run against the nine ROMs in mkeeter/raven's snapshot suite — each at its own resolution, since the canvas geometry is six constants — eight match raven's reference renders pixel for pixel, including screen_blending (every blend mode × depth × flip) and mandelbrot (108864 pixels of pure integer arithmetic). The ninth, piano, differs by 22 pixels: an audio level meter, which is the missing Audio device showing through.
make test-uxn-refs runs that comparison. It is kept out of make test because it downloads the ROMs and reference renders from GitHub and the suite has to work offline; they are cached under tests/.uxn-refs/, so only the first run needs network. This is worth running after any change to the Screen path — uxn.slap's own ~110-assertion self-test passed while three real Screen bugs were live, and whole-frame comparison found all three in an afternoon.
make test-uxn-sweep asks a question the comparison cannot: does each ROM's render move with the frame count the way it should? Rendering at 1, 60 and 120 separates a ROM that is right from one that is right at exactly 60 because two errors cancelled there. Seven of the nine are byte-identical from 1 frame to 240 and must stay that way — a static ROM that starts drifting is state surviving between Screen-vector calls. screen and audio genuinely animate and must keep doing so; screen shows 0 diffs at 60, 16 at 59, and 610 at 61.
make bench-uxn reports throughput. The dump prints an INS line counting instructions retired in the frame loop, and each ROM is timed twice — at 0 frames and at N — so the fixed boot cost drops out. screen.rom sustains roughly 83k uxn instructions/sec (~26k per frame at 256×176) and drool.rom about 169k (~21k per frame at 320×240); the gap is sprite blitting versus plain compute. screen is the headline because it is also pixel-exact, so its number is timing work that is known correct — drool has no reference render at all, and the harness keeps it out of the comparison and the sweep rather than pretending otherwise.
bunnymark, the obvious choice, is not usable. Its RNG is a self-modifying xorshift seeded from the Datetime device, and zero is xorshift's fixed point, so before Datetime existed the generator emitted 0 forever and no bunny ever spawned. Implementing Datetime fixed the RNG — the seed literal now boots nonzero and the render animates instead of sitting frozen — but the population still never exceeds one bunny and the ROM still idles, now at 441 instructions/frame instead of 425. It installs no Mouse vector, so the harness has no lever on the spawn path; the remaining blocker is unidentified.
App demos (terminal build):
| File | Description |
|---|---|
wiki.slap |
HTTP wiki server: browse, edit, and link pages stored as flat text files |
kv-server.slap + kv-client.slap |
Persistent key/value store over TCP with a one-shot CLI client |
serve.slap |
Static file HTTP server: directory listings, MIME by extension, path-traversal refusal |
fetch.slap |
A small curl. Builds the request with http-request and reads the reply with parse-http |
feed.slap |
RSS 2.0 and Atom reader — the consumer for xml.slap and rss.slap |
todo.slap |
Todo list in a JSON file, decoded with jd-* and written with je-* |
banner.slap |
Text as ASCII art from a real .uf1 bitmap font, via icn.slap |
plasma.slap |
Writes a 24-bit TGA. Float-only: there is no sin, so it builds one |
cat examples/lib/strings.slap examples/lib/parse.slap examples/wiki.slap | ./slap 8080 examples/wiki-pages
# then open http://localhost:8080/The wiki serves one blocking connection at a time (HTTP/1.0, connection-close), parses requests with parse.slap combinators, escapes all user content, and rejects page names that could escape the pages directory. [PageName] in a page body becomes a link.
# terminal 1: start the store (port, snapshot file)
cat examples/lib/strings.slap examples/lib/parse.slap examples/kv-server.slap | ./slap 4321 kv.snap
# terminal 2: talk to it
cat examples/lib/strings.slap examples/lib/parse.slap examples/kv-client.slap | ./slap 4321 set greeting hello world
cat examples/lib/strings.slap examples/lib/parse.slap examples/kv-client.slap | ./slap 4321 get greeting # -> VALUE hello world# a static file server, and the repo's own client fetching from it
cat examples/lib/strings.slap examples/lib/parse.slap examples/serve.slap | ./slap 8080 .
cat examples/lib/strings.slap examples/lib/parse.slap examples/fetch.slap | ./slap 127.0.0.1 8080 /readme.md -i
# an RSS/Atom digest, and a todo list in JSON
cat examples/lib/strings.slap examples/lib/parse.slap examples/lib/xml.slap \
examples/lib/rss.slap examples/feed.slap | ./slap examples/feeds/sample.xml
cat examples/lib/strings.slap examples/lib/parse.slap examples/lib/json.slap \
examples/todo.slap | ./slap todo.json add buy milk
# a bitmap font as ASCII art, and a plasma written out as a real TGA
cat examples/lib/strings.slap examples/lib/icn.slap examples/banner.slap | ./slap SLAP
cat examples/lib/strings.slap examples/lib/parse.slap examples/lib/tga.slap \
examples/plasma.slap | ./slap plasma.tga 64serve.slap decodes the request target before testing it, so %2e%2e and .. are the same string by the time the rule sees them, and the rule is a byte allowlist rather than a denylist. fetch.slap is the one place parse-http is used as designed: it reads a response, where the number after the first space is the status — hand it a request line and it reports 0 and loses the method and path, which is why both servers here parse requests by hand.
feed.slap and todo.slap are the first consumers of xml.slap/rss.slap and json.slap; until now those 1500 lines only ever ran their own self-tests. Both branch with must and each rather than case, then or default, which is a habit from when those three staged the whole scrutinee through a 16384-slot buffer and would have capped the file size; case unwraps in place now, so the habit costs nothing and buys nothing. The parse is still capped, but further out: 16861 bytes of the shape tests/run_feed.py builds parses and 17008 does not, and that test pins the boundary from both sides.
The store keeps its data in a dict threaded on the stack and persists to a flat key<TAB>value snapshot on SAVE/SHUTDOWN. The protocol is one LF-terminated command per connection (SET/GET/DEL/KEYS/SAVE/PING/SHUTDOWN); malformed input, dead peers, and an unwritable snapshot are all reported without taking the single-threaded server down, while a corrupt snapshot is refused loudly at boot rather than silently pruned.
Slap has no import statement. A program is whatever you pipe into slap, so you compose files by concatenating them:
cat lib.slap main.slap | slapOrder matters — definitions must appear before use. This works for remote libraries too:
curl -s https://example.com/lib.slap | cat - main.slap | slapOr pre-fetch and cache:
curl -so lib.slap https://example.com/lib.slap
cat lib.slap main.slap | slapThe type checker runs on all code (builtins, prelude, user) before execution. It catches the following at compile time:
Linear resources (boxes):
- A box must be consumed exactly once via
free,lend,mutate, orclone. - Embedding a box into a stackable container (list, tuple, record, tagged) is rejected.
- Tagging a box keeps it linear, so
42 box 'x tagcannot then be dropped, duplicated, pushed or inserted.42 box ok must freeis the legitimate shape and still works. - A Box binding is single-use across all its lookups: the name may be read as many times as you like, but only one read may reach a word that retires the cell.
lendandmutatehand the same box back, so freeing what they return consumes the binding too;swapmerely moves it and does not. - Duplicating a box with
dupis rejected (boxes aren't copyable). - A closure that captures a linear outer binding is marked linear itself — applying it twice is rejected.
- Higher-order ops (
each,fold,while,find) reject bodies that capture linear outer bindings. composepropagates linear-capture: merging a linear-capturing closure with a pure one yields a linear-capturing result.- A linear-capturing closure cannot recurse on itself — each recursive call would re-consume the capture.
Aliasing through lend/mutate:
lend's body may notlet-bind the snapshot when the box contains a box or a dict — those are copied by pointer, so a latermutatefrees the contents while the binding still points at them. Boxed lists, records, tuples and tagged values are copied bitwise and are safe to bind.- Indexed read access via
'name k nthork peekis exempt — neither pulls the list to the stack as a binding.
Tagged unions:
- Tags are open by default;
caseon an untyped tagged value accepts any variant with a default clause. - Declaring a closed schema via
{'sym 'type ...} unionenables exhaustiveness:casemust cover every variant (hard-errored when any variant carries a linear payload, soft-warned otherwise). eitherin effect annotations ({'ok int 'no str} either) validatestagemission sites: the body may only emit variants named in the schema.
Protocols (typeclasses):
ord(lt,sort) accepts int and float only. Symbols are Eq but not Ord — ordering symbols by intern id is an implementation accident, not a semantic.num,integral,seq,semigroup,sized,functor,monadgate other ops.
Effect annotations:
(body) [sig] effect 'name letvalidates the body's stack shape against the declared signature.- Forward declarations
'name [sig] effectreconcile with the body whennameis later defined.
What the type system does not catch:
- Division by zero, modulo by zero, out-of-bounds
set,nth, orpeek(runtime panics). - Non-exhaustive
caseon a tagged value with nouniondeclaration — the default clause silently fires on any unmatched variant (this is by design; declare a union to opt into exhaustiveness). - Correctness of effect-annotation schemas (the user is trusted when they write
[sig] effect; only the body's shape is validated, not its meaning). - Recursion depth, memory limits, or other runtime resource exhaustion.
make testRuns:
make check-refs— every file the build and docs reference actually existscat examples/lib/strings.slap examples/lib/parse.slap tests/expect.slap | ./slap— 640+ integration assertions./slap --check < tests/type.slap+./slap < tests/type.slap— type system validation- Same expect.slap stream re-run under
--check python3 tests/run_panic.py+python3 tests/run_type_errors.py— expected error messagesargshandling, with and without positional argumentspython3 tests/run_euler.py— 52 Project Euler solutions (strings.slap prepended)python3 tests/run_wiki.py,run_kv.py,run_feed.py,run_todo.py,run_serve.py,run_codec.py— the app demos, each really booted or really driven: servers over a socket on a random port, CLIs against a real file, the codecs checked from outside Slap against the font file and the TGA spec- The headless self-tests for
chip8.slap,uxn.slap,maze.slapandraycast.slap— SDL words type-check unconditionally andhaltfires before any of them dispatch, so none of these need an SDL build - Loadable libraries under
examples/lib/— each run and type-checked in the combos it's designed for bash tests/adversarial/run.sh— adversarial probes
Adversarial probes classify each probe as TYPECHECK_REJECT / PANIC / CLEAN_RUN and compare against a declared expectation. The point is that a classification never silently downgrades: a probe that slides from TYPECHECK_REJECT to CLEAN_RUN means the checker went blind. Probes marked -- KNOWN-GAP: are cases the docs claim are caught statically but currently are not; they print on every run.
Requires only a C99 compiler and -lm. No dependencies beyond SDL2 for the graphics build.
make slap # terminal interpreter
make slap-sdl # SDL2 graphics build (requires SDL2)
make slap-wasm FILE=examples/life.slap # Emscripten/WASM build (requires emcc)
make clean # remove binaries and generated pagesslap-wasm writes <name>.html, <name>.js, and <name>.wasm, wrapping the program in shell.html (a 640×480 canvas; SLAP_NAME is substituted with the program name). It needs emcc on PATH — brew install emscripten, or install emsdk.
The build passes -sGROWABLE_ARRAYBUFFERS=0. With ALLOW_MEMORY_GROWTH, emscripten ≥6 backs the heap with a resizable ArrayBuffer, and browsers' TextDecoder.decode() rejects those — startup throws and the canvas stays blank.



