View the online playground at abclang.tahmid.io
abclang is a small, dynamically-typed programming language with a hand-written tree-walking interpreter in Rust, compiled to WebAssembly so it can run entirely in your browser. It ships with both a native REPL and a web-based playground.
The language design is heavily inspired by the Monkey language from Thorsten Ball's Writing an Interpreter in Go (let bindings, first-class functions, closures, and a Pratt parser), extended with floats, for ... in loops, mutable arrays/hashmaps, and a handful of extra builtins.
This is an ongoing project, and I will continue to extend the language with as many things as I can think of.
Last updated: 08/01/2026
abclang supports
- Two number types: 64-bit integers and floats, with automatic int to float promotion when mixed
letbindings and reassignment (x = v,arr[i] = v,map[k] = v)- First-class functions (
fn), closures, and recursion if/elseas an expression that evaluates to a valuefor ... inloops over arrays, ranges, and hashmaps (for key, value in map)- Strings with
+concatenation - Arrays and hashmaps (both mutable, both allowed to be heterogeneous)
- Index access & assignment, including nested (
people[1]["name"] = "z") //line comments- A standard library of builtins:
len,max,min,first,last,rest,push,range,print,println
There is no server-side runtime. The interpreter compiles to WebAssembly and executes fully client-side in the web playground.
// closures capture their surrounding environment
let newAdder = fn(x) {
fn(y) { x + y };
};
let addTwo = newAdder(2);
println(addTwo(2)); // => 4
// if/else is an expression
let classify = fn(n) {
if (n > 0) { "positive" } else { "non-positive" };
};
// arrays and hashmaps are mutable: assign straight into an index or key
let scores = [10, 20, 30];
scores[0] = 99; // => [99, 20, 30]
let ages = {"alice": 30, "bob": 25};
ages["carol"] = 41; // insert a new key
ages.bob = 26; // dot sugar for ages["bob"] = 26
for name, age in ages {
println(name, age);
}Note
The web playground ships with a set of runnable examples (arithmetic, closures, recursion, iterators, and even a LeetCode "Two Sum" solution). They live in app/src/lib/examples.ts.
Last updated: 08/01/2026
This repository is a Cargo workspace plus a frontend, split into a few distinct pieces:
- The interpreter itself, written in Rust. This is the heart of the project: a hand-written lexer, a Pratt parser that produces an AST, and a tree-walking evaluator with its own object system, environments, and builtins.
- A native REPL built on rustyline that consumes the
interpretercrate directly. It supports adprintprefix to dump the parsed AST for any line. - A thin wasm-bindgen wrapper that exposes the interpreter to JavaScript. It exports an
Interpreterclass (withevaluateandreset) as well as atokenizefunction used to drive editor syntax highlighting. - The web playground, written in TypeScript and React 19 + Vite, with a CodeMirror 6 editor. It loads the compiled
.wasmmodule and runs abclang entirely in the browser. - My container image, defined as a multi-stage Docker build that compiles the wasm, builds the React app, and serves the static bundle with nginx.
- My CI/CD pipeline, which runs on GitHub Actions with reusable composite actions and Bun + TypeScript scripts. On
mainit tests, builds & pushes thetahminator/abclangimage to Docker Hub, and tags the commit. Tags then trigger a GitOps deploy that opens a PR against my Kubernetes manifest repo.
abclang
├── interpreter # the language, as a reusable Rust library crate
│ └── src
│ ├── lexer # source text -> tokens (+ comment spans for highlighting)
│ ├── parser # tokens -> AST, via a Pratt (operator-precedence) parser
│ ├── ast # statement & expression node definitions
│ └── eval # tree-walking evaluator
│ ├── builtins.rs # len, max, min, first, last, rest, push, range, print, println
│ └── object # runtime object system + lexical environments
├── repl # native rustyline REPL that links against `interpreter`
├── wasm # wasm-bindgen bindings: Interpreter class + tokenize()
│ └── src/tokenizer # maps token types -> highlight categories
├── app # React + Vite + CodeMirror playground
│ └── src
│ ├── ui/editor # Editor, Toolbar, CodePanel, OutputPanel
│ ├── hooks/editor.ts # editor state (code, examples, run, clear)
│ └── lib
│ ├── abclang # generated wasm bindings (output of `just build-wasm`)
│ ├── examples.ts # the runnable example snippets
│ └── editor # CodeMirror syntax highlighting via wasm `tokenize`
├── infra
│ └── Dockerfile # wasm builder -> frontend builder -> nginx runtime
├── .github # CI/CD workflows, composite actions, and Bun scripts
└── Justfile # dev/build/test task runner commands
A program flows through the same three stages whether it runs in the REPL or the browser:
- Lexing:
Lexerwalks the source and produces a stream ofTokens. Keywords likefn,let,if,for, andinare recognized via a compile-timephfmap. The lexer also records comment spans separately so the editor can highlight them. - Parsing:
Parserturns tokens into an AST using a Pratt parser. Operator precedence runs fromLowestup through equality, comparison, sum, product, prefix, call, and index. - Evaluation:
evaluatewalks the AST against anEnvironment. Every value is anObject(Integer,Float,Boolean,String,Array,Hash,Function, and so on). Closures capture their defining environment, andprint/printlnwrite into an output buffer that the host (REPL or web app) drains after the run.
Note
The interpreter crate has no I/O of its own beyond that captured output buffer, which is what makes it safe and easy to drop into a WebAssembly sandbox.
- Rust (2024 edition) with
cargo just: the task runner used for all commands belowwasm-pack: to compile the interpreter to WebAssemblypnpm: to run the frontend
All common tasks are wired up in the Justfile:
# start the native REPL
just dev
# compile the interpreter to wasm and write bindings into app/src/lib/abclang
just build-wasm
# build the wasm, then start the Vite dev server for the playground
just wasm-dev
# run the Rust test suite and the frontend lint checks
just testWarning
The frontend imports the generated wasm bindings from app/src/lib/abclang. If you're running the playground for the first time (or after changing any Rust code), run just build-wasm first. just wasm-dev does this for you.
The REPL prompt is << . Prefix any line with dprint to print the parsed AST for that line instead of evaluating it, which is handy when debugging parser behavior.
<< let x = 5 * (2 + 3);
<< x
25
For example, dprint 1 + 2 * 3 shows how precedence nests the multiplication under the addition in the parsed AST:
Program {
statements: [
Expression(
ExpressionStatement {
token: Token { literal: "1", typ: Int },
expr: Infix(
InfixExpression {
token: Token { literal: "+", typ: Plus },
left: IntegerLiteral(
IntegerLiteralExpression {
token: Token { literal: "1", typ: Int },
value: 1,
},
),
op: "+",
right: Infix(
InfixExpression {
token: Token { literal: "*", typ: Asterisk },
left: IntegerLiteral(
IntegerLiteralExpression {
token: Token { literal: "2", typ: Int },
value: 2,
},
),
op: "*",
right: IntegerLiteral(
IntegerLiteralExpression {
token: Token { literal: "3", typ: Int },
value: 3,
},
),
},
),
},
),
},
),
],
}
The Dockerfile is a three-stage build:
- A
ruststage installswasm-packand compiles thewasmcrate. - A
nodestage installs frontend dependencies, pulls in the generated wasm bindings, and runspnpm build. - An
nginx:alpinestage serves the staticdist/bundle.
On every push to main, CI runs the test suite, builds & pushes the tahminator/abclang image to Docker Hub, and creates a new git tag. Pushing a tag triggers CD, which promotes the image and opens a GitOps PR against my Kubernetes manifest repo to roll the new version out to production.
