Sprout is a small programming language that runs on Go. It ships with a lexer, a Pratt parser, and two execution engines. Version 0.2 adds a stack-based bytecode virtual machine. Version 0.3 adds a module system for splitting programs across files. The interpreter and the VM share one runtime and one standard library. Sprout produces friendly diagnostics that point at the exact problem.
The goal is a language that is easy to learn and easy to read. The design favors a short standard library over magic. The codebase is structured to grow cleanly over time.
- A documented grammar with a line-aware Pratt parser.
- A bytecode compiler and a stack-based virtual machine.
- A tree-walking interpreter that shares the runtime with the VM.
- A disassembler for the compiled instruction stream.
- A module system with imports, exports, and cycle detection.
- Source diagnostics with a gutter, line, and caret.
- A small standard library for real example programs.
- A REPL for interactive experiments.
- Deterministic tests for every stage of the pipeline.
You need Go 1.22 or newer.
go build ./cmd/sprout
Run an example program on the interpreter.
./sprout run examples/fizzbuzz.spr
Run the same program on the bytecode virtual machine.
./sprout vm examples/fizzbuzz.spr
Start an interactive session.
./sprout repl
On Windows the binary is sprout.exe.
Save this file as hello.spr.
let name = "world"
print("hello, " + name)
Run it.
sprout run hello.spr
You see this output.
hello, world
Run the tour in docs/tour.md for a full walkthrough.
| Command | Purpose |
|---|---|
sprout run file.spr |
Runs a program on the interpreter. |
sprout vm file.spr |
Runs a program on the bytecode VM. |
sprout dis file.spr |
Shows the compiled bytecode. |
sprout repl |
Starts a session. |
sprout lex file.spr |
Shows the tokens. |
sprout parse file.spr |
Shows the syntax tree. |
sprout check file.spr |
Checks the file and its imports without running. |
sprout version |
Shows the version. |
Pass a file path with no command to run it.
Run sprout help to see the full usage.
Add -color always to force colored diagnostics.
Sprout reports errors with context. A compile error looks like this.
error: undefined name 'y'
--> demo.spr:2:7
|
2 | print(y)
| ^
A runtime error includes the call stack.
error: cannot divide by zero
--> demo.spr:3:9
|
3 | return n / 0
| ^
|
at inner (demo.spr:5:12)
at outer (demo.spr:9:5)
The checker finds problems before the program runs. It reports undefined names, bad constants, and misplaced control flow. Both engines report runtime errors in this format.
Sprout has integers, floats, strings, booleans, and nil. Lists and maps are first-class and mutable. Functions are values. They capture their scope as closures.
fn make_counter() {
let count = 0
return fn() {
count = count + 1
return count
}
}
let next = make_counter()
print(next()) // 1
print(next()) // 2
Only nil and false are falsy.
An expression ends at a newline unless it is inside brackets.
See docs/grammar.md for the formal grammar.
Version 0.2 adds a compiler and a stack-based virtual machine.
The compiler turns a syntax tree into bytecode.
The VM executes that bytecode with an operand stack and call frames.
Both engines share the runtime, so they behave identically.
The dis command shows the compiled instructions.
$ sprout dis examples/hello.spr
== fn <main> ==
Params:
Slots: 1
0000 PUSH_CONST ; examples/hello.spr:5:12 0 (world)
0003 SET_LOCAL ; examples/hello.spr:5:1 0
0006 BUILTIN ; examples/hello.spr:7:1 0 (print)
0009 PUSH_CONST ; examples/hello.spr:7:7 1 (hello, )
0012 GET_LOCAL ; examples/hello.spr:7:19 0
0015 ADD ; examples/hello.spr:7:17
0016 CALL ; examples/hello.spr:7:6 1
Each line shows the offset, the opcode, and the source position.
The engine parity tests prove the VM matches the interpreter.
See docs/bytecode.md for the full reference.
Version 0.3 adds a module system.
Split a program across files with import and export.
A module runs once, in its own scope.
Only exported names are visible to importers.
Save a library module.
// lib/mathx.spr
export fn double(x) {
return x * 2
}
Use it from a program.
import "lib/mathx"
print(mathx["double"](21)) // 42
The bound name comes from the file name.
Use as to choose a different name.
Paths resolve against the importing file directory.
Circular imports are an error.
The loader caches each module, so it runs once per program.
The check command validates the whole import graph.
The VM compiles imports into IMPORT instructions.
Run examples/modules.spr to see a working library.
See docs/modules.md for the full reference.
The standard library is small and documented.
It covers output, conversion, lists, maps, strings, and numbers.
It also provides assert for tests and examples.
let nums = [1, 2, 3, 4]
print(map(nums, fn(x) { return x * 2 }))
print(fold(nums, 0, fn(acc, x) { return acc + x }))
See docs/stdlib.md for the full reference.
The examples directory holds documented programs.
hello.sprprints a greeting.fizzbuzz.sprplays the classic game.fibonacci.spruses recursion.primes.sprfinds primes below 30.counters.sprshows closures.collections.sprworks with lists and maps.strings.sprshows string functions.math.sprshows numbers and rounding.higher_order.spruses map, filter, and fold.guess.spris an interactive game.modules.sprimports the library inexamples/lib.
Each example has a golden output in test/golden.
Both engines must match the goldens.
The repository is a pipeline of small Go packages.
cmd/sprout command line interface
internal/source source files and positions
internal/token token definitions
internal/lexer the scanner
internal/ast the syntax tree
internal/parser the Pratt parser
internal/checker static analysis
internal/module module loading, caching, and cycles
internal/runtime value semantics and the standard library
internal/interp the tree-walking interpreter
internal/code opcodes and the instruction stream
internal/compiler bytecode compiler
internal/vm stack-based virtual machine
internal/diag diagnostics and rendering
internal/repl the interactive session
Each stage is independent. The parser feeds the checker and the compiler. The runtime is the single source of truth for both engines. The module loader runs on either engine. Adding a feature means updating the runtime, then both engines stay in step.
Format the code.
gofmt -l .
Check the code.
go vet ./...
Run all tests.
go test ./...
Run a single package.
go test ./internal/vm/
All tests pass on Go 1.22 and newer.
| Suite | Scope |
|---|---|
internal/lexer |
Tokens, positions, and lexer errors. |
internal/parser |
Precedence, statements, and recovery. |
internal/checker |
Scope and static errors. |
internal/module |
Path resolution, caching, and cycles. |
internal/runtime |
Arithmetic, comparison, and indexing. |
internal/interp |
Evaluation, closures, and runtime errors. |
internal/code |
Opcodes, the builder, and disassembly. |
internal/compiler |
Bytecode for expressions and control flow. |
internal/vm |
Execution, closures, and runtime errors. |
internal/diag |
Diagnostic rendering. |
test |
Example goldens, engine parity, and command line. |
The parity tests run each program on both engines. The VM tests match the same goldens as the interpreter. Tests use only the standard library. They need no network or secrets.
Version 0.2 is complete. It adds the bytecode virtual machine. It keeps the same parser and checker.
Version 0.3 adds a module system and a build tool. The module system is complete. Imports, exports, and cycle detection ship in this release. The build tool remains.
Version 0.4 adds structs, methods, and interfaces. Version 0.5 adds result types and pattern matching. Version 0.6 adds concurrency with channels.
- The language has no classes or structs yet.
- Type annotations are optional and checked lightly.
- Map keys must be strings.
- Integer division truncates toward zero.
- There is no tail-call optimization.
- The standard library is small by design.
- The VM materializes a loop iterable before the loop starts.
breakandcontinueinside a closure are not supported.- Module values are read-only. There is no package index yet.
- Import errors point at the import statement, not inside the module.
Sprout is MIT licensed. See LICENSE.