Skip to content

Repository files navigation

LATEST VERSION RUST WASM

The abclang web playground running LeetCode's Two Sum

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.

Features

Last updated: 08/01/2026

abclang supports

There is no server-side runtime. The interpreter compiles to WebAssembly and executes fully client-side in the web playground.

Small example

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

Structure

Last updated: 08/01/2026

This repository is a Cargo workspace plus a frontend, split into a few distinct pieces:

Directory tree

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

How it works

A program flows through the same three stages whether it runs in the REPL or the browser:

  1. Lexing: Lexer walks the source and produces a stream of Tokens. Keywords like fn, let, if, for, and in are recognized via a compile-time phf map. The lexer also records comment spans separately so the editor can highlight them.
  2. Parsing: Parser turns tokens into an AST using a Pratt parser. Operator precedence runs from Lowest up through equality, comparison, sum, product, prefix, call, and index.
  3. Evaluation: evaluate walks the AST against an Environment. Every value is an Object (Integer, Float, Boolean, String, Array, Hash, Function, and so on). Closures capture their defining environment, and print/println write 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.

Setup

Requirements

  • Rust (2024 edition) with cargo
  • just: the task runner used for all commands below
  • wasm-pack: to compile the interpreter to WebAssembly
  • pnpm: to run the frontend

Commands

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 test

Warning

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.

REPL tips

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,
                                    },
                                ),
                            },
                        ),
                    },
                ),
            },
        ),
    ],
}

Deployment

The Dockerfile is a three-stage build:

  1. A rust stage installs wasm-pack and compiles the wasm crate.
  2. A node stage installs frontend dependencies, pulls in the generated wasm bindings, and runs pnpm build.
  3. An nginx:alpine stage serves the static dist/ 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.

About

Custom interpreter written in Rust

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages