Sprite logo

Rust ยท scripting language

A language small
enough to read whole.

Sprite is a tiny, embeddable scripting language in Rust. A one-pass lexer, a recursive-descent parser, and a tree-walking interpreter, with one eval call to drop into any Rust program. Try it below, right in your browser.

Try it in your browser View on GitHub
How to use this playground
Type or edit a script in the box below and press Run to execute it in your browser.

Try a subset in your browser

This box runs a small JavaScript reimplementation of a subset of Sprite (expressions, let, print, if/else, while) so you can poke at the syntax here. The real interpreter is the Rust crate.

runs entirely in your browser, nothing is sent anywhere
Click Run to evaluate the script above.

the demo is a subset in JavaScript, the real engine is the Rust crate behind the CLI and the eval API

Real output, from the real interpreter.

The crate ships a CLI. Point it at a script file, or start a REPL. This is the actual output of the bundled examples, captured from a release build.

$ sprite run examples/factorial.sprite
fact(1) = 1
fact(2) = 2
fact(5) = 120
fact(10) = 3628800

$ sprite # start the REPL
sprite 0.1.0 - a tiny embeddable scripting language
type an expression or statement, ctrl-d to exit
> let x = 41
> x + 1
42
> print("hi")
hi

factorial.sprite prints fact(1) through fact(10); shown abridged

How it compares

Sprite is deliberately at one end of the spectrum. The README says it plainly: if you want the capable option, reach for Lua or Rhai. If you want to understand every line of the interpreter running your script, reach for Sprite.

Lua

A mature, fast embeddable language with decades of features, coroutines, metatables, and a large C runtime. Far more capable, and far more to learn.

Rhai

A capable Rust-native scripting engine with a rich type system and deep Rust interop. Excellent when you need that breadth.

Sprite

The other end: a handful of value types, no standard library beyond print, no bytecode, no VM. Small enough to read start to finish, with one eval call and a Session for persistent state.

The pieces that matter

Four stages, a handful of types, two safety bounds.

Lexer

One pass over the source, no lookahead beyond one character, no external lexer generator. Skips whitespace and # comments, reports a typed Lex error on bad input, never panics.

Parser

Recursive descent producing a plain AST. Standard precedence from assignment down to calls and primaries. Returns a typed Parse error rather than crashing.

Interpreter

Walks the AST directly. No bytecode, no intermediate representation, no VM. A block evaluates to its last expression so a REPL prints something useful.

Value model

Five value types: numbers (f64), strings, booleans, nil, and functions. Functions close over their defining scope via a reference-counted, interior-mutable environment.

safety Parser depth bound

A recursion counter (MAX_DEPTH = 200) turns pathological input like thousands of nested parens into a Parse error instead of a native stack overflow.

safety Call depth bound

A call-depth counter (MAX_CALL_DEPTH = 512) turns runaway recursion in a Sprite script into a catchable Runtime("stack overflow"), not a crashed host process.

Language tour

Variables, arithmetic, comparisons, booleans, control flow, functions, closures, and string concatenation.

# variables and arithmetic
let x = 2 + 3 * 4
print(x)                    # 14

# recursive functions
fn fact(n) {
    if n <= 1 {
        return 1
    }
    return n * fact(n - 1)
}
print(fact(5))               # 120

# while loops
let i = 1
let sum = 0
while i <= 10 {
    sum = sum + i
    i = i + 1
}
print(sum)                    # 55

# closures
fn make_counter() {
    let count = 0
    fn increment() {
        count = count + 1
        return count
    }
    return increment
}
let counter = make_counter()
print(counter())              # 1
print(counter())              # 2

Embedding

A clean API for dropping a scripting layer into any Rust program. Errors are typed results, never panics on bad input.

One-shot

let value = sprite::eval(
    "2 + 3 * 4"
).unwrap();

assert_eq!(
    value.to_string(),
    "14"
);

Persistent session

let mut s = sprite::Session::new();
s.run("let score = 0").unwrap();
s.run("score = score + 10").unwrap();

let v = s.run("score").unwrap();
assert_eq!(v.to_string(), "10");

Use it

A library API, a CLI, and a test suite. That is the whole surface.

Library

sprite::eval(source) for one-shot evaluation, or sprite::Session::new() when variables and functions should survive across calls. Both return Result<Value, SpriteError>.

CLI

sprite run FILE executes a script; running sprite with no arguments starts a REPL. Example scripts live in examples/.

Errors

One SpriteError type with three variants, Lex, Parse, and Runtime, each carrying a message. No stage panics, unwraps, or indexes out of bounds on attacker-controlled input.

Tests

The suite covers precedence, control flow, closures, string concatenation, and error paths, including a 10,000-deep paren expression that must return a Parse error rather than crash.

# build, run an example, test
git clone https://github.com/pavanchow/sprite
cd sprite
cargo build --release
cargo run --release -- run examples/factorial.sprite
cargo test