Tejas GK

Sprout programming language

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:

  1. figure out where ./math points,
  2. add .sprout when necessary,
  3. read the file,
  4. tokenize it,
  5. parse it,
  6. execute it,
  7. collect its exports,
  8. 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

  1. Package manager

    Added a package manager