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.

  1. Sandboxing untrusted code: what a --sandbox flag can’t cover, what the container and the browser add, and the process limit of mine that turned out to do nothing.
  2. Error messages that teach: how edit distance finds the Did you mean…? suggestion, why every error carries a stable kind, and why try refuses to catch a name typo.
  3. 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.
  4. From source text to syntax tree: INDENT and DEDENT tokens, f-strings desugared in the lexer, and a recursive-descent precedence ladder.
  5. 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.