A complete R7RS-small Scheme implementation, written in Zig .
Kaappi implements every identifier from R7RS Appendix A — 641 built-in procedures, 32 syntax forms, and all 14 standard libraries — plus 171 SRFIs, a C FFI, OS threads and fibers, an LLVM native-code backend, a package manager, and a stepping debugger. The runtime is a register-based bytecode VM with generational garbage collection and stack-copying first-class continuations.
The name is Malayalam and Tamil for coffee — see the FAQ for the story.
Note: Kaappi was built with the assistance of AI (Claude by Anthropic).
No install needed — run Scheme in your browser at the playground , or take the guided 12-lesson tour .
Install script (macOS, Linux, FreeBSD, OpenBSD, NetBSD) curl -fsSL https://kaappi-lang.org/install.sh | bash This installs kaappi and thottam (the package manager) to ~/.local/bin/ and the standard libraries to ~/.kaappi/lib/ , verifying SHA256 checksums along the way. On the BSDs the script works from the base system alone — when neither curl nor wget is installed it falls back to the base fetch (FreeBSD) or ftp (OpenBSD, NetBSD) for downloads and sha256 for verification.
Prebuilt binaries for every platform are on the releases page . macOS binaries are Developer ID signed and notarized; all releases ship SHA256SUMS with a GPG signature ( SHA256SUMS.asc , key at keybase.io/baijum ). See the download page for manual install and verification steps.
Requires Zig 0.16+ and a C toolchain (for the vendored linenoise library):
git clone https://github.com/kaappi/kaappi.git cd kaappi zig build # → zig-out/bin/kaappi zig build run # launch the REPL zig build run -- program.scm # run a Scheme file zig build test # run the unit tests Supported platforms OS Architecture Build Tests Native compilation macOS aarch64 (Apple Silicon) yes yes LLVM backend Linux x86_64 yes yes LLVM backend Linux aarch64 yes yes LLVM backend Linux riscv64 yes yes interpreter only Linux s390x (big-endian) yes yes interpreter only Linux ppc64le yes yes interpreter only Windows aarch64 (ARM64), x86_64 yes yes LLVM backend (needs a C toolchain) FreeBSD x86_64, aarch64 yes yes LLVM backend (base cc suffices) OpenBSD x86_64, aarch64 yes yes LLVM backend (base cc suffices) NetBSD x86_64, aarch64 yes yes LLVM backend (needs pkgsrc clang ; base cc is GCC) WebAssembly wasm32-wasi yes — interpreter only The WASM build ( zig build wasm ) runs in browsers and WASI runtimes — it powers the playground .
The Windows port ( zig build -Dtarget=aarch64-windows or -Dtarget=x86_64-windows ) covers the full interpreter — REPL (plain line editing, no history/completion), fibers, channels, OS threads, FFI ( LoadLibrary ), and the kaappi test runner. thottam installs packages on Windows too (with Git for Windows on PATH); only manifests with a build: command are refused — the C-FFI packages' Makefiles target POSIX. Platform differences: fd readiness covers sockets (event-driven, WSAEventSelect) and pipes (polled) — file ports keep blocking reads (timers and cross-thread wakeups always work) — and the POSIX-only slice of SRFI-170 (uid/gid, symlinks, chmod/umask, user/group info) raises a catchable file error. cond-expand distinguishes the platforms: Windows builds expose the windows feature identifier instead of posix .
The FreeBSD port ( zig build -Dtarget=x86_64-freebsd or aarch64-freebsd ) is full POSIX with no degradations: kqueue-backed fiber I/O, OS threads, complete SRFI-170, the full linenoise REPL, and thottam with build: support. kaappi compile links native binaries with the base system's cc — no extra toolchain needed.
The OpenBSD port ( zig build -Dtarget=x86_64-openbsd or aarch64-openbsd ) is the same full-POSIX kqueue platform — fiber I/O, threads, complete SRFI-170, the full REPL, build: support, and native compilation with base cc . Two accommodations for OpenBSD's hardening, both automatic: each binary is marked PT_OPENBSD_NOBTCFI at build time to opt out of BTCFI enforcement (Zig 0.16 emits no BTI landing pads), and the interpreter raises its own stack limit at startup to clear OpenBSD's tight 4 MiB default. See docs/dev/openbsd.md .
The NetBSD port ( zig build -Dtarget=x86_64-netbsd or aarch64-netbsd ) completes the BSD trio — the same full-POSIX kqueue feature set, verified on NetBSD 10.1. The runtime binds NetBSD's versioned libc symbols explicitly ( __kevent50 , __opendir30 , __getpwnam50 — the plain names are old-ABI compat symbols that silently misparse modern structs) and resets the aarch64 FPCR at startup, which NetBSD boots in flush-to-zero mode that would break IEEE gradual underflow. The native backend ( kaappi compile ) needs clang from pkgsrc — NetBSD's base cc is GCC, which can't consume LLVM IR. See docs/dev/netbsd.md .
$ kaappi kaappi> (define (fib n) ... (if (< n 2) n ... (+ (fib (- n 1)) (fib (- n 2))))) kaappi> (fib 20) 6765 kaappi> (map (lambda (x) (* x x)) '(1 2 3 4 5)) (1 4 9 16 25) kaappi> `(the answer is ,(* 6 7)) (the answer is 42) kaappi> (string-length "héllo") 5 kaappi> (char-alphabetic? #\λ) #t The REPL has syntax highlighting , line editing , persistent history ( ~/.kaappi/history ), tab completion for all built-in and user-defined symbols, and multi-line input with automatic paren balancing.
( define-syntax my-when ( syntax-rules () ((my-when test body ...) ( if test ( begin body ...))))) (my-when #t ( display " hello world " ) (newline)) Libraries (define-library (mylib math) (export square cube) (import (scheme base)) ( begin ( define ( square x ) ( * x x)) ( define ( cube x ) ( * x x x)))) (import (mylib math)) (cube 5 ) ; => 125 First-class continuations ( define saved #f) ( + 1 ( call/cc ( lambda ( k ) ( set! saved k) 10))) ; => 11 (saved 42 ) ; => 43 Features Complete R7RS-small Proper tail calls — (define (loop n) (loop (+ n 1))) runs forever without growing the stack First-class continuations — multi-shot call/cc via stack copying, dynamic-wind for cleanup Exception handling — guard , raise , with-exception-handler , typed error objects ( file-error? , read-error? ) Hygienic macros — syntax-rules with scope-based renaming; pattern variables, ellipsis, literals, underscore wildcards Library system — define-library , import with only / except / rename / prefix , .sld file loading, cond-expand Numeric tower — fixnum, bignum (arbitrary precision), exact rational, flonum (IEEE 754 f64), complex; automatic promotion on overflow Full Unicode — UTF-8 strings indexed by codepoint, Unicode character classification and case mapping Records, ports, lazy evaluation, multiple values, parameters — the whole standard, with no known functional gaps Beyond the standard 171 SRFIs — 12 built-in, 156 as portable .sld libraries, plus SRFI 261 portable library references ( (srfi srfi-1) , (srfi lists-1) ) resolved in the importer and SRFI 226/160 as sub-libraries only (full list in CONFORMANCE.md ) Native binaries — kaappi compile program.scm -o program compiles Scheme to a native executable via LLVM, with self-tail-calls compiled as loops ( details ) Standalone bundles — zig build -Dbundle-src=program.scm embeds bytecode + libraries in a single executable C FFI — call shared libraries from Scheme via (kaappi ffi) ; 18 marshalled types, callbacks for passing Scheme procedures to C Concurrency — green threads with channels via (kaappi fibers) , plus real OS threads via SRFI-18 Stepping debugger — breakpoints (with conditions), watch expressions, step/next/step-out, frame navigation, locals — all from the REPL Profiler — kaappi --profile or ,profile expr : per-function self/total time, call counts, allocation bytes Sandbox mode — kaappi --sandbox blocks FFI, file I/O, eval , load , and environment access Bytecode caching — compiled .sbc files are reused when the source is unchanged Machine-legible diagnostics — every error carries a stable KP code ( error[KP3001] ), with --diagnostics=json (LSP shape), kaappi explain <code> , and a Scheme accessor (error-object-code e) in (kaappi diagnostics) for dispatching on codes ( details ) Capability discovery — kaappi features [--json] reports this build's version, target, compiled-in subsystems, SRFIs, and limits from one source of truth ( details ) Editor support — a bundled LSP server ( kaappi-lsp ) and a VS Code extension Ecosystem Kaappi ships thottam , a package manager for its growing library ecosystem:
# Install the web framework (auto-installs kaappi-http, kaappi-json, kaappi-net) thottam install kaappi-web # Now it just works — no --lib-path flags needed kaappi app.scm Package Description kaappi-net TCP/TLS networking kaappi-http HTTP/HTTPS client + server (pre-fork, threaded) kaappi-web Web framework — routing, middleware, JSON helpers kaappi-json JSON parser and serializer kaappi-pg PostgreSQL client with cursors and type conversion kaappi-redis Redis client — lists, hashes, pub/sub, pipelining kaappi-examples REST API, task queue, CRUD app, file server More libraries (CSV, TOML, YAML, logging, templates, testing, crypto, SQLite, email, CLI parsing) are listed in the ecosystem docs .
thottam install <pkg> resolves dependencies, supports version constraints ( thottam install kaappi-net@">=0.2.0" ), and installs to ~/.kaappi/lib/ where libraries are discovered automatically.
(import (kaappi web) (kaappi pg) (kaappi json)) ( define db (pg-connect " dbname=myapp " )) ( define app (routes (GET " /users/:id " ( lambda ( req params ) ( let ((rows (pg-query db " SELECT * FROM users WHERE id = $1 " (param/number params " id " )))) (json-response ( if ( null? rows) ' (( " error " . " not found " )) ( car rows)))))) (POST " /users " ( lambda ( req params ) ( let ((body (request-json req))) (pg-exec db " INSERT INTO users (name) VALUES ($1) " ( cdr ( assoc " name " body))) (json-response ' (( " created " . #t )) 201 )))))) (serve (wrap app wrap-json-body wrap-logging wrap-errors) 8080 ) Concurrency Green threads (fibers) for cooperative multitasking within one OS thread:
(import (kaappi fibers)) ( define ch (make-channel)) (spawn ( lambda () (channel-send ch " hello from fiber " ))) ( display (channel-receive ch)) ; => hello from fiber Scheduling is cooperative: spawned fibers run when the main program blocks ( channel-receive on an empty channel, fiber-join ) or calls (yield) . A fiber that blocks on an empty channel is parked and woken by the next channel-send on that channel. When the main program ends, fibers that are still parked (e.g. workers that never received a stop sentinel) are simply discarded and the process exits — like goroutines in Go. If the main program blocks on a channel that no runnable or parked-and-wakeable fiber can ever send to, channel-receive raises a deadlock error (an error object, catchable with guard ); the same applies to fiber-join on a fiber that can never complete.
Real OS threads via SRFI-18 — each thread gets its own VM and GC, enabling true parallel I/O (e.g., thread-per-connection servers):
(import (srfi 18 )) ( define t (thread-start! (make-thread ( lambda () ( display " running on OS thread " ) (newline))))) (thread-join! t) Architecture Source → Reader → Expander → IR → Bytecode emission → VM (UTF-8 (syntax- (analysis + (register- (generational GC, lexer) rules) optimization based) stack-copied passes) continuations) Component Role Reader Tokenizer + recursive descent parser for the full R7RS lexical syntax, including Unicode identifiers and #\λ character literals. Expander syntax-rules pattern matching and hygienic template instantiation. IR Tree-structured intermediate representation (33 node types) with analysis passes (tail positions, primitives, constants) and optimization passes (constant folding, dead-branch elimination, and more). Compiler IR → register-based bytecode. VM Bytecode interpreter with growable register file and frame stack, exception handler and dynamic-wind stacks, stack-copying continuations, and a stepping debugger. GC Generational collector (young/old) with write barrier for old→young references. V
Hacker News
news.ycombinator.com