The Story

Why I Built a Programming Language at 17

Vexel started as a frustrated teenager's weekend project and turned into something I'm genuinely proud of. This is the full story: the motivation, the failures, the late nights, and how a language I never planned to finish ended up compiling real programs.

The Problem: Python Was Killing My Games

I've been writing Python since I was around 13. It was my first real language, and I loved it. The syntax made sense. The feedback loop was fast. You could write something and run it in seconds.

Then I started trying to make games. Real games. Ones where something is always moving, collisions are checked on every frame, and the physics update 60 times per second. That's when the cracks started showing. My Python game would run at a stuttering 20 frames per second on a machine that should have been handling it with no trouble at all.

I tried everything. NumPy for math, Cython for hot paths, PyPy as the runtime. Some things helped. Nothing fixed it. At some point I realized that the problem wasn't my code or my approach. The problem was the fundamental nature of a dynamically typed, interpreted language running on a GIL-locked runtime trying to do real-time graphics work.

So I looked at the alternatives. C was fast but felt hostile. C++ was even faster but the syntax made me feel like I was reading a legal contract. Rust was fascinating but had a learning curve that felt like climbing a mountain in flip flops. Go was clean but wasn't built for game dev.

I wanted something that looked and felt like Python but compiled down to native code. I looked around and couldn't find anything that scratched that specific itch. So the thought that had been sitting at the back of my head for months finally surfaced: what if I just build it?

Starting from Zero: What Even Is a Compiler?

I had essentially no idea how compilers worked. I had heard the word "lexer" before but I couldn't have explained what it did. I had zero formal computer science education. I was just a teenager who had been teaching himself to code from YouTube tutorials and Stack Overflow.

The first week was mostly reading. I found "Crafting Interpreters" by Robert Nystrom and read the first few chapters. I watched a dozen YouTube videos on how parsers work. I learned what an Abstract Syntax Tree was and drew one out on paper for a tiny piece of fake code just to make sure I understood it.

The pipeline started to make sense: you take raw source text, break it into tokens (the lexer), group those tokens into a tree that represents the structure of the program (the parser), check that the tree is logically valid (the analyzer), and then walk the tree and emit code (the code generator).

What I still didn't know was how to get from "a tree of nodes" to "actual machine instructions." That's where LLVM came in.

Discovering LLVM

LLVM is essentially a toolkit that does the hard parts of compiling: optimizing code, handling different CPU architectures, and converting an intermediate representation into actual machine code. Languages like Clang (C/C++), Rust, Swift, and many others all use LLVM as their backend.

I found a Python binding called llvmlite, which lets you build LLVM Intermediate Representation (IR) programmatically using Python objects. That meant I could write my compiler in Python, which I already knew, while still generating fast native binaries at the end.

The first time I got LLVM to actually emit an executable was one of the most satisfying moments I've had writing code. I had written a tiny program: a function that took two integers and returned their sum, called from a main that printed the result. I ran my compiler on it, it produced an object file, I linked it, and typed the program name in the terminal. It printed a number. The correct number.

That moment took about three weeks of evenings and weekends to reach. I sat back in my chair and just stared at the terminal for a while.

The Design Decisions

From the beginning I had a clear picture of what I wanted the language to feel like: Python's indentation and clean expression syntax, but with mandatory type annotations and a compiled, statically typed backend.

I made a decision early on to use fn instead of def because fn signals "this is compiled and typed" in a way that def never could. I chose let and const over Python's implicit assignment because the intent is clearer.

Structs came from my game dev needs. A game has players, enemies, bullets, projectiles. You need a way to group related data together. I built struct support, field access, and methods. Then I built interfaces and the impl X for Y syntax so that different types could satisfy the same contract.

Match statements came from the realization that if/elif/else chains on enum values get ugly fast. I built a match expression that works on both enum variants and type patterns, which makes game-state logic much cleaner.

SDL2 support was the feature that brought the whole thing full circle. Being able to write a game loop in Vexel, compile it with --sdl2, and see a window open with a bouncing square running at 300+ fps felt like the original goal finally closing.

The Hardest Part: The Garbage Collector

The garbage collector took longer than anything else. My first attempt was a basic reference counter, which worked until I tried to allocate a circular data structure and watched the program leak memory indefinitely.

I scrapped it and built a mark-and-sweep collector. The idea is simpler in concept: every allocation is tracked in a global heap list. When the GC runs, it starts from the known roots (stack variables, globals) and marks every object it can reach. Anything left unmarked is dead and gets freed. Then the mark bits are cleared and you start again.

Getting this to work correctly with LLVM-generated code, where memory layouts are not exactly transparent, meant I had to instrument every allocation with a header containing type metadata and pointer offsets. The compiler now emits that metadata automatically so the GC knows which fields in each struct are themselves pointers.

The day the GC tests all passed, I genuinely celebrated. That felt like crossing from "toy project" to "real language."

Where It Stands Now

Vexel v0.1 is a real, working compiler. It handles the full language: functions, variables, structs, enums, interfaces, generics, lambdas, pattern matching, try/catch, imports, type aliases, SDL2 graphics, and a working garbage collector. It compiles programs to native binaries that run on Windows without needing anything else installed.

It is also absolutely not finished. The standard library is tiny. Error messages could be much better. Linux and macOS support is not there yet. There is no package manager, no formatter, no language server.

But the foundation is solid, and I ship. v0.2 is already in progress.

A Note to Anyone Who Wants to Build Something

When I told people I was building a programming language, the most common reaction was some version of "that's really ambitious" said in a tone that mostly meant "that sounds impossible for you specifically." I was seventeen, teaching myself out of blog posts and documentation. There was no reason to expect it to work.

The thing is, "ambitious" is not actually a warning. It just means the project is interesting enough that it will take a while. Every problem in building a compiler has a solution. The solutions are documented. They have been figured out before. You just have to be stubborn enough to find each one.

If you are reading this and you have a project you've been putting off because it feels too big: start anyway. Ship the first version before it's ready. You can fix it later. The most important version of any project is the one that actually exists.

C

Cliff

Creator of Vexel. 17 years old. Self-taught.

Interested in compilers, game dev, and systems programming. Building Vexel as a solo project in my spare time.

GitHub Blog Community