I Built a Functional Programming Language in C — Meet Pilang
For the past months, I’ve been building a programming language from scratch in C called Pilang.
It started as an experiment to better understand virtual machines, parsers, interpreters, and low-level systems programming. But over time, it evolved into something much bigger: a lightweight functional programming language focused on mathematical notation, tensor operations, and expressive real-world scripting.
The project is open source here:
In this article I want to share:
- Why I started building a language
- How Pilang works internally
- The features I implemented
- The hard problems I faced
- What I learned from building a VM from scratch
- Where I want to take the project next
Why Build a Programming Language?
Most programmers eventually ask themselves:
“Could I build my own language?”
For me, the answer became “I have to try.”
I didn’t want to build just another parser tutorial project. I wanted to create a language that could actually be useful for real programs while still remaining small and understandable.
I was especially interested in:
- Functional programming
- Mathematical syntax
- Tensor operations
- Lightweight scripting
- Runtime design
- Language simplicity
I also wanted complete control over the runtime.
Using C gave me direct access to:
- Memory management
- Performance optimizations
- VM design
- Bytecode execution
- Runtime systems
- Garbage collection
The goal became:
Build a small but expressive language that combines functional programming ideas with mathematical notation while still remaining lightweight and practical.
What is Pilang?
Pilang is a custom interpreted programming language written in C.
It includes:
- A custom lexer
- A parser
- Bytecode compiler
- Virtual machine
- Garbage collector
- Functional programming utilities
- Tensor and matrix operations
- Mathematical notation support
- Lightweight scripting features
One of the main ideas behind Pilang is making mathematical and functional code feel natural without forcing everything into object-oriented patterns.
For example, instead of writing:
nums.filter(...)
Pilang uses explicit functional operations:
filter(nums, ...)
This keeps transformations predictable and keeps the language centered around functions and data instead of objects and methods.
Example Syntax
Here’s a simple example:
fun fib(n) {
if n <= 1
return n
return fib(n - 1) + fib(n - 2)
}
println(fib(10))
Pilang also supports arrow functions and functional-style utilities:
let nums = [1,2,3,4,5]
println(map(filter(nums,
x -> x % 2 == 0),
x -> x * 10))
The language tries to stay lightweight and expressive without becoming overly complicated.
Building the VM
The virtual machine is the heart of the project.
The execution flow roughly looks like this:
Source Code
↓
Lexer
↓
Parser
↓
AST / Bytecode
↓
Virtual Machine
↓
Runtime Execution
At first, I underestimated how difficult VM design actually is.
Simple things quickly become complicated:
- Closures
- Garbage collection
- Variable scope
- Function calls
- Native functions
- Memory ownership
- Arrays and objects
- Error handling
One of the hardest parts was implementing closures correctly without objects being garbage collected too early.
I eventually moved toward a tri-color marking garbage collector after running into random crashes caused by premature collection.
That debugging process alone taught me more about runtime systems than any tutorial ever could.
Mathematical and Tensor Operations
One of the most important goals for Pilang is making mathematical programming feel natural.
The language includes built-in support for:
- Matrices
- Tensors
- Vector operations
- Matrix multiplication
- Dot products
- Cross products
- Numeric transformations
Instead of relying entirely on external libraries, many mathematical operations are implemented directly in the runtime for simplicity and performance.
I wanted mathematical code to feel like a core part of the language itself rather than an afterthought.
Functional Programming Utilities
Pilang also includes built-in functional programming helpers like:
mapfilterreduce- Iterators
- Arrow functions
Example:
let nums = [1, 5, 12, 15, 20];
let result = reduce(map(filter(nums, (x) -> x > 10), (x) -> x * 2), (a, b) -> a + b, 0);
println(result);
Implementing this inside a custom VM was surprisingly challenging because functions themselves become runtime objects.
That means closures, scopes, and references all have to work correctly with garbage collection.
Building the VM
The virtual machine is the heart of the project.
The execution flow roughly looks like this:
Source Code
↓
Lexer
↓
Parser
↓
AST / Bytecode
↓
Virtual Machine
↓
Runtime Execution
At first, I underestimated how difficult VM design actually is.
Simple things quickly become complicated:
- Closures
- Garbage collection
- Variable scope
- Function calls
- Native functions
- Memory ownership
- Arrays and objects
- Error handling
One of the hardest parts was implementing closures correctly without objects being garbage collected too early.
I eventually moved toward a tri-color marking garbage collector after running into random crashes caused by premature collection.
That debugging process alone taught me more about runtime systems than any tutorial ever could.
Why Functional Programming?
I’ve always liked how functional programming encourages thinking in terms of transformations instead of mutable state.
Instead of attaching behavior to objects everywhere, Pilang tries to keep functions explicit and composable.
This makes many operations easier to reason about, especially when working with:
- Numerical data
- Matrix operations
- Tensor transformations
- Data pipelines
- Scripting utilities
The language is not purely functional, but functional ideas strongly influence its design.
Hard Lessons Learned
Building a language sounds fun.
And it is.
But it also forces you to confront some brutal engineering problems.
Here are a few things I learned:
1. Memory management is hard
You don’t truly understand memory management until your interpreter starts randomly crashing after 30 minutes because one closure was freed too early.
2. Parsers become messy very quickly
Small syntax additions can explode parser complexity.
Adding assignment expressions, arrow functions, and custom operators required major parser refactoring.
3. Language design becomes complicated very quickly
Small syntax additions can have surprisingly large consequences.
Adding features like:
- Arrow functions
- Assignment expressions
- Functional transformations
- Tensor operations
- Mathematical notation
required major parser and VM refactoring
4. Performance matters everywhere
Even tiny inefficiencies become noticeable inside interpreters.
I spent a lot of time optimizing:
- Parser functions
- Object allocations
- Array handling
- Matrix operations
- VM dispatch loops
Why I’m Continuing the Project
Most hobby languages eventually die.
But Pilang has become more than a learning experiment for me.
It’s now a platform where I can:
- Experiment with language design
- Experiment with language design
- Learn compiler theory
- Explore VM optimization
- Improve functional programming systems
- Experiment with mathematical abstractions
The deeper I go into the project, the more I realize how much there still is to learn.
And honestly, that’s the fun part.
Future Plans
Some things I want to add next:
- Better package/resource system
- Better tensor operations
- Improved tooling
- Better documentation
- More optimized garbage collection
- Better debugging tools
- WebAssembly support
- Better package management
- More mathematical utilities
- Better standard library support
I’m also interested in making Pilang easier for other developers to try.
Right now the project is very low-level and experimental, but that’s also part of its identity.
Final Thoughts
Building a programming language completely changed the way I think about software.
It made me better at:
- C programming
- Debugging
- Memory management
- System design
- Performance optimization
- Math
- Tooling
- Software architecture
More importantly, it reminded me why programming is fun.
Sometimes the best projects are the ones that seem impossibly ambitious at first.
Pilang is still evolving, but it has already taught me more than almost any other project I’ve worked on.
If you’re interested in interpreters, virtual machines, functional programming, or language design, feel free to check out the repository and follow the project.
GitHub:
Top comments (1)
Compiler theory is fun! I have a question - why would you need to add WebAssembly support, instead of having it baked in (as C already compiles to WebAssembly)? I'm guessing the answer has to do with Pilang being interpreted and not compiled.