DEV Community

Roland Brake
Roland Brake

Posted on

I Built a Programming Language and Tiny Game Engine in C — Meet Pilang

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(...)
Enter fullscreen mode Exit fullscreen mode

Pilang uses explicit functional operations:

filter(nums, ...)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  • map
  • filter
  • reduce
  • 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);

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

https://github.com/rolandbrake/pilang

Top comments (1)

Collapse
 
ender_minyard profile image
ender minyard

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.