Series · 13 October 2026 · 3 min read
Sprout: a beginner's programming language, interpreter and all, in C
See what Sprout code looks like, the one question behind its syntax, and which part of a C interpreter to read first.
Someone in their first hour of programming saves a two-line file and misspells a name on line 2.
The file is hello.sprout. Line 1 is make name = "Sam", line 2 is show nme, and the interpreter stops there and prints this:
Sprout error in hello.sprout (line 2): I don't know what 'nme' is.
Did you mean 'name'?
I wrote Sprout for that person. It’s a small language built from scratch in C with no external dependencies: a hand-written lexer, a recursive-descent parser, a tree-walking interpreter and a garbage collector, all in one file. That file, src/sprout.c, was 4,774 lines at v0.1.22, the version this series reads. CI builds and tests it on Linux, macOS and Windows, and a WebAssembly build runs it in the browser.
What a Sprout program looks like
make score = 8
when score >= 9:
show "outstanding"
orwhen score >= 7:
show "great job"
otherwise:
show "keep going"
task greet(who):
give "Hello, " + who + "!"
show greet("Sprout")
No let, no print, no if. You create a name with make, change it with set, print with show, and define a function with task, which returns with give. I wanted someone to be able to guess what a program does by reading it out loud.
Would a 10-minute beginner find this obvious?
That line from the roadmap is the test every feature has to pass. Power features stay opt-in and off the hello-world path. I gave each decision a fourth column, what pays its cost back, because a decision without one has only moved the cost onto the user:
| Decision | Helps the beginner | Costs | Paid down by |
|---|---|---|---|
Own keywords (make, show, task) |
Readable out loud | Unfamiliar to experienced devs | A misspelled keyword at the start of a line, followed by a value, gets Did you mean…? |
make vs set |
Re-make-ing a name in the same scope is an error, not a silent overwrite |
Two verbs to learn | The error names the fix: use ‘set’ to change it |
| Doubles only | No number type to pick before make x = 5 |
Precision at the extremes, no bigint | Whole numbers print without .0 |
| Indentation blocks | No {} / ; noise |
Tabs-vs-spaces pitfalls | An exact-dedent error, sprout format |
I’ve also reversed my own non-goals three times. The README and roadmap ruled out user-defined types, operator overloading and negative indexing; later releases shipped all three (classes in v0.1.7, operator overloading in v0.1.11, xs[-1] in v0.1.21), opt-in and off the hello-world path. The lesson I took: when you set a non-goal, write down what evidence would reverse it.
Five parts, one piece of the interpreter each
Each part quotes the C it’s about, from src/sprout.c, and a technique you can reuse in your own tools. If you run other people’s code, start with part 1; if you’ve never watched memory get reclaimed, start with part 3. The rest read fine in any order.
- Sandboxing untrusted code: what a
--sandboxflag can’t cover, what the container and the browser add, and the process limit of mine that turned out to do nothing. - Error messages that teach: how edit distance finds the Did you mean…? suggestion, why every error carries a stable kind, and why
tryrefuses to catch a name typo. - A garbage collector in C: conservative mark-and-sweep that finds its roots by scanning the C stack, what that scan can’t see, and the rooting rule for everything else.
- From source text to syntax tree: INDENT and DEDENT tokens, f-strings desugared in the lexer, and a recursive-descent precedence ladder.
- Contract tests and CI: freezing v0.1, CI gates that were green for the wrong reason, a test that couldn’t fail, and docs that drift from the code.
Where to find the code
The source is on GitHub at github.com/fizzexual/Sprout. The browser playground runs the WebAssembly build on your own machine, with nothing to install. Paste the score program from above into it and change the 8 to a 9.