One year ago, I didn't know how to write a single line of code. I was trapped in what the community calls "tutorial hell," building generic web apps and feeling like I wasn't pushed enough. I had this lingering fear that if I only stuck to trivial, high-level projects, Iâd never truly understand how computers work under the hood.
So, 30 days ago, I decided to take the ultimate plunge into low-level engineering. I decided to build my own programming language from scratch.
Meet Arc (not to be confused with the Lisp dialect!): an interpreted language powered by a custom, stack-based bytecode virtual machine written entirely in pure C. It features class-based abstractions, automatic list/string structures, custom error handling (TRY/CATCH), file I/O, and its own modular file-import system.
Here is a quick look at what native Arc code looks like running on my runtime:
IMPORT "math.arc"
FN printSum(a, b) THEN
print(a + b)
END
VAR i = 0
WHILE i < argc THEN
print("Arg", i, ": " + argv[i])
i = i + 1
END
The entire project is completely free and open-source. If you want to dive into the codebase, see the compilation pipeline, or test it out yourself, consider dropping a star on the repository!
Link to Arc Repository on GitHub
The Twist: Completing "Easy Mode" on "Nightmare Mode"
When I started, I didn't know how to write an abstract syntax tree or a compiler pipeline. I found a couple of great videos demonstrating how to build a basic mathematical parser and conditions, but they were written in Python. Instead of copy-pasting the instructor, I forced a brutal constraint on myself: Watch the concepts in Python, but implement them entirely in pure C.
This instantly transformed the project from a standard tutorial into a low-level gauntlet.
- Python handles variable sizes, dynamic strings, arrays, and type objects completely behind the scenes.
- In C, I had no safety nets. Every time I wanted to append to a list, read a file, or create an object, I had to manually wrestle with memory tracking, pointers, and structures.
The Architectural Evolution:
From Tree-Walking to Bytecode VM
Originally, Arc started as a standard tree-walk interpreter. It evaluated the Abstract Syntax Tree recursively line-by-line. It worked, but it was slow, and I wanted to know how real industrial languages executed code.
So, I ripped the execution engine out and replaced it with a Stack-Based Bytecode Virtual Machine.
[Source Code] ---> [Lexer] ---> [Parser/AST] ---> [Bytecode Compiler] ---> [Custom VM Stack Engine]
Instead of running instructions straight from the parse tree, Arc now compiles code down into raw, compact bytecode array instructions, passing them into an execution loop that mutates values across a lightweight custom VM evaluation stack.
Moving Off-Script (Building Features In the Dark)
After the first few videos, I stopped watching tutorials completely. The remaining features were coded purely by reading documentation, drawing memory layouts on paper, and running experiments. I built:
- Scope-Aware Variable Lookups: Managing standard and nested scopes across execution blocks.
- Complex Loops: Native execution of WHILE and index-swapping FOR...IN loops.
- Explicit Class Architectures: Grouping data and methods seamlessly without an implicit this/self binding requirement, requiring users to explicitly pass data structures back into encapsulated functions.
- Native C Function Bindings: Exposing high-performance C standard utilities to the Arc runtime environment.
The Memory Battle: Malloc, Arenas, and AddressSanitizer
Replacing Python's dynamically typed objects with real, explicit C structs was by far the hardest engineering challenge I have ever tackled.
Early on, every string initialization, list manipulation, and class instantiation was handled with messy, individual malloc and calloc calls. This quickly turned into an absolute nightmare of memory leaks, dangling pointers, and segmentation faults.
During weeks two and three, Valgrind and AddressSanitizer (ASan) became my absolute best friends. I spent entire nights tracking down single byte offsets that were corrupting the runtime stack.
To bring order to the chaos, I ended up rewriting major portions of the core architecture to rely on Memory Pools and Arenas. Instead of blasting the heap with thousands of tiny allocations, the runtime requests large chunks of contiguous memory up front and partitions it safely out to strings, structures, and arrays.
What It Feels Like to Run Your Own Runtime
There is a genuinely crazy, indescribable satisfaction that comes from writing a text script, passing it into a binary you wrote, and seeing your custom VM process data perfectly, catch its own errors safely via a TRY/CATCH block, and exit with 0 memory leaks.
TRY
VAR file = open_file("missing.txt", "r")
CATCH e THEN
print("Caught expected runtime error safely:", e)
END
Building Arc proved to me that systems programming isn't an exclusive club reserved for veterans who have been coding for twenty years. It's accessible to anyone willing to suffer through the segmentation faults, look closely at the memory addresses, and put in the hours.
What's Next for Arc?
The language is evolving rapidly. My next milestone is to abstract file handles into an entirely native object-oriented file architecture (class File).
The codebase is structured to be highly readable for anyone else who wants to learn how compilers and interpreters work without getting lost in millions of lines of corporate boilerplate.
- Have you ever attempted to write a low-level runtime engine?
- What strategies do you prefer when managing custom type objects inside an Arena? Let me know your thoughts or feedback in the comments below! Check out the source code, view the standard libraries, and track my updates on GitHub!
Top comments (0)