Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

147 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FontBook

Game of Life Boids flocking Langton's ant

slap

A stack-based programming language with static type inference and linear types. Single-file C99 interpreter (~3000 lines).

install

brew install surprisetalk/tap/slap

Or build from source:

make slap

quick start

echo '2 3 plus print' | slap        # → 5
slap < examples/euler/1.slap        # → 233168

For the SDL graphics build:

make slap-sdl                        # requires SDL2
slap-sdl < examples/life.slap

CLI:

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 0
  • cwd — pushes current working directory as string

language tour

arithmetic and stack

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)

floats

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

symbols

Atomic identifiers. Prefixed with '.

'hello               -- 'hello
'hello 'hello eq     -- 1
'hello 'world eq     -- 0

booleans

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

definitions

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.

control flow

-- 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

recursion

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

closures

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

composition

(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

data types

lists

-- 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]

tuples

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

records

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

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.

tagged unions (sum types)

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 (linear types)

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

type system

All code is type-checked before execution. Types are inferred — no annotations required.

stackable vs linear

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.

effect annotations

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).

protocol constraints

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.

prelude

~70 definitions in slap itself, loaded at startup.

stack

Word Effect Example
over a b → a b a 1 2 over1 2 1
nip a b → b 1 2 nip2
rot a b c → b c a 1 2 3 rot2 3 1
tuck a b → b a b 1 2 tuck2 1 2
not n → n==0 1 not0
repeat x n f → f^n(x) 1 10 (2 mul) repeat1024

arithmetic

Word Effect Example
inc n → n+1 5 inc6
dec n → n-1 5 dec4
neg n → -n 5 neg-5
abs n → |n| -3 abs3
sqr n → n*n 5 sqr25
cube n → n*n*n 3 cube27
max a b → max 3 5 max5
min a b → min 3 5 min3
sign n → -1/0/1 -3 sign-1
clamp lo hi n → clamped 1 10 5 clamp5

comparison

Word Effect Example
neq a b → a!=b 3 5 neq1
gt a b → a>b 5 3 gt1
ge a b → a>=b 3 3 ge1
le a b → a<=b 3 5 le1

predicates

Word Effect Example
iszero n → n==0 0 iszero1
ispos n → n>0 5 ispos1
iseven n → even? 4 iseven1
isodd n → odd? 3 isodd1
divides a b → b%a==0 3 9 divides1
isbetween n lo hi → in range? 5 1 10 isbetween1

list utilities

Word Effect Example
sum list → total [1 2 3] sum6
product list → product [1 2 3 4] product24
max-of list → max [5 1 3] max-of5
min-of list → min [5 1 3] min-of1
first list → elem [1 2 3] first1
last list → elem [1 2 3] last3
member list val → bool [1 2 3] 2 member1
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) reduce6
keep-mask list mask → filtered [10 20 30] [1 0 1] keep-mask[10 30]

structural utilities

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]

tagged unions

Word Effect Example
ok x → x 'ok tagged 42 ok42 'ok tagged
no x → x 'no tagged "err" no"err" 'no tagged
tag x 'sym → tagged 1 'foo tag1 'foo tagged
then tagged body → tagged 42 ok (inc) then43 'ok tagged
default tagged fallback → value 42 ok -1 default42

float math

Word Effect Example
fneg f → -f 3.0 fneg-3.0
fabs f → |f| -2.5 fabs2.5
frecip f → 1/f 4.0 frecip0.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 fclamp5.0
lerp a b t → interpolated 0.0 10.0 0.5 lerp5.0

constants

Word Value
pi 3.14159265...
tau 6.28318530...
e 2.71828182...

time

Two clocks, because they answer different questions and neither substitutes for the other.

Word Effect Example
millis → milliseconds millis1837402
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 011
2 day 131
3 hour 023
4 minute 059
5 second 060 (60 is a leap second)
6 day of week 06, 0 = Sunday
7 day of year 0365
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.

strings

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 must6
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

bitwise and byte utilities

Word Effect Example
byte-mask n → n & 0xFF 300 byte-mask44
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-byte5
chunks list n → sublists of size n [1 2 3 4 5 6] 2 chunks[[1 2] [3 4] [5 6]]

binary format codecs

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)

networking / http

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)

SDL graphics

Build with make slap-sdl. Opens a 640x480 canvas with 2-bit grayscale (4 shades: 0=black, 1=dark, 2=light, 3=white).

primitives

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

events

Event Stack on callback
tick frame-count
keydown SDL keycode
keyup SDL keycode
mousedown x y
mouseup x y
mousemove x y

example: Game of Life (abridged)

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

examples

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 emulator

The 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 64

serve.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.

libraries

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 | slap

Order matters — definitions must appear before use. This works for remote libraries too:

curl -s https://example.com/lib.slap | cat - main.slap | slap

Or pre-fetch and cache:

curl -so lib.slap https://example.com/lib.slap
cat lib.slap main.slap | slap

what the type system catches

The 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, or clone.
  • Embedding a box into a stackable container (list, tuple, record, tagged) is rejected.
  • Tagging a box keeps it linear, so 42 box 'x tag cannot then be dropped, duplicated, pushed or inserted. 42 box ok must free is 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. lend and mutate hand the same box back, so freeing what they return consumes the binding too; swap merely moves it and does not.
  • Duplicating a box with dup is 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.
  • compose propagates 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 not let-bind the snapshot when the box contains a box or a dict — those are copied by pointer, so a later mutate frees 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 nth or k peek is exempt — neither pulls the list to the stack as a binding.

Tagged unions:

  • Tags are open by default; case on an untyped tagged value accepts any variant with a default clause.
  • Declaring a closed schema via {'sym 'type ...} union enables exhaustiveness: case must cover every variant (hard-errored when any variant carries a linear payload, soft-warned otherwise).
  • either in effect annotations ({'ok int 'no str} either) validates tag emission 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, monad gate other ops.

Effect annotations:

  • (body) [sig] effect 'name let validates the body's stack shape against the declared signature.
  • Forward declarations 'name [sig] effect reconcile with the body when name is later defined.

What the type system does not catch:

  • Division by zero, modulo by zero, out-of-bounds set, nth, or peek (runtime panics).
  • Non-exhaustive case on a tagged value with no union declaration — 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.

testing

make test

Runs:

  1. make check-refs — every file the build and docs reference actually exists
  2. cat examples/lib/strings.slap examples/lib/parse.slap tests/expect.slap | ./slap — 640+ integration assertions
  3. ./slap --check < tests/type.slap + ./slap < tests/type.slap — type system validation
  4. Same expect.slap stream re-run under --check
  5. python3 tests/run_panic.py + python3 tests/run_type_errors.py — expected error messages
  6. args handling, with and without positional arguments
  7. python3 tests/run_euler.py — 52 Project Euler solutions (strings.slap prepended)
  8. 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
  9. The headless self-tests for chip8.slap, uxn.slap, maze.slap and raycast.slap — SDL words type-check unconditionally and halt fires before any of them dispatch, so none of these need an SDL build
  10. Loadable libraries under examples/lib/ — each run and type-checked in the combos it's designed for
  11. 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.

building

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 pages

slap-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 PATHbrew 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.

About

linear concat language

Resources

Stars

53 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages