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.
str
,num
, andbool
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
+
(onlystr + str
) print
accepts literals or identifiers (with or without parentheses) and renders values by typeif / else
andwhile
blocks with curly braces (nesting supported)try { ... } catch { ... }
blocks; when an error is caught the block receivesstr error_message
andnum 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)
- 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?
- 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 aconst
identifier raises an error. - Save programs as
.nrx
files and run them with thenatrix
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.
TL;DR: build a single
natrix
binary frommain.c
, then run.nrx
files. The interpreter refuses non-.nrx
extensions by design.
# 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
# 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
# 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
# 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
# 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
:: 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
sudo apt update && sudo apt install -y build-essential clang
clang -std=c11 -Wall -Wextra -O2 -DNDEBUG -o natrix main.c
./natrix sample.nrx
# Will run:
./natrix sample.nrx
# Will be rejected with an error (by design):
./natrix sample.txt
- 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.
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.
count = count + 1;
greeting = "updated";
Reassignment requires the identifier to be declared already and enforces type safety.
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(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.
str greeting = "hello";
str name = "world";
str combined = greeting + ", " + name;
print combined;
Only string operands may be concatenated with +
.
if ready && count >= 10 {
print("ready!");
} else {
print("not ready");
}
Blocks are wrapped in { ... }
. else if
chains are supported by writing else if <expr> { ... }
.
num counter = 0;
while counter < 3 {
print("loop tick");
counter = counter + 1;
}
Loop conditions re-evaluate before every iteration and must be bool
.
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 {
num result = Scale("oops");
} catch {
print("error: " + error_message);
}
Errors raised inside the try
block populate error_message
and error_line
inside catch
.
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.
Anything after //
on a line is ignored.
- Place
#!/usr/bin/env natrix
(or any shebang) on line 1; it is ignored by the interpreter. sys.platform
,sys.version
, andsys.banner
expose platform, version, and banner strings.sys.exec_path
andsys.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-insys
object); use bracket indexing when you need dynamic string keys. - Run
natrix --version
to print the banner and exit, ornatrix --quiet-version script.nrx
to suppress the banner when executing a file.
See sample.nrx
for a ready-to-run script that demonstrates const bindings, arithmetic, conditionals, loops, try/catch, metadata helpers, and naming conventions.
Contributions and PRs are welcome—try adding new types, expressions, or control flow!
This project is licensed under the MIT License.