🥤Walt is JavaScript-shaped syntax over raw wasm. Like WAT, you manage memory by hand, but the code is far easier to look at.
- Template: cart_walt - press "Use this template" and you have a working game.
- Docker image:
ghcr.io/notnullgames/null0-cart-walt:latest - Your code:
cart/main.walt, compiled with walt - Bindings:
carts/walt/null0.walt(a reference list to copy from) - WASI is available, so ordinary file and stdio calls work.
- See carts made with Walt
- API docs for Walt
building
You do not install walt - the docker image has it, and the current null0 bindings, baked in. There is nothing in your project to keep in sync with the engine.
docker run --rm --user $(id -u):$(id -g) \
-v ./cart:/src -v ./webroot:/out \
ghcr.io/notnullgames/null0-cart-walt:latest mygameThe template wraps that in npm start, which also serves the result and rebuilds when you save.
Walt notes
- Imports are per-module: copy the
type+importpair for each function you use. - You need a local memory (
const memory: Memory = { initial: 1 }) even for a plain store. - A
Coloris a *pointer to 4 bytes* you write into memory yourself. - No
malloc, so struct-returning functions do not work on the web host.
a cart, in Walt
This is carts/walt/simple/main.walt in the engine repo - the same file CI builds into the cart below, so it always compiles.
// simple walt cart for null0 (see null0.walt for the full bindings)
//
// Color is a pointer to 4 bytes (r, g, b, a) in memory, and a string is a
// pointer to null-terminated bytes - neither is a packed scalar, so both are
// written into memory below and passed as addresses.
// two pages because everything above lives at 65536, one page in; exported
// because the host reads those pointers out of cart.memory
export const memory: Memory = { initial: 2 };
const BLUE: i32 = 65536; // rgba(0, 121, 241, 255)
const RED: i32 = 65540; // rgba(230, 41, 55, 255)
const WHITE: i32 = 65544; // rgba(255, 255, 255, 255)
const TEXT: i32 = 65548; // "hello from Walt"
type Clear = (i32) => void;
type DrawCircle = (i32, i32, i32, i32) => void;
type DrawText = (i32, i32, i32, i32, i32) => void;
import { clear: Clear, draw_circle: DrawCircle, draw_text: DrawText } from 'null0';
// called on load
export function load(): void {
// walt has no data segments, so the bytes go in at runtime. these stores
// must be inline in a function body - wrapping them in their own function
// triggers a walt-compiler codegen bug (invalid bytecode)
i32.store8(65536, 0);
i32.store8(65537, 121);
i32.store8(65538, 241);
i32.store8(65539, 255);
i32.store8(65540, 230);
i32.store8(65541, 41);
i32.store8(65542, 55);
i32.store8(65543, 255);
i32.store8(65544, 255);
i32.store8(65545, 255);
i32.store8(65546, 255);
i32.store8(65547, 255);
i32.store8(65548 + 0, 104);
i32.store8(65548 + 1, 101);
i32.store8(65548 + 2, 108);
i32.store8(65548 + 3, 108);
i32.store8(65548 + 4, 111);
i32.store8(65548 + 5, 32);
i32.store8(65548 + 6, 102);
i32.store8(65548 + 7, 114);
i32.store8(65548 + 8, 111);
i32.store8(65548 + 9, 109);
i32.store8(65548 + 10, 32);
i32.store8(65548 + 11, 87);
i32.store8(65548 + 12, 97);
i32.store8(65548 + 13, 108);
i32.store8(65548 + 14, 116);
i32.store8(65548 + 15, 0);
clear(BLUE);
draw_circle(100, 100, 50, RED);
draw_text(0, TEXT, 170, 92, WHITE);
}
// called on every frame
export function update(): void {}
carts/walt/simple/main.waltcallbacks
A cart implements the callbacks it cares about. In Walt they look like this:
export function update() {}See anatomy of a cart for the full list and what each one is passed.