A small BASIC-like language compiler written in Rust. The compiler parses source code with
chumsky, lowers it into a simple AST, emits WebAssembly with wasm-encoder, and can run the
generated module either in the browser or through a native Wasmtime demo.
src/ast.rs: AST definitions for statements and expressions.src/parser.rs: source parser and parser regression tests.src/compiler.rs: semantic checks, WebAssembly emission, and compiler regression tests.src/lib.rs: wasm-bindgen entry point used by the browser UI.src/main.rs: native Wasmtime demo behind thenativeCargo feature.index.htmlandscript.js: simple browser editor and output pane.
Run the native demo:
cargo run --features nativeRun tests:
cargo testCheck the default build:
cargo checkBuild the browser package with wasm-pack:
wasm-pack build --target webThis will generate ./pkg, and contains WASM artifacts. I am currently manually copy-pasting wasm_basic.js and other assets to the root directory for the playground to work on github pages.
The language is an integer-only, statement-oriented BASIC-style language. Programs compile to a WebAssembly module with a start function; running the module executes the program immediately.
Keywords are uppercase and case-sensitive: LET, PRINT, IF, THEN, FOR, TO, and END.
Identifiers use Chumsky's ASCII identifier parser: ASCII letters, digits, and underscores, with the
usual identifier start-character restrictions. Identifiers are case-sensitive, so A and a are
different names.
Integer literals are base-10 signed 32-bit values in the compiler output. The parser currently
accepts unsigned decimal literal syntax; negative values can be produced with subtraction
expressions such as 0 - 1.
Spaces, tabs, and carriage returns are ignored between tokens. Newlines and semicolons separate statements. Multiple separators are allowed, so blank lines and repeated semicolons are valid.
Comments are not currently supported.
A program is a sequence of statements:
LET A = 10
A = A + 1
PRINT AStatements may be separated with newlines:
LET A = 1
PRINT Aor semicolons:
LET A = 1; PRINT A;Leading separators, trailing separators, and blank lines are allowed.
LET declares a new variable:
LET TOTAL = 0Declarations are source-order checked. A variable must be declared before it is assigned or read. Redeclaring a name is a compiler error:
LET A = 1
LET A = 2Use bare assignment to update an existing variable:
LET A = 1
A = A + 1Assignments do not declare variables. This is a compiler error:
A = 1There is no block-local scope yet. Names declared inside IF or FOR bodies reserve names in the
same program-wide namespace, so another LET or loop variable with the same name is rejected.
Expressions are integer expressions.
Supported atoms:
- Integer literal:
123 - Variable reference:
A - Parenthesized expression:
(A + 1)
Supported arithmetic operators:
+: addition-: subtraction*: multiplication/: signed integer division%: signed integer remainder
Precedence, from strongest to weakest:
- Parentheses
*,/,%+,-
Operators are left-associative:
LET A = 10 - 3 - 2is parsed as:
(10 - 3) - 2
Unary operators are not supported yet.
PRINT evaluates an expression and passes the resulting i32 to the imported WebAssembly function
env.print:
PRINT A + 1In the native demo, env.print maps to println!("{}", x). In the browser UI, it appends the
printed value to the output pane.
IF conditionally executes a block:
IF A < B THEN
PRINT A
ENDSupported comparison operators:
<: signed less-than>: signed greater-than==: equality
The condition syntax is:
IF <expression> <comparison-operator> <expression> THEN
<statements>
END
ELSE is not supported yet. Nested IF statements are supported.
FOR repeats a block over an inclusive integer range:
FOR I = 1 TO 5
PRINT I
ENDThe loop variable is declared by the FOR statement and must not conflict with any existing name.
The current compiler requires both bounds to be integer literals:
FOR I = 0 TO 3
PRINT I
ENDThis is rejected for now, even though the parser accepts the syntax:
LET END_VALUE = 3
FOR I = 0 TO END_VALUE
PRINT I
ENDLoop ranges are inclusive. If the start literal is greater than the end literal, the loop emits no body executions. Loops are currently unrolled at compile time instead of emitted as WebAssembly loop instructions.
Blocks are statement lists terminated by END. Blank lines are valid inside blocks and before
END:
FOR I = 1 TO 2
IF I < 2 THEN
PRINT I
END
ENDThe compiler reports semantic errors before emitting Wasm. Current compile-time errors include:
- Duplicate declarations:
LET A = 1followed byLET A = 2 - Duplicate loop/declaration names:
LET I = 0followed byFOR I = 1 TO 3 - Assignment before declaration:
A = 1 - Reading an undeclared variable:
PRINT A - Non-literal
FORbounds in the current compiler
The emitted module imports one function:
env.print: (i32) -> ()
The generated program function is exported as the module start function, so instantiating the module runs the program immediately. There are no exported user functions.
These are not part of the current language:
- Strings
- Comments
- Functions or
RETURN - Arrays
INPUTELSEWHILE- User-defined procedures
- Floating point numbers
- Boolean literals
- Unary minus or unary plus
