Sprout programming language
@tejasgk|2026 — Present
I Built My Own Programming Language. Then I Accidentally Built npm for It.
I started Sprout because I wanted to understand what actually happens when we write code.
Not how to build another React app.
Not another API.
Not another wrapper around an LLM.
I wanted to go one layer lower.
When I write:
plant name = "Tejas";
branch (name == "Tejas") {
bloom("Hello");
}
how does a computer go from a bunch of characters in a text file to actually understanding what I meant?
So I started building a programming language.
I called it Sprout.
And somewhere along the way I ran into another problem:
Okay... I have a programming language now. How the hell does one Sprout project use code from another Sprout project?
And that's how Sprout stopped being just an interpreter.
I started building a package manager too.
It started with tokens
At the lowest level, Sprout doesn't understand:
plant age = 23;
It sees characters.
So the first thing I needed was a lexer.
The lexer's job is basically to walk through the source code character by character and turn it into meaningful tokens.
For example:
plant age = 23;
becomes conceptually:
LET
IDENT(age)
=
NUMBER(23)
;
Sprout supports numbers, strings, identifiers, comments, operators and punctuation, and it keeps track of line and column information so errors can point back to the source.
But I didn't want Sprout to simply look like JavaScript with a different file extension.
The language has a gardening theme.
So Sprout understands both conventional keywords and Sprout-flavoured ones:
let → plant
const → seed
fn → grow
if → branch
else → otherwise
while → tend
return → harvest
break → prune
continue → skip
Internally, both versions become the same tokens.
So this:
plant x = 10;
branch (x > 5) {
bloom("big");
}
still behaves like a normal programming language.
It just has considerably more photosynthesis.
Then I had to teach it grammar
Tokens aren't enough.
Knowing that something is a number, identifier or + doesn't tell you what an entire program means.
Consider:
plant result = 5 + 10 * 2;
The interpreter needs to understand that multiplication happens before addition.
It needs to understand that:
foo(10)
is a function call.
That:
person.name
is property access.
That:
numbers[2]
is indexing.
That:
x++
updates a variable.
So after lexing comes parsing.
My parser takes the token stream and builds an Abstract Syntax Tree.
I defined expressions for literals, variables, lists, objects, assignments, indexing, member access, unary and binary operations, updates, calls and functions. Statements cover variables, constants, functions, blocks, conditionals, loops, returns, break, continue, imports and exports.
Something like:
5 + 10 * 2
essentially becomes:
+
/ \
5 *
/ \
10 2
Then the interpreter can walk that structure instead of trying to understand raw text.
I implemented operator precedence directly in the parser, with logical OR and AND at the bottom and multiplication/division/modulo above addition and subtraction.
This was one of those moments where something I'd used thousands of times suddenly stopped being magic.
Of course:
5 + 10 * 2
equals 25.
But somebody has to actually write the rules that make the language interpret it that way.
This time that somebody was me.
Then Sprout needed memory
Consider:
plant x = 10;
grow test() {
plant y = 20;
bloom(x + y);
}
test() can access x.
But code outside test() shouldn't magically have access to y.
So now I needed scope.
I built an Environment that stores variables in a map and can point to a parent environment.
When Sprout asks for a variable, it checks:
current scope
↓
parent scope
↓
parent's parent
↓
...
until it finds it or throws an undefined-variable error.
Constants are tracked separately so assigning to a seed after declaration produces an error.
This also gave me closures.
A Sprout function remembers the environment where it was created.
Suddenly I wasn't just parsing syntax anymore.
Sprout had actual runtime semantics.
Then came the interpreter
The interpreter walks the AST and executes it.
A declaration:
plant x = 10;
creates a value in the current environment.
A branch:
branch (x > 5) {
bloom("yes");
}
evaluates the condition and executes the appropriate block.
Loops repeatedly evaluate their condition.
Functions create their own environments.
harvest returns values.
prune breaks loops.
skip continues them.
Imports load another module.
Exports expose values from that module.
The execution engine handles all of those statement types explicitly.
I also gave Sprout built-in functions.
For output:
print(...)
show(...)
bloom(...)
For collections:
len(...)
push(...)
pop(...)
range(...)
slice(...)
map(...)
filter(...)
reduce(...)
Strings:
upper(...)
lower(...)
trim(...)
contains(...)
startsWith(...)
endsWith(...)
split(...)
join(...)
And math:
sqrt(...)
floor(...)
ceil(...)
round(...)
abs(...)
min(...)
max(...)
pow(...)
random()
These are registered in the interpreter's global environment and bridge Sprout code to native TypeScript functionality.
At this point I had something that genuinely felt like a small programming language.
And naturally my next thought was:
Okay, let's make the language import files.
That innocent decision eventually resulted in me building a package manager.
The moment Sprout became more than one file
Suppose I write:
export grow add(a, b) {
harvest a + b;
}
Then somewhere else:
import { add } from "./math";
bloom(add(10, 20));
Now the interpreter needs to do quite a lot.
It has to:
- figure out where ./math points,
- add .sprout when necessary,
- read the file,
- tokenize it,
- parse it,
- execute it,
- collect its exports,
- expose the requested export to the importing module.
Sprout's core interpreter already understands the semantics of imports and exports: exported declarations are stored in a module export map, while imports ask the module loader for the referenced module and bind the requested names into the current environment.
But the core deliberately doesn't know anything about Node's filesystem.
That's where I created NodeInterpreter.
The core interpreter says, effectively:
I know what an import means.
The Node interpreter says:
I know how to find the file.
I liked that separation.
It means the language itself isn't inherently tied to Node's filesystem.
And then I tried importing a package
Relative imports are easy enough:
import { add } from "./math";
But what I actually wanted was this:
import { add } from "math-utils";
Now we have a different problem.
Where the fuck is math-utils?
JavaScript has node_modules.
Python has site-packages and environments.
Rust has Cargo.
Go has modules.
Sprout had...
nothing.
So I built one.
Meet garden_modules
Naturally I couldn't call it node_modules.
This is Sprout.
So installed packages live in:
garden_modules/
A Sprout project can look like:
my-app/
├── garden.json
├── garden-lock.json
├── index.sprout
└── garden_modules/
└── math-utils/
├── garden.json
└── index.sprout
And dependencies aren't called dependencies anymore.
They're grafts.
Because you're taking another plant and grafting it into your project.
Yes, I committed to the bit.
garden.json
Every Sprout project gets a manifest.
Something like:
{
"name": "my-app",
"version": "0.1.0",
"main": "index.sprout",
"grafts": {
"math-utils": "file:../math-utils"
}
}
This is basically Sprout's equivalent of:
package.json
It tells the toolchain:
- what this project is called,
- what version it is,
- where its entry point lives,
- which packages have been grafted into it.
For backwards compatibility, I even let the tooling understand the older terminology:
garden.json or sprout.json
garden-lock.json or sprout-lock.json
grafts or dependencies
garden_modules or sprout_modules
I didn't just rename things and break everything I'd already built.
Sprout can gradually migrate toward the gardening vocabulary.
Creating a project: cultivate
Instead of:
npm init
Sprout gets:
sprout cultivate
or:
sprout cultivate my-app
That creates:
garden.json
with an initial project definition:
{
"name": "my-app",
"version": "0.1.0",
"main": "index.sprout",
"grafts": {}
}
I still support:
sprout init
as an alias.
But come on.
cultivate is objectively funnier.
Installing a package: graft
Then I wanted this:
sprout graft ../math-utils
Sprout currently supports local packages, rather than downloading packages from a public registry.
So imagine:
projects/
├── calculator/
└── math-utils/
Inside math-utils:
math-utils/
├── garden.json
└── index.sprout
Its manifest might be:
{
"name": "math-utils",
"version": "1.0.0",
"main": "index.sprout"
}
Then from calculator:
sprout graft ../math-utils
Sprout resolves the package directory, reads its manifest, determines the package name and copies it into:
calculator/garden_modules/math-utils/
Then it modifies the application's garden.json:
{
"grafts": {
"math-utils": "file:../math-utils"
}
}
And prints:
grafted math-utils@1.0.0
At this point I realized:
Wait. I actually built package installation.
Not npm-scale package installation, obviously.
But the fundamental mechanism was there.
Then I needed a lock file
Because apparently once you start recreating package managers, the problems never stop.
I wanted Sprout to remember exactly what was installed.
So now there's:
garden-lock.json
It stores installed plants:
{
"plants": {
"math-utils": {
"version": "1.0.0",
"source": "file:../math-utils"
}
}
}
Again:
packages → plants
dependencies → grafts
node_modules → garden_modules
At this point the gardening metaphor had escaped containment.
Installing the whole garden
If garden.json already contains:
{
"grafts": {
"math-utils": "file:../math-utils",
"greeter": "file:../greeter"
}
}
I don't want to manually run:
sprout graft ../math-utils
sprout graft ../greeter
So:
sprout graft
with no package argument walks through the project's grafts and installs them.
That's essentially Sprout's current equivalent of:
npm install
The package manager is still intentionally tiny—the current implementation only accepts local file: dependencies—but the architecture is already there for other sources later.
A registry could eventually turn:
sprout graft lodash-equivalent
into something conceptually like:
Sprout CLI
↓
registry
↓
resolve version
↓
download package
↓
garden_modules/
↓
garden-lock.json
That's when this little experiment starts becoming a proper ecosystem.
Importing an installed package
Installing something is useless unless the language can find it.
So my module resolver handles two categories.
For:
import { foo } from "./foo";
it resolves relative to the importing file.
For:
import { add } from "math-utils";
it starts from the importing module's directory and searches upward for:
garden_modules/math-utils/
Once it finds the package, it reads its manifest and determines the entry point:
main
or defaults to:
index.sprout
It also supports package subpaths.
Conceptually:
import { foo } from "some-package/utils";
can resolve inside the package instead of always using its main entry point.
This is where I started appreciating how much invisible work something as ordinary as:
import express from "express";
actually represents.
That one line assumes the existence of an entire module resolution system.
I also cache modules
Suppose five different files import:
import { add } from "math-utils";
Sprout shouldn't read, parse and execute math-utils five times.
So NodeInterpreter maintains a module cache.
When loading a module:
resolve filename
↓
already cached?
↙ ↘
YES NO
↓ ↓
return create module
module ↓
read file
↓
parse
↓
evaluate
↓
cache
That also becomes important if modules eventually contain state.
Again, something I've relied on for years in existing languages suddenly became a design decision I had to make myself.
Removing packages became uproot
Obviously.
sprout uproot math-utils
removes the package from:
garden_modules/
removes its entry from:
garden.json
and removes it from:
garden-lock.json
The old command still works:
sprout uninstall math-utils
But:
sprout uproot math-utils
is considerably more Sprout.
And viewing dependencies became garden
Instead of:
npm list
I added:
sprout garden
which prints the project's grafts.
For example:
math-utils file:../math-utils
greeter file:../greeter
So the CLI now has this vocabulary:
sprout cultivate
sprout graft
sprout uproot
sprout garden
That's one of my favourite parts of the project.
It's not just another programming language where I renamed function to something quirky.
The metaphor starts extending through the tooling.
You don't initialize a Sprout project.
You cultivate one.
You don't install a dependency.
You graft it.
You don't uninstall it.
You uproot it.
You don't inspect dependencies.
You inspect your garden.
Running Sprout
The CLI also doubles as the actual language runner.
So:
sprout hello.sprout
reads the file and sends it through:
Source Code
↓
Lexer
↓
Tokens
↓
Parser
↓
AST
↓
Interpreter
↓
Output
The exported run() API exposes essentially that same pipeline programmatically.
And because the CLI checks whether the first argument is one of the package-management commands, the same executable handles both:
sprout app.sprout
and:
sprout graft ../math-utils
That makes Sprout feel less like an interpreter file sitting in a repository and more like an actual toolchain.
The language became surprisingly capable
What started as a lexer/interpreter experiment now supports quite a bit.
You can write:
seed name = "Sprout";
plant numbers = [1, 2, 3, 4, 5];
grow square(x) {
harvest x * x;
}
plant squared = map(numbers, square);
bloom(squared);
You have mutable variables and immutable constants.
Functions and closures.
Lists and objects.
Indexing.
Property access.
Arithmetic.
Comparisons.
Boolean logic.
Assignment operators:
+=
-=
*=
/=
%=
Increment/decrement:
++
--
Loops.
break.
continue.
Imports.
Exports.
String interpolation.
Higher-order collection functions.
Math functions.
And an actual module/package system.
The parser's AST supports those language constructs directly, rather than translating Sprout into JavaScript first.
That distinction is important to me.
Sprout isn't:
Sprout syntax
↓
convert to JavaScript
↓
Node runs JavaScript
It's:
Sprout syntax
↓
Sprout lexer
↓
Sprout parser
↓
Sprout AST
↓
Sprout interpreter
Node hosts the interpreter, but Sprout itself is being interpreted according to rules I wrote.
The package manager changed how I understood programming languages
Building the interpreter taught me what happens inside a language.
Building the package manager taught me how much exists around a language.
A useful programming language isn't only:
lexer + parser + interpreter/compiler
It's eventually:
LANGUAGE
│
┌───────────┼───────────┐
↓ ↓ ↓
Parser Runtime Modules
│
↓
Package Manager
│
┌───────────┼───────────┐
↓ ↓ ↓
Manifest Resolver Lockfile
│
↓
Registry
Then eventually you start thinking about:
formatters,
linters,
debuggers,
language servers,
test runners,
documentation generators,
registries,
version resolution,
dependency conflicts,
security,
publishing,
caching...
And suddenly you understand why programming language ecosystems are enormous projects.
And that's probably my favourite thing about Sprout
I didn't build Sprout because the world desperately needs another programming language.
Nobody was sitting around thinking:
Fuck. JavaScript is okay, Python is okay, Rust is okay... but where is the plant-based programming language?
😭
I built it because I wanted to understand things I'd been using almost every day for years.
I've written:
npm install
probably thousands of times.
It's completely mundane.
You type it.
Some text flies across the terminal.
A gigantic node_modules appears.
Done.
But once you try building even the tiniest version yourself, npm install stops looking simple.
Now I see:
read manifest
↓
resolve dependency
↓
find package
↓
determine version
↓
install files
↓
update manifest
↓
update lockfile
↓
make module resolver find it
↓
load entry point
↓
cache module
↓
expose exports
And my package manager currently supports the easiest possible version of that problem: local packages.
No remote registry.
No semantic version ranges.
No transitive dependency graph.
No integrity hashes.
No dependency deduplication.
No conflicting versions.
No authentication.
No publishing.
No security auditing.
No peer dependencies.
And even this tiny version taught me a ridiculous amount.
That's exactly why I like projects like this.
What's next?
The obvious next step would be turning Sprout's local package manager into an actual package ecosystem.
Imagine:
sprout graft fern
instead of:
sprout graft ../fern
Sprout contacts a registry.
Finds fern.
Resolves the requested version.
Downloads it.
Installs it into:
garden_modules/fern
Records the exact result in:
garden-lock.json
and then Sprout code simply does:
import { something } from "fern";
Then package publishing:
sprout publish
Version constraints:
{
"grafts": {
"fern": "^2.1.0"
}
}
Dependency trees.
Caching.
Checksums.
Maybe even:
sprout water
for updating packages.
Okay, maybe that one is going too far.
Actually no.
sprout water is absolutely happening.
😭
And after that?
A REPL.
Better error messages.
A formatter.
Testing.
Maybe static typing.
Maybe bytecode.
Maybe compile Sprout instead of interpreting it.
Maybe a language server so VS Code actually understands .sprout.
Maybe syntax highlighting.
Maybe package publishing.
Maybe a website where you can browse the garden.
There's an enormous amount left to build.
And that's exactly the point.
I understand npm install differently now
That's probably the best summary of this entire project.
Before Sprout:
npm install
was a command.
After building Sprout:
I see a manifest parser.
A dependency graph.
A resolver.
A filesystem layout.
A package source.
A lockfile.
A module loader.
An import system.
A cache.
A runtime.
And underneath all of that, another programming language implementing the package manager in the first place.
That's the reason I built Sprout.
Not because I think it's going to replace JavaScript.
Not because I'm planning to convince companies to rewrite their backend in:
plant server = ...
😂
I built it because there's a huge difference between:
knowing how to use something
and
knowing why it works.
I knew how programming languages worked in theory.
Lexer.
Parser.
AST.
Interpreter.
Yeah yeah.
I've read those words before.
Now I've written them.
I knew package managers resolved dependencies.
Now I've written a resolver.
I knew modules were cached.
Now I've had to decide where and when to cache them.
I knew package.json and lockfiles existed.
Now I've needed my own.
And somewhere between writing my lexer and implementing:
sprout graft ../math-utils
I stopped looking at programming languages as magical black boxes.
They're programs.
Extremely complicated programs, sure.
But still programs.
Someone decides what if means.
Someone decides how + behaves.
Someone decides how scope works.
Someone decides what happens when you import a module.
Someone decides where packages live.
Someone decides how dependencies are recorded.
Someone decides what an error looks like.
And for this tiny language—
I get to be that someone.
Project timeline
Package manager
Added a package manager