The Sprout Programming Language
A practical, chapter-by-chapter guide to writing Sprout—from your first bloom to reusable gardens of packages.
Read from top to bottom if you are new to Sprout, or use the chapter list as a reference. Every example is complete enough to paste into the playground.
Your first program
Create a file named hello.sprout. Statements end with semicolons, and bloom sends values to standard output.
bloom("Hello, garden!");npm start -- hello.sproutHello, garden!
Installing and running Sprout
Sprout runs on Node.js. From the project root, install the TypeScript development dependencies, then pass a .sprout file to the CLI.
npm install
npm start -- examples/fizzbuzz.sproutOr pipe a program directly into the interpreter:
printf 'bloom("Hello, Sprout!");
' | npm startSprout syntax
Every core keyword follows the garden metaphor. You plant mutable values, protect constants as seed, grow functions, branch through decisions, and bloom output. Conventional spellings remain available as compatibility aliases.
seed message = "Small syntax, big ideas.";
grow celebrate(value) {
branch (value != nil) {
bloom("Sprout: ${value}");
}
harvest true;
}
celebrate(message);plantmutable bindingalias: letseedconstant bindingalias: constgrowfunctionalias: fnharvestreturn a valuealias: returnbranchconditionalalias: ifotherwisealternate branchalias: elsetendwhile loopalias: whileeachfor loopalias: forprune / skipbreak / continuealias: break / continuebloomdisplay outputalias: print / showValues & variables
Use plant for values that may change and seed for values that stay fixed. Sprout supports numbers, strings, booleans, nil, lists, and objects.
seed language = "Sprout";
plant year = 2026;
plant ready = true;
plant colors = ["moss", "fern", "lime"];
plant author = { name: "Ada", curious: true };
year += 1;
bloom(language, year);Data types and scope
Sprout is dynamically typed: values carry their type at runtime, and bindings do not need annotations. A nested block can read bindings from its parent scope. A plant can be reassigned; a seed cannot.
42, 3.14Arithmetic and comparisons"fern"Text and interpolationtrue, falseBranch conditionsnilThe absence of a value[1, 2, 3]Ordered mutable values{ name: "ivy" }Named mutable propertiesgrow(x) { … }Callable closuresseed garden = "north";
plant count = 1;
{
plant count = 2; // shadows the outer count
bloom(garden, count);
}
count += 1;
bloom(count);Functions and closures
Declare named functions with grow and send a value back with harvest. A function without an explicit harvest produces nil.
grow greet(name) {
harvest "Hello, ${name}!";
}
bloom(greet("gardener"));Anonymous functions capture their garden
An anonymous grow expression can be stored or passed to another function. It remembers bindings from the scope where it was created.
grow multiplier(factor) {
harvest grow(value) {
harvest value * factor;
};
}
plant double = multiplier(2);
bloom(double(21));42
Control flow
Use branch/otherwise for decisions, tend for condition-based loops, and each ... in for collections. Inside a loop, prune stops growth and skip moves to the next item.
each (plant n in range(1, 6)) {
branch (n % 2 == 0) {
bloom(n, "is even");
} otherwise {
bloom(n, "is odd");
}
}Strings and comments
Strings use double quotes. Put an expression inside ${...} to interpolate it. A comment begins with // and continues to the end of the line.
seed plantName = "sunflower";
seed height = 180;
// Expressions are evaluated in the current scope.
bloom("The ${plantName} grew to ${height / 100}m");
bloom("line one\nline two");Supported escapes are \n, \t, \", and \\. Unknown escapes keep the escaped character.
Lists & objects
Lists are ordered and zero-indexed. Objects group named values. Read and update both with familiar index or property syntax.
plant favorite = {
name: "Monstera",
heights: [12, 18, 25]
};
favorite.heights[0] += 3;
bloom(favorite.name, max(favorite.heights));Collection pipelines
Higher-order built-ins make list transformations concise. Each callback receives the current value and can close over surrounding bindings.
seed harvests = [3, 8, 5, 12, 7];
plant ripe = filter(harvests, grow(weight) {
harvest weight >= 7;
});
plant crates = map(ripe, grow(weight) {
harvest weight * 2;
});
plant total = reduce(crates, grow(sum, weight) {
harvest sum + weight;
}, 0);
bloom(crates);
bloom(total);[16, 24, 14] 54
Built-ins
Sprout includes practical helpers for output, collections, strings, and numbers.
bloom(...values)Display values on one line (print and show are aliases).len(value)Return the length of a string, list, or object.range(start?, end, step?)Create a list of numbers.map(list, grow)Transform every item in a list.filter(list, grow)Keep items that pass a test.reduce(list, grow, seed)Fold a list into one value.split(text, separator)Split text into a list.join(list, separator)Join a list into text.min(...) / max(...)Find the smallest or largest number.Modules
Split programs with named exports and imports. Local module paths resolve relative to the importing file.
// math.sprout
export grow square(n) { harvest n * n; }
export plant answer = 42;
// app.sprout
import { square, answer } from "./math";
bloom(square(answer));The Garden package manager
Sprout manages packages as a garden. A project is cultivated, dependencies are grafted into it, installed packages live in garden_modules, and unwanted packages are uprooted. Package sources are currently local directories; a network nursery is not implemented yet.
Cultivate a garden
mkdir my-garden && cd my-garden
npm start -- cultivate my-gardenThe command creates a garden.json manifest:
{
"name": "my-garden",
"version": "0.1.0",
"main": "index.sprout",
"grafts": {}
}Graft a package
A package is another directory with its own garden.json and entry file. Graft it by local path, then inspect the garden:
npm start -- graft ../pollinator
npm start -- gardenGrafting copies the package into garden_modules, adds a file: entry under grafts, and updates garden-lock.json. Commit both garden files so another gardener can reproduce the same installation.
import { pollinate } from "pollinator";
bloom(pollinate("tomato"));Restore or uproot grafts
# Restore every graft listed in garden.json
npm start -- graft
# Remove a graft from the manifest, lockfile, and modules bed
npm start -- uproot pollinatorErrors and debugging
Sprout reports lexical, parsing, and runtime errors with a short message. The CLI writes errors to standard error and exits with status 1, which makes failures usable in shell scripts and CI.
seed height = 10;
height = 20;cannot assign to const 'height'Common errors
undefined variable 'name'Declare the binding before use and check its scope.expected ';' after expressionEnd the previous statement with a semicolon.expected N arguments, got MCall the function with exactly its declared parameter count.index out of rangeUse an integer index between zero and len(list) - 1.operator expects numbersCheck the operand types before arithmetic.module not foundCheck the relative path or graft the missing package.CLI reference
The CLI runs source files and manages local gardens. Commands are invoked through the repository's npm script.
npm start -- file.sproutRun a source file.
npm start -- cultivate [name]Create garden.json in the current directory.
npm start -- graft [./path]Graft one local package, or restore every manifest graft.
npm start -- gardenList grafts in the current garden.
npm start -- uproot <name>Remove a graft from disk, manifest, and lockfile.
npm start -- helpShow the command summary.
The older commands init, install, list, and uninstall remain supported for existing scripts.
Keywords and operators
Keywords
plantseedgrowharvestbranchotherwisetendeachinpruneskipimportexportfromtruefalsenilandorOperators
+ - * / %== != < <= > >=! and or= += -= *= /= %=++ --value[index] object.propertyTruthiness
nil, false, 0, and an empty string are falsey. All other values—including empty lists and empty objects—are truthy. The and and or operators short-circuit and return operand values.