Skip to content

basperheim/interpreted-language-example-clang

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Natrix Interpreter (Clang)

Created Using ChatGPT 5

This repository contains Natrix (.nrx), a tiny strictly typed interpreted language implemented in C (compiled with Clang). The interpreter is intentionally small and readable so you can dig into how tokenization, typed storage, arithmetic, and control flow might work when crafting a toy language from scratch.

Language features

  • str, num, and bool variable declarations
  • Strong typing with reassignment checks (only reuse the original type)
  • Numeric literals accept integers or decimals and support + - * / with parentheses and unary negation
  • Boolean expressions support equality/relational operators plus &&, ||, and unary !
  • Strings concatenate with + (only str + str)
  • print accepts literals or identifiers (with or without parentheses) and renders values by type
  • if / else and while blocks with curly braces (nesting supported)
  • try { ... } catch { ... } blocks; when an error is caught the block receives str error_message and num error_line
  • Typed functions with defaults: func add(num left, num right=0) : num => { return left + right; }
  • obj values for string-keyed objects with literal syntax ({ key: value }) and property access via dot or string index lookups
  • Line comments start with //
  • Source files must use the .nrx extension (the interpreter refuses other extensions)
  • const declarations for immutable bindings
  • Built-in metadata helpers: sys.platform, sys.version, sys.exec_path, sys.script_path, sys.arg_count, sys.banner, and first-line shebang support (#! is ignored when present)

TODOs (Nice to Have)

  • Add collections (arrays or maps) with strict element typing.
  • Introduce a bytecode layer for easier optimization.
  • Expose a small FFI to call C functions safely.
  • Experiment with immutable bindings (const) and deeper immutability rules.
  • Implement async/await, child processes, or something like go routines? Is this feasible?

Conventions and best practices

  • Keep one statement per line; terminate statements with ; to avoid parse surprises.
  • Prefer declaring variables close to their use and keep scopes small—{ } introduce new lexical scopes, while globals (declared before any function runs) stay accessible to every function.
  • Variable and parameter names must be snake_case; function names must be CapitalCase (errors are thrown otherwise).
  • Prefer const for immutable bindings; reassignment to a const identifier raises an error.
  • Save programs as .nrx files and run them with the natrix binary; other extensions are rejected.
  • Always provide a default or call-site value for function parameters as needed; defaults must match the declared type.
  • Stick with double quotes for strings; there is no escaping yet, so prefer simple ASCII literals.
  • Raise deliberate errors with mismatched types inside try blocks when you want to demonstrate catch handling.
  • Reuse helper functions instead of duplicating logic; functions are strongly typed and reusable across files.

Here's a drop-in replacement for your Quick start section—expanded, clean, and platform-specific.


Quick start

TL;DR: build a single natrix binary from main.c, then run .nrx files. The interpreter refuses non-.nrx extensions by design.

macOS (Apple Silicon, arm64)

# Debug build with sanitizers (recommended while hacking):
clang -std=c11 -Wall -Wextra -g -fsanitize=address,undefined -o natrix main.c

# Or a small release build:
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c

# Run:
./natrix sample.nrx
# Print just the interpreter version:
./natrix --version
# Suppress the version banner when running:
./natrix --quiet-version sample.nrx

macOS (Intel, x86_64)

# If you need to force arch on a universal Clang:
clang -std=c11 -Wall -Wextra -g -fsanitize=address,undefined -arch x86_64 -o natrix main.c
# Or release:
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -arch x86_64 -o natrix main.c

./natrix sample.nrx
# And the same optional flags:
./natrix --quiet-version sample.nrx

Linux (x86_64)

# Using GCC
gcc -std=c11 -Wall -Wextra -g -fsanitize=address,undefined -o natrix main.c
# Or Clang
clang -std=c11 -Wall -Wextra -g -fsanitize=address,undefined -o natrix main.c

# Release build:
gcc   -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c
# or
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c

./natrix sample.nrx
./natrix --version
./natrix --quiet-version sample.nrx

Linux (ARM64 / aarch64)

# Clang or GCC both fine on ARM64:
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c
# or
gcc   -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c

./natrix sample.nrx
./natrix --version
./natrix --quiet-version sample.nrx

Windows (MSYS2 / MinGW-w64)

# In an MSYS2 MinGW64 shell (pacman -S mingw-w64-x86_64-gcc):
gcc -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix.exe main.c

# Debug:
gcc -std=c11 -Wall -Wextra -g -o natrix.exe main.c

# Run (PowerShell or CMD):
.\natrix.exe sample.nrx
.\natrix.exe --version
.\natrix.exe --quiet-version sample.nrx

Windows (Visual Studio "Developer Command Prompt")

:: Use the VS x64 Native Tools Command Prompt
cl /std:c11 /W4 /MD /O2 /DNDEBUG main.c /Fe:natrix.exe
:: Debug:
cl /std:c11 /W4 /Zi main.c /Fe:natrix.exe

natrix.exe sample.nrx

Windows via WSL (Ubuntu)

sudo apt update && sudo apt install -y build-essential clang
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c
./natrix sample.nrx

Verify extension enforcement

# Will run:
./natrix sample.nrx

# Will be rejected with an error (by design):
./natrix sample.txt

Pro tips

  • Dev fast, fail loud: build with -g -fsanitize=address,undefined (Clang/GCC) until stable.
  • Ship small: -O2 -DNDEBUG for releases.
  • Static on Linux (optional): -static (GCC) can produce a fully static binary, but expect larger size and glibc/musl caveats.
  • Cross-compile: use CC=aarch64-linux-gnu-gcc (or Clang with --target=) to produce ARM64 binaries from x86_64.

Language guide

Declarations

str greeting = "hello world";
num count = 42;
num ratio = count / 2;
bool ready = true;
obj config = { host: "localhost", port: 8080 };

Identifiers must start with a letter or underscore and contain only letters, digits, and underscores.

Reassignment

count = count + 1;
greeting = "updated";

Reassignment requires the identifier to be declared already and enforces type safety.

Const declarations

const num max_workers = 4;
const str greeting_prefix = "hello, ";

const bindings must be initialized immediately and cannot be reassigned. Attempting to assign to a const identifier raises a runtime error.

Print statements

print(greeting);
print count;
print "literal strings work too";

print accepts parentheses or whitespace separation, and you can pass literals or identifiers. Values must be str, num, or bool; printing obj values is not supported.

String concatenation

str greeting = "hello";
str name = "world";
str combined = greeting + ", " + name;
print combined;

Only string operands may be concatenated with +.

Conditionals

if ready && count >= 10 {
    print("ready!");
} else {
    print("not ready");
}

Blocks are wrapped in { ... }. else if chains are supported by writing else if <expr> { ... }.

Loops

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

Loop conditions re-evaluate before every iteration and must be bool.

Functions

func Shout(str name, str suffix="!") : void => {
    print(name + suffix);
}

func Scale(num value, num factor=2) : num => {
    return value * factor;
}

Shout("hello");
num doubled = Scale(21);

Functions require explicit parameter and return types. Default values are optional but must match the parameter type. Non-void functions must return a value.

Try / Catch

try {
    num result = Scale("oops");
} catch {
    print("error: " + error_message);
}

Errors raised inside the try block populate error_message and error_line inside catch.

Objects

obj values are string-keyed maps. Create them with object literals and access fields with dot notation when the key is identifier-friendly, or bracket notation for dynamic strings.

obj config = {
    host: "localhost",
    port: 8080,
    ssl: false
};

str host = config.host;        // dot notation
num port = config["port"];     // bracket notation for string keys
bool secure = config.ssl;

Object literals accept identifiers or string literals as keys. Property lookups require the target value to be obj, and printing objects directly is unsupported—print individual fields instead. Object values are created with their initial keys and currently cannot be mutated from within Natrix code.

Comments

Anything after // on a line is ignored.

Metadata, versioning, and shebangs

  • Place #!/usr/bin/env natrix (or any shebang) on line 1; it is ignored by the interpreter.
  • sys.platform, sys.version, and sys.banner expose platform, version, and banner strings.
  • sys.exec_path and sys.script_path expose the executable path and the currently running script.
  • sys.arg_count returns the integer count of process arguments (argc).
  • Dot notation works with obj values (including the built-in sys object); use bracket indexing when you need dynamic string keys.
  • Run natrix --version to print the banner and exit, or natrix --quiet-version script.nrx to suppress the banner when executing a file.

Example program

See sample.nrx for a ready-to-run script that demonstrates const bindings, arithmetic, conditionals, loops, try/catch, metadata helpers, and naming conventions.

Contributions

Contributions and PRs are welcome—try adding new types, expressions, or control flow!

License

This project is licensed under the MIT License.

About

Provides a basic understanding of building an interpreter for a custom programming language.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published