Create Your Own Programming Language (Natrix Example)
ChatGPT & Benji AsperheimThu Oct 9th, 2025

Create Your Own Programming Language (Natrix Example)

Natrix is a tiny, strictly typed, interpreted language implemented in C. It’s built to be read, modified, and extended—so you can learn real language design without drowning in a huge codebase. The interpreter shows how tokenization, parsing, and runtime evaluation fit together, with just enough features to feel practical.

Why build an interpreted language in C at all?

Natrix leans into clarity over cleverness. You’ll see strict types, simple control flow, and a small standard library. From there, you can layer in objects without classes, method-call sugar, and even PCRE2-powered regex—incrementally and intentionally. You can browse the Github repo source code to dig into the details and contribute.

Create a Programming Language

You don’t need a giant compiler course to start. A practical path:

A minimal Natrix script looks like this:

// Types: str, num, bool. Functions have typed params and returns.
func Add(num a = 0, num b = 0) : num => { return a + b; }

num x = Add(2, 3);
print x;        // 5

bool flag = true;
if (flag) { print "Hello, Natrix!"; } else { print "Nope"; }

This is enough to get a working system and a place to add features later (records, regex, and so on).

Create A Programming Language

When you build yours, decide early on:

Example of control flow and error handling:

func Divide(num a, num b) : num => {
  if (b == 0) { throw "division by zero"; }
  return a / b;
}

try {
  num v = Divide(10, 2);
  print v;          // 5
  v = Divide(1, 0); // throws
  print v;          // never reached
} catch {
  // Built-in: error_message, error_line (names may vary in your build)
  print "Caught: " + error_message;
}

Create Your Own Programming Language

Here’s a pattern that keeps things simple while offering “OO-ish” ergonomics without real classes:

// Data schema (nominal)
struct Parrot { name: str; wants: str; age: num }

// A plain function that *looks* like a method when called with sugar
func Speak(Parrot p, str what) : void => {
  print p.name + ": " + what;
}

Parrot polly = obj { name: "Polly", wants: "cracker", age: 2 };

// Method sugar: polly.Speak("hello") → Speak(polly, "hello")
polly.Speak("hello");    // Polly: hello
polly.age = 3;
print polly.age;         // 3

This gives you data modeling, type checks on fields, and clean ergonomics. No classes, no inheritance, no giant runtime.

Interpreter Language

An interpreter executes your AST (or bytecode) directly. That means:

A tiny loop with state:

num i = 0;
while (i < 3) {
  print "tick: " + i;
  i = i + 1;
}

What Is Interpreted Language

It’s a language where the primary execution model is evaluate-as-you-go, not ahead-of-time compilation to a native binary. Interpreted systems often have:

Natrix shows these traits but keeps types strict for clarity:

str platform_label = sys.platform;
str version_label  = sys.version;

print platform_label;   // e.g., "darwin"
print version_label;    // e.g., "0.2.0"

Interpreted Languages Examples

Famous examples include Python, Ruby, and Lua. Each prioritizes speed of development. Natrix follows that spirit with stricter typings and a small core designed for learning:

func Greet(str name = "world") : void => {
  print "Hello, " + name + "!";
}

Greet();            // default param
Greet("Natrix");    // explicit

Compiled Language Vs Interpreted Language

Both models are useful; they just optimize for different moments:

You can mix approaches. Many interpreters add a bytecode layer or optional JIT later for hot paths. Start simple, then optimize what matters.

How Natrix Was Designed And Built

Natrix’s aim is clarity over cleverness. The implementation choices reflect that:

You can start with “tiny” print, sys.platform, and sys.version calls, and then implement regex through a C library like PCRE2 for performance with minimal code. Idiomatic built-ins:

bool ok = RegexMatch("[A-Z]{3}[0-9]{2}", "ABC12 xyz");   // true
num  i  = RegexSearch("error: .*", "warn\nerror: oops"); // start index
str  s  = RegexReplace("(\\w+)-(\\d+)", "item-42", "<\\1 #\\2>");
print ok;
print i;
print s;

Use the const (shallow) keyword for bindings you don’t want reassigned:

const Parrot p = obj { name: "Kiwi", wants: "seeds", age: 4 };
p.age = 5;        // allowed (field mutate)
// p = obj { ... }  // error: cannot reassign const binding

TL;DR

Build Tip (If You Add PCRE2)

Break long commands for readability:

clang -O2 -std=c17 -Wall -Wextra -o natrix \
  src/lexer.c src/parser.c src/runtime.c src/main.c \
  -lpcre2-8

That’s it. You’ll have a compact, teachable system that’s fun to extend and easy to reason about.


Conclusion

You don’t need a massive compiler stack to learn language design. With Natrix you get a compact interpreter that favors strict, readable rules: typed variables and functions, clean control flow, and objects without classes via struct, obj { ... }, and call sugar. From there you can add practical power—like PCRE2 regex—without inflating the core.

If you’re curious where to go next:

Grab the code, run the examples, and try a feature or two of your own. Small, comprehensible systems are the best place to learn—and Natrix is designed exactly for that.