Scarlet is a dynamically typed, object-oriented, garbage-collected programming language written in Rust. It compiles source code to bytecode in a single pass and executes it on a stack-based virtual machine, with the entire runtime weighing in at just ~500 KB. Scarlet runs natively as a CLI tool and also compiles to WebAssembly for embedded use in browsers and Node.js environments.
- Dynamic typing: variables hold any value type at runtime
- Object-oriented: classes, single inheritance,
this,super, and constructors viainit - Closures: first-class functions that capture variables from enclosing scopes via upvalues
- Garbage collection: tri-color mark-and-sweep GC
- String interning: identical strings share a single allocation, enabling O(1) equality checks
- Bitwise operators:
&,|,^,!(bitwise NOT),<<,>> - REPL: interactive read-eval-print loop with persistent state across lines
- Debug/disassembly mode: prints annotated bytecode before execution
- Native functions: built-in functions for I/O, type introspection, coercion, and timing
- WebAssembly target: compile to
.wasmfor web, bundler, and Node.js targets viawasm-pack
| Type | Examples |
|---|---|
number |
42, 3.14, -7 |
boolean |
true, false |
nil |
nil |
string |
"hello", "world" |
function |
defined with fun |
class |
defined with class |
instance |
created by calling a class |
Variables are declared with let. Uninitialized variables default to nil.
let x = 10;
let name = "scarlet";
let nothing; // nil
Arithmetic
1 + 2 // 3
10 - 4 // 6
3 * 5 // 15
10 / 4 // 2.5
10 % 3 // 1
Comparison
1 == 1 // true
1 != 2 // true
3 > 2 // true
3 >= 3 // true
2 < 5 // true
2 <= 2 // true
Logical
true && false // false
true || false // true
!true // false
Bitwise (integer operands only, no fractional part)
6 & 3 // 2 (AND)
6 | 3 // 7 (OR)
6 ^ 3 // 5 (XOR)
!5 // bitwise NOT, same as Rust
2 << 3 // 16 (left shift)
16 >> 2 // 4 (right shift)
String concatenation
"hello" + " " + "world" // "hello world"
if / else
if (x > 0) {
println("positive");
} else {
println("non-positive");
}
while
let i = 0;
while (i < 5) {
println(i);
i = i + 1;
}
for
for (let i = 0; i < 5; i = i + 1) {
println(i);
}
Functions are first-class values declared with fun. They support recursion and closures.
fun add(a, b) {
return a + b;
}
println(add(3, 4)); // 7
Closures
fun make_counter() {
let count = 0;
fun increment() {
count = count + 1;
return count;
}
return increment;
}
let counter = make_counter();
println(counter()); // 1
println(counter()); // 2
Classes are declared with class. The special init method is the constructor. Use this to reference the current instance.
class Animal {
init(name) {
this.name = name;
}
speak() {
println(this.name + " makes a sound.");
}
}
let a = Animal("Dog");
a.speak(); // Dog makes a sound.
Inheritance
Use the inherits keyword to extend a class. Call superclass methods with super.
class Dog inherits Animal {
init(name) {
super.init(name);
}
speak() {
println(this.name + " barks.");
}
}
let d = Dog("Rex");
d.speak(); // Rex barks.
These functions are built in and available globally.
| Function | Description | Platform |
|---|---|---|
print(...) |
Print values without a trailing newline | All |
println(...) |
Print values followed by a newline | All |
len(s) |
Return the length of a string | All |
type(v) |
Return the type of a value as a string | All |
to_string(v) |
Convert a value to its string representation | All |
to_number(v) |
Parse a string or coerce a boolean to a number | All |
clock() |
Return the current Unix timestamp in milliseconds | Native only |
sleep(ms) |
Sleep for the given number of milliseconds | Native only |
read(prompt) |
Print a prompt, read a line of stdin, and return it | Native only |
let t = type(42); // "number"
let s = to_string(3.14); // "3.14"
let n = to_number("99"); // 99
println(len("hello")); // 5
Only single-line comments are supported, using //.
// This is a comment
let x = 1; // inline comment
Scarlet is organized as a classic pipeline with a separate WebAssembly entry point.
Source text is fed into the Scanner, a lexer that produces a stream of tokens. Those tokens flow into the Compiler, a hand-crafted single-pass Pratt parser that consumes the token stream and directly emits bytecode, with no AST built. At runtime, the Virtual Machine executes that bytecode on a stack-based interpreter, handling function calls, closures, and triggering garbage collection. All heap-allocated objects (strings, functions, closures, class instances) live in the Heap, which also owns the string intern table and the globals table.
The garbage collector uses a mark-and-sweep strategy, tracing live objects from the VM stack, call frames, open upvalues, and globals, then sweeping anything unreachable.
The entire compiler and virtual machine weigh in at roughly ~500 KB as a release binary.
cargo build --bin scarlet --releasewasm-pack build --target webwasm-pack build --target bundlerwasm-pack build --target nodejsAll wasm-pack outputs are written to pkg/.
The .scar file extension is the convention for Scarlet script files.
scarlet --run path/to/script.scarscarlet --repl
# or just
scarletscarlet --run script.scar --debug
scarlet --repl --debugMIT
Scarlet is built for educational purposes, heavily inspired by the clox implementation of the Lox language from the excellent Crafting Interpreters book by Bob Nystrom.