WALC /wɑlts/ compiles stand-alone modules in WebAssembly into pure closed untyped lambda expressions.
The input modules are only allowed to use custom WALC functions to input a byte, output a byte, and exit, see example programs written in Rust.
The output lambda expressions are in human-readable WALC format, which just uses square brackets instead of the lambda symbol. There is even a one-line script to convert it to the standard mathematical notation.
All lambda calculus semantics and purity is preserved. In order to perform I/O, the interpreter decodes the program as an I/O command, executes the command, supplies encoded user input if needed, and repeats again.
See example interpreters written in Lua and TypeScript in under 300 LOC or in C in under 850 LOC that are optimized for running lambda calculus for a long time with stable speed and reasonable memory consumption.
You can run some example lambda expressions with:
interp/lambda.ts examples/walc/hello.walcYou might also utilize overview notes as a starting point for digging into the codebase.
Enjoy!
cargo run -- INPUT.wasm -o OUTPUT.walcor install it globally:
cargo install --path .
walc INPUT.wasm -o OUTPUT.walcExample Rust programs are here.
-
Install the WASM toolchain for Rust:
rustup target add wasm32v1-none
You can also experiment with the standard
wasm32-unknown-unknowntoolchain, even though its feature set is unstable and in the future it might extend beyond what WALC supports:rustup target add wasm32-unknown-unknown
-
Build for release. You can use the provided Makefile that will tell Cargo to also install all the
.wasmfiles into theexamples/rust/bindirectory:make -C examples/rust
Or, for the
wasm32-unknown-unknowntarget:make -C examples/rust TARGET=wasm32-unknown-unknown
Using the C interpreter:
mkdir bin
walc examples/rust/bin/mandelbrot.wasm -o bin/mandelbrot.walc
gcc tools/lambda.c -o bin/lambda -O3
tools/text2bin.ts bin/mandelbrot.walc -o bin/mandelbrot.bin
bin/lambda bin/mandelbrot.binUsing the TypeScript or Lua interpreters:
mkdir bin
walc examples/rust/bin/mandelbrot.wasm -o bin/mandelbrot.walc
tools/lambda.ts bin/mandelbrot.walc
# or:
tools/lambda.lua bin/mandelbrot.walcJust for comparison, here is some approximate performace data from running on my machine:
| Interpreter | Compiler/Runtime | Execution time | Peak memory usage |
|---|---|---|---|
| lambda.c 1.0 | GCC 13.3 (-O3) | 4 min | 75 MB |
| lambda.ts 1.0 | Deno 2.7 | 15 min | 400 MB |
| lambda.lua 1.0 | LuaJIT 2.1 | 106 min (*) | >900 MB |
(*) Lua execution time is extrapolated from running half of the program for 53 min. 🤷
While this might seem underwhelming, note that the interpreter was not the main focus of this project and it took quite a bit of optimization to achieve even this performance. I would love to hear about more efficient approaches! 🧑🔬 Who knows, maybe graph reduction techniques or conversion to combinatory calculus might do a 10x speedup. Or sophisticated compiler optimizations?
Output:
..............................:::::!?:!!:............
...............................:::::::!?@!:::::............
..............................:::::::?@@@@@@?!::::::...........
.............................::::::::::?@@@@@@@!:::::::::..........
..........................:::::??@!::@@??@@@@@@@??!@:::::@::.........
......................::::::::::::@@@@@@@@@@@@@@@@@@@@@?@@@@!::..........
..................:::::::::::::::?!@@@@@@@@@@@@@@@@@@@@@@@@@!::::..........
...............::!:::::::::::::::?@@@@@@@@@@@@@@@@@@@@@@@@@@@@::::...........
............::::::@!!!:!@!:::::!?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@::...........
..........::::::::::?@@@@@@@@@?!?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@??::............
........::::::::::!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?:::............
..:...:::::::::!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?::::............
:?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?!:::::............
..:...:::::::::!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?::::............
........::::::::::!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?:::............
..........::::::::::?@@@@@@@@@?!?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@??::............
............::::::@!!!:!@!:::::!?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@::...........
...............::!:::::::::::::::?@@@@@@@@@@@@@@@@@@@@@@@@@@@@::::...........
..................:::::::::::::::?!@@@@@@@@@@@@@@@@@@@@@@@@@!::::..........
......................::::::::::::@@@@@@@@@@@@@@@@@@@@@?@@@@!::..........
..........................:::::??@!::@@??@@@@@@@??!@:::::@::.........
.............................::::::::::?@@@@@@@!:::::::::..........
..............................:::::::?@@@@@@?!::::::...........
...............................:::::::!?@!:::::............
WALC supports:
- WebAssembly 1.0 (pdf), the WWW standard released in 2019
- Linear Memory 1.0 extensions
WALC does not support:
-
Dynamic type checking and bounds checking.
Only division by zero and signed division overflow are checked. Other checks are ignored for efficiency, even though this is non-compliant behavior.
-
Floating-point arithmetic.
Given the scope of the project, there is simply no point in implementing floats.
To avoid as much compilation problems as possible, floats are stored as integers. Reinterpreting conversions between floats and integers are replaced with nops and all other operations are replaced with traps. This behavior might be useful when you use a standard function like
printfthat can use floats internally, but your program never invokes it with any float values.