Modern syntax & features. C output. No magic.
btrc is a statically-typed language that transpiles to C. It adds classes, generics, type inference, lambdas, f-strings, imports, collections, threads, GPU compute, automatic reference counting, exception handling, and a growing standard library -- all while staying compatible with C. The generated C is strict C11: no compiler extensions, no garbage collector, and no virtual machine. Core CPU programs embed the small helpers they use; optional GPU, GUI, tray, and other native backends link their platform runtimes explicitly. You can inspect, debug, and link the output with a C11 toolchain. It comes with a VS Code extension, a language server, and hundreds of compiler/language tests.
And no – it's not actually better than C, but I like the name, which I ripped off from btrfs.
Here's an example:
#include "engine/engine.btrc"
int main() {
var engine = Engine("btrc 3D Ball", 800, 600);
var player = new GameObject();
float speed = 4.0;
while (engine.isRunning()) {
engine.update();
float dt = engine.time.deltaTime;
if (engine.input.key(KEY_W)) { player.move(0.0, 0.0, speed * dt); }
if (engine.input.key(KEY_S)) { player.move(0.0, 0.0, -speed * dt); }
if (engine.input.key(KEY_A)) { player.move(speed * dt, 0.0, 0.0); }
if (engine.input.key(KEY_D)) { player.move(-speed * dt, 0.0, 0.0); }
if (engine.input.key(KEY_SPACE)) { player.jump(speed); }
player.applyPhysics(dt);
engine.render(player);
}
return 0;
}
I’ve wanted a modern, ergonomic take on C for years: something fast, simple, cross-platform, built with intent, and featuring (iffy) built-in GPU support. btrc is a personal project that tries to scratch this itch, and I've had it on the backburner for years. I never had the time (and honestly, I still don't), but with the help of AI, I've managed to bring it to life over some late night hacking. The experience of using AI to create an ambitious project from scratch made the project worth it. Also, the irony isn't lost on me: I'm fully aware of how silly it is to use AI to write a programming language in a time where we are writing less and less code ourselves.
btrc is defined through a formal EBNF grammar, which mathematically defines every keyword and operator; an algebraic AST spec defines every node type for the language graph; and a compiler pipeline consumes both the spec and the graph, walking your code through six stages (lexical analysis, syntax analysis, semantic analysis, intermediate code generation, code optimization, code generation). However, instead of outputting an intermediate language like LLVM or assembly code directly, it outputs C code. I don't expect folks will want to look at the C code outside of debugging errors, but it should resemble something that a human could have written (but more verbose and with a lot more underscores). You should be able to read it, debug it, and link it anything (or link anything else to it). It's just C11.
Depending on how you define things, it might be more accurate to call btrc a transpiler rather than a compiler. You get gcc and clang compatibility for free, but you also inherit many of C's limitations. There is no Rust-style borrow checker here. The analyzer does enforce managed-value ownership and lifetime rules at call, assignment, projection, aggregate, and exception boundaries, while ARC handles most managed-object cleanup (including cycles and allocations unwound by exceptions). Raw pointers and explicit destruction remain C-like, so the compiler still cannot prevent every use-after-free or dangling-pointer bug.
Probably not. But you're welcome to contribute if you find this kind of thing fun.
If you need a production systems language with full safety guarantees, use Rust, Zig, Odin, or C3. Those languages are more mature, robust, and real.
Plus, btrc definitely has bugs.
# Option 1: Nix (recommended — all dependencies handled)
nix develop
make build
nix run .#btrc -- hello.btrc -o hello.c
# Option 2: Devcontainer (VS Code)
make devcontainer # build container image
# then "Reopen in Container" in VS Code
# Option 3: Manual (Python 3.13+, gcc, pytest required)
make build
# Compile and run a program
./bin/btrcpy hello.btrc -o hello.c
gcc hello.c -o hello -lm
./hello
# Strict imports are the default. Legacy projects can opt out temporarily.
./bin/btrcpy tool.btrc -o tool.c
./bin/btrcpy --relaxed-imports tool.btrc -o tool.c
# Or use the Python compiler directly
python3 -m src.compiler.python.main hello.btrc -o hello.cThe flake exports the compiler as packages.<system>.btrcpy,
packages.<system>.btrc, and apps.<system>.btrc, so downstream flakes can
depend on BTRC directly instead of shelling into this repository.
Useful compiler modes include:
# Build the stdlib once, then emit program-only C against that archive.
./bin/btrcpy --build-stdlib build/stdlib
./bin/btrcpy --stdlib build/stdlib app.btrc -o app.c
# Reassert the strict default and bypass the transpilation cache.
./bin/btrcpy --strict-imports --no-cache app.btrc -o app.c
# Temporarily compile a legacy project with implicit cross-file visibility.
./bin/btrcpy --relaxed-imports app.btrc -o app.c
# Keep all generated declarations for inspection, or profile compiler phases.
./bin/btrcpy --no-dce app.btrc -o app.c
./bin/btrcpy --profile app.btrc -o app.cSee the precompiled-stdlib design for the
archive layout and cross-translation-unit ownership contract. Run
./bin/btrcpy --help for the complete current option list.
| C Pain Point | btrc Solution |
|---|---|
| No classes | Full OOP: classes, inheritance, interfaces, abstract classes, properties |
| No generics | Monomorphized generics (Vector<T>, Map<K,V>, user-defined) |
| No memory management | ARC (Automatic Reference Counting) |
| No type inference | var x = 42; just works |
printf formatting |
f-strings: f"x = {x + 1}" |
| Ad hoc include order | import std.{json, process}, import ./src/**, plus old #include compatibility |
| No collections | Vector<T>, Map<K,V>, Set<T>, List<T>, Array<T> with rich APIs |
| No lambdas | Arrow lambdas: (int x) => x * 2 |
| No exceptions | try/catch/finally with ARC-safe cleanup on throw |
| No operator overloading | __add__, __sub__, __eq__, __neg__ |
| No string methods | .len(), .contains(), .split(), .trim(), .toUpper(), and many more |
| No threads | spawn + Thread<T> + Mutex<T> |
| No GPU compute | @gpu functions transpile to WGSL shaders with auto-generated WebGPU boilerplate |
| Null pointer chaos | Nullable types (T?), optional chaining ?., null coalescing ?? |
| Manual memory only | Automatic reference counting with keep/release + cycle detection |
Strict imports are the language, API, and CLI default. Every source file must
import the files that own the top-level language symbols it references. An
import edge is directed and transitive: an importer sees its dependency, but
the dependency does not see back into the importer, and sibling imports do not
see one another automatically.
The compiler still supports C-style #include "file.btrc" for compatibility.
btrc includes are textual compilation-unit composition, so include-connected
fragments share visibility in both directions. New modular code should prefer
import; --relaxed-imports is the explicit legacy opt-out.
import std.{cli, fs, json, process, toml, ui}
import std.*
import ./src/core/*
import ./src/**
Supported forms are:
std.namefor one standard-library modulestd.{a, b, c}for a small ordered setstd.*orstd.**for the discovered standard library- relative files such as
./helpers/message.btrc - directory globs with
./dir/* - recursive directory globs with
./dir/**
Import composition is bounded before files are concatenated: resolved source may contain at most 64 MiB across 10,000 unique files, import nesting is capped at 256 levels, and a directory glob scans at most 100,000 entries. Exceeding a limit produces a compiler diagnostic instead of exhausting the host process.
Explicit imports are always resolved. --no-stdlib only disables the implicit
stdlib composition used by --relaxed-imports; it has no effect on normal
strict-mode imports. See the normative
import and compilation-unit contract for visibility,
deduplication, source-macro, enum-member, and compatibility semantics.
The current stdlib surfaces are intentionally practical:
Stringsfor object-oriented string helpers, conversion, splitting, joining, padding, and comparisons, plusStringBuilderfor amortized text assemblyCommand,CommandOutput,UnixShell,ShellWords,UnixPipe, andChildProcessfor shell/process orchestrationFileSystem,PathTools,Directory,DirectoryLease, andFileStatusfor filesystem work, including identity-matched recursive removalJsonObject,JsonValue, andTomlfor declarative data, including compact, pretty, and canonical newline-terminated JSON document serializationCliArgs,CliCommand,CliCommandLine, andCliHelpfor simple CLIsUiDocument,Window,Tray,DaemonSpec, and related daemon/UI models for lightweight native-app scaffoldingPlatform,Environment, andTerminalfor OS/runtime integration
Low-level stdlib internals may call C APIs because that is how btrc exposes
platform primitives. Application and test code should use the object-oriented
wrappers instead of reaching for strcmp, __btrc_strdup, manual shell string
assembly, or raw path manipulation.
ChildProcess.run can borrow explicit parent descriptors into fixed child
descriptor numbers and borrow a working-directory descriptor on native Linux.
Those capabilities have no pathname fallback and fail closed on other targets.
A project can declare local or Git dependencies in the nearest btrc.toml:
[package]
name = "myapp"
[dependencies]
mathx = { path = "../mathx" }
netkit = { git = "https://example.com/netkit.git", rev = "v1.2.0" }Compiling any source below that manifest resolves dependencies and atomically
writes btrc.lock. Git entries preserve the requested ref and pin its exact
commit; path entries are stored relative to the manifest. A changed dependency
table is detected automatically. Pass --fetch only when you intentionally
want to advance a moving Git ref and rewrite its lock entry. Git checkouts use
~/.btrc/pkgs/ by default, or $BTRC_PKG_CACHE when set. Manifest input is
bounded to 1 MiB, and Git operations are non-interactive with a five-minute
timeout so a compiler invocation cannot hang on a credential prompt or dead
network indefinitely.
Package imports address the dependency name and then an optional module path:
import mathx
import netkit.http
The resolver looks under each dependency's src/ directory first, then its
root. The compiler and LSP keep package maps isolated per invocation/workspace,
so one project's manifest cannot leak into another project.
Package-name imports from btrc.toml are currently a btrcpy/LSP feature.
The self-hosted btrcc fails closed for imports such as import mathx.vec;
it never guesses that a package name refers to a same-named local file. Use
btrcpy for package projects, or spell local dependencies explicitly as
import ./path.btrc (or a quoted path).
- Direct memory control with
new/deleteand pointers - Full C interop -- call any C library, use any C header
#include,struct,typedef,extern-- all still work- Same mental model: stack vs heap, pointers, manual lifetime management
- Generated C is strict C11 -- continuously tested with GCC and Clang; Windows bundles use MinGW-w64
// Primitives
int x = 42;
float f = 3.14;
double d = 2.718281828;
long big = 100000;
bool flag = true;
char c = 'A';
string name = "btrc";
// Extended integer types (same as C)
short s = 10;
unsigned int u = 42;
long long ll = 9999999999;
// Pointers (just like C)
int* ptr = &x;
int val = *ptr;
// Type inference
var count = 10; // int
var msg = "hello"; // string
var items = [1, 2, 3]; // Vector<int>
var cache = {"a": 1}; // Map<string, int>
int dec = 255;
int hex = 0xFF;
int bin = 0b11111111;
int oct = 0o377;
float f = 3.14f;
// if / else if / else
if (x > 0) {
print("positive");
} else if (x == 0) {
print("zero");
} else {
print("negative");
}
// C-style for
for (int i = 0; i < 10; i++) {
sum += i;
}
// for-in with range
for i in range(10) { }
for i in range(2, 8) { }
for i in range(0, 20, 2) { }
// for-in over collections and strings
for val in myVector { }
for key, value in myMap { }
for ch in someString { }
// while / do-while
while (running) { tick(); }
do { x++; } while (x < 10);
// switch
switch (status) {
case 200: handle_ok(); break;
case 404: handle_not_found(); break;
default: handle_error();
}
int add(int a, int b) {
return a + b;
}
// Default parameters
string greet(string name, string prefix = "Hello") {
return f"{prefix}, {name}!";
}
greet("world"); // "Hello, world!"
greet("world", "Hey"); // "Hey, world!"
// Named arguments
int mix(int a, int b = 2, int c = 3) {
return a + b * 10 + c * 100;
}
mix(1, c=4); // same as mix(1, 2, 4)
mix(c=5, a=6, b=7); // same as mix(6, 7, 5)
// Forward declarations (mutual recursion)
bool is_even(int n);
bool is_odd(int n) { return n == 0 ? false : is_even(n - 1); }
bool is_even(int n) { return n == 0 ? true : is_odd(n - 1); }
// Arrow syntax (expression body)
var double_it = (int x) => x * 2;
// Arrow syntax (block body)
var abs_fn = (int x) => {
if (x < 0) { return -x; }
return x;
};
// Verbose syntax
var multiply = int function(int a, int b) { return a * b; };
// Use with collection methods
nums.forEach(void function(int x) { print(f"{x}"); });
Vector<int> evens = nums.filter(bool function(int x) { return x % 2 == 0; });
class Point {
public int x;
public int y;
private string label = "origin"; // default field values
public Point(int x, int y) {
self.x = x;
self.y = y;
}
public int distSquared() {
return self.x * self.x + self.y * self.y;
}
// Static method
class Point zero() { return Point(0, 0); }
// Destructor -- called when refcount reaches zero or on delete
public void __del__() { }
}
Point p = Point(3, 4);
assert(p.distSquared() == 25);
Point z = Point.zero();
Access levels: public, private, class (static).
class Animal {
public string name;
public Animal(string name) { self.name = name; }
public string speak() { return "..."; }
}
class Dog extends Animal {
public Dog(string name) { self.name = name; }
public string speak() { return "Woof"; }
}
Dog d = Dog("Rex");
print(d.speak()); // "Woof"
print(d.name); // "Rex"
The compiler validates that method overrides have compatible signatures -- mismatched return types or parameter types are caught at compile time.
interface Drawable {
void draw();
}
abstract class Shape {
public abstract double area();
public string kind() { return "shape"; } // concrete method allowed
}
class Circle extends Shape implements Drawable {
public double r;
public Circle(double r) { self.r = r; }
public double area() { return 3.14159 * self.r * self.r; }
public void draw() { print(f"circle r={self.r}"); }
}
Interfaces are compile-time implementation contracts. Dispatch is static, so variables, fields, parameters, and return values use the implementing concrete class rather than an interface type.
Interfaces support inheritance (interface A extends B). The compiler checks that implementing classes provide all required methods with compatible signatures.
btrc generics are monomorphized -- the compiler generates specialized C code for each type combination. Zero runtime overhead, but binary size grows with each unique type combination (the same trade-off as C++ templates and Rust generics).
class Box<T> {
public T value;
public Box(T val) { self.value = val; }
public T get() { return self.value; }
}
Box<int> bi = Box(42);
Box<string> bs = Box("hello");
class Pair<A, B> {
public A first;
public B second;
public Pair(A a, B b) { self.first = a; self.second = b; }
}
Pair<string, int> entry = Pair("score", 100);
Generic interfaces are also supported (e.g. Iterable<T>).
class Vec2 {
public int x;
public int y;
public Vec2(int x, int y) { self.x = x; self.y = y; }
public Vec2 __add__(Vec2 other) {
return Vec2(self.x + other.x, self.y + other.y);
}
public Vec2 __neg__() {
return Vec2(-self.x, -self.y);
}
public bool __eq__(Vec2 other) {
return self.x == other.x && self.y == other.y;
}
}
Vec2 c = Vec2(1, 2) + Vec2(3, 4); // Vec2(4, 6)
Vec2 d = -c; // Vec2(-4, -6)
Supported operators: __add__, __sub__, __mul__, __div__, __mod__, __neg__, __eq__.
class Temperature {
private float celsius;
public Temperature(float c) { self.celsius = c; }
public float fahrenheit {
get { return self.celsius * 9.0 / 5.0 + 32.0; }
set { self.celsius = (value - 32.0) * 5.0 / 9.0; }
}
}
var t = Temperature(100.0);
float f = t.fahrenheit; // 212.0 (getter)
t.fahrenheit = 32.0; // sets celsius to 0.0 (setter)
Auto-properties are also supported: public int x { get; set; }.
// Simple enums
enum Color { RED, GREEN, BLUE };
enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };
// Rich enums (algebraic data types / tagged unions)
enum class Shape {
Circle(double radius),
Rect(double w, double h),
Point
}
Shape s = Shape.Circle(5.0);
if (s.tag == Shape.Circle) {
print(f"radius: {s.data.Circle.radius}");
}
// Auto-generated toString
print(s.toString()); // "Circle(radius=5.0)"
(int, int) divmod(int a, int b) {
return (a / b, a % b);
}
(int, int) result = divmod(17, 5);
assert(result._0 == 3); // quotient
assert(result._1 == 2); // remainder
// Nested tuples
(int, (string, bool)) nested = (1, ("yes", true));
Vector<int> nums = [10, 20, 30];
nums.push(40);
nums[0] = 99;
int val = nums.pop();
for x in nums { print(f"{x}"); }
// Rich API -- sort, reverse, slice, take, drop, distinct, copy, ...
nums.sort();
Vector<int> sub = nums.slice(1, 3);
bool has = nums.contains(20);
int total = nums.sum();
// Higher-order functions
Vector<int> evens = nums.filter(bool function(int x) { return x % 2 == 0; });
nums.forEach(void function(int x) { print(f"{x}"); });
bool any_neg = nums.any(bool function(int x) { return x < 0; });
int sum = nums.reduce(0, int function(int acc, int x) { return acc + x; });
nums.free();
Also available: .insert(), .remove(), .indexOf(), .lastIndexOf(), .swap(), .fill(), .clear(), .first(), .last(), .min(), .max(), .distinct(), .take(), .drop(), .copy(), .extend(), .all(), .findIndex(), .join().
List<int> ll = List();
ll.pushBack(1);
ll.pushFront(0);
int front = ll.front();
int removed = ll.popFront();
Vector<int> v = ll.toVector();
ll.free();
Map<string, int> ages = {"alice": 30, "bob": 25};
ages.put("carol", 35);
int age = ages.get("alice");
bool exists = ages.has("bob");
int fallback = ages.getOrDefault("dave", 0);
Vector<string> keys = ages.keys();
Vector<int> values = ages.values();
for k, v in ages {
print(f"{k}: {v}");
}
ages.free();
Also available: .putIfAbsent(), .remove(), .merge(), .containsValue(), .size(), .isEmpty(), .clear().
Set<int> s = {};
s.add(10);
s.add(20);
s.add(10); // duplicate ignored
Set<int> other = {};
other.add(20);
other.add(30);
Set<int> u = s.unite(other); // {10, 20, 30}
Set<int> i = s.intersect(other); // {20}
Set<int> d = s.subtract(other); // {10}
Also available: .symmetricDifference(), .isSubsetOf(), .isSupersetOf(), .filter(), .any(), .all(), .forEach(), .toVector(), .copy().
Array<int> arr = Array(100);
arr.set(0, 42);
int val = arr.get(0);
arr.fill(0);
arr.free();
Any class that implements iterLen() and iterGet(int i) can be used in for-in loops. All built-in collections implement this.
btrc strings have a full method API -- no more strlen/strstr/strtok gymnastics.
string s = "hello world";
int len = s.len();
bool has = s.contains("world");
int idx = s.indexOf("world");
bool starts = s.startsWith("hello");
string up = s.toUpper();
string trimmed = " hi ".trim();
string replaced = s.replace("world", "btrc");
string sub = s.substring(0, 5); // "hello"
string padded = "42".zfill(5); // "00042"
// Concatenation and conversion
string full = "hello" + " " + "world";
string num = 42.toString();
// Iterate characters
for ch in "hello" { print(f"{ch}"); }
Also available: .toLower(), .capitalize(), .title(), .swapCase(), .reverse(), .repeat(), .lstrip(), .rstrip(), .removePrefix(), .removeSuffix(), .padLeft(), .padRight(), .center(), .charAt(), .charLen(), .lastIndexOf(), .endsWith(), .count(), .find(), .isEmpty(), .equals(), .split(), .isDigit(), .isAlpha(), .isAlnum(), .isUpper(), .isLower(), .isBlank(), .toInt(), .toFloat(), .toDouble(), .toLong().
btrc has nullable types, optional chaining, and null coalescing. The compiler warns when you use .field on a nullable type without ?., helping catch null dereferences at compile time.
// Nullable type annotation
Box? b = findBox(id); // b might be null
// Optional chaining -- safe navigation
int val = b?.value; // 0 if b is null, no crash
// Null coalescing -- provide defaults
string name = ptr ?? "anonymous";
int value = b?.val ?? -1;
btrc uses lightweight automatic reference counting (ARC) for memory management. Every class instance tracks how many references point to it. When the count reaches zero, the object is automatically destroyed. No garbage collector -- deterministic cleanup at scope boundaries.
Safety model: btrc inherits C's memory model. The compiler checks types and access control at compile time. ARC handles common memory management automatically, but does not prevent all use-after-free or dangling pointer bugs. If you need full memory safety guarantees, use Rust. btrc is for programmers who want C's control with better ergonomics.
// Heap allocation -- refcount starts at 1
Node n = new Node(99);
n.val = 100;
delete n; // force destroy, set to NULL
// ARC auto-releases at scope exit
void example() {
Node n = new Node(42);
// ... use n ...
} // n automatically released here (rc--)
// Pointers work like C
int x = 42;
int* ptr = &x;
int val = *ptr;
// C memory functions available
int* buf = (int*)malloc(100 * sizeof(int));
free(buf);
| Keyword | Usage | Meaning |
|---|---|---|
keep |
Function param: store(keep T t) |
Keep the argument alive until the call returns |
keep |
Function return: keep T pop() |
Explicitly documents the managed-return ABI; managed btrc returns are already caller-owned |
keep |
Statement: keep p; |
Explicit rc++ (keep alive past scope exit) |
release |
Statement: release p; |
rc--; destroy at zero; p = NULL |
// Managed fields own their stored references. A keep parameter also protects
// the argument for the duration of the call.
class Container {
public Node item;
public void store(keep Node n) {
self.item = n;
}
}
void example() {
var c = new Container();
var n = new Node(42);
c.store(n); // item retains n; call guard is then released
delete c; // Container destructor releases item (rc--)
// n is still alive through its local owned reference
delete n; // force destroy
}
delete is an explicit force-destroy operation. Use it only after every other
owner has released the object; it intentionally invalidates outstanding aliases.
Use release when shared owners may still exist. Storing a managed value in a
class field or auto-property retains it independently of a parameter annotation.
Every class value returned by a btrc function or method gives
the caller one owned reference. Returning a fresh value or owned local transfers
that reference; returning a borrowed parameter, self, field, or property
retains it first. The keep return spelling remains useful as explicit API/ABI
documentation, but it is not required to make a managed return caller-owned.
Managed property reads remain field-like borrowed projections; when their
receiver is itself a temporary owner, the compiler retains the projected value
before releasing that receiver.
Tuples, C structs, fixed C arrays, and rich-enum payloads are shallow value aggregates: class elements inside them are borrowed references. Keep an explicit class owner alive for at least as long as the aggregate. The compiler rejects embedding or assigning a caller-owned temporary directly because these aggregates have no copy/destructor protocol with which to release it.
Pay for managed values only: Refcount operations are emitted at managed ownership boundaries; primitive-only code does not incur ARC work.
Cycle detection: For classes that can form reference cycles (A -> B -> A), the compiler includes a trial-deletion cycle collector. Non-cyclable types pay zero overhead.
Exception safety: ARC-tracked objects allocated inside try blocks are automatically cleaned up when an exception is thrown.
void validate(int x) {
if (x < 0) {
throw "negative value";
}
}
try {
validate(-1);
} catch (string e) {
print(f"caught: {e}");
} finally {
print("cleanup runs always");
}
Exceptions use setjmp/longjmp under the hood. ARC-managed objects are cleaned up automatically on throw.
btrc has built-in threading with spawn, typed Thread<T>, and Mutex<T>.
// Spawn a thread -- returns Thread<T> where T is the lambda return type
Thread<int> t = spawn(() => {
return 42;
});
int result = t.join(); // blocks until thread completes
// Captured variables are copied into the thread
int x = 10;
Thread<int> t = spawn(() => {
return x * 2; // captures x by value
});
// Mutex for shared mutable state
Mutex<int> counter = Mutex(0);
counter.set(counter.get() + 1);
int val = counter.get();
counter.destroy();
Captured class instances are ARC-safe -- the compiler increments the reference count at spawn time and decrements it when the thread completes. Under the hood, spawn creates a POSIX pthread.
Array params become storage buffers, scalar params become uniforms, gpu_id() maps to the global invocation index, and return writes to an output buffer. Void-returning kernels mutate arrays in-place.
#include <gpu.btrc>
// In-place mutation: each thread scales one element
@gpu
void scale(float[] data, float factor) {
int i = gpu_id();
data[i] = data[i] * factor;
}
// Return variant: each thread produces one output element
@gpu
float[] sgdUpdate(float[] weights, float[] gradients, float lr) {
int i = gpu_id();
return weights[i] - lr * gradients[i];
}
For a full example that combines @gpu kernels with btrc classes, see examples/sgd/sgd.btrc -- GPU-accelerated stochastic gradient descent that learns y = 2x + 3 from training data.
btrc includes a Unity-inspired 3D game engine built on WebGPU rendering. A ball on a ground plane with WASD movement, space to jump, real-time shadows, and SDF raymarching -- all in ~570 lines of btrc across 11 engine modules.
#include "engine/engine.btrc"
int main() {
var engine = Engine("btrc 3D Ball", 800, 600);
var player = new GameObject();
float speed = 4.0;
while (engine.isRunning()) {
engine.update();
float dt = engine.time.deltaTime;
if (engine.input.key(KEY_W)) { player.move(0.0, 0.0, speed * dt); }
if (engine.input.key(KEY_S)) { player.move(0.0, 0.0, -speed * dt); }
if (engine.input.key(KEY_A)) { player.move(speed * dt, 0.0, 0.0); }
if (engine.input.key(KEY_D)) { player.move(-speed * dt, 0.0, 0.0); }
if (engine.input.key(KEY_SPACE)) { player.jump(speed); }
player.applyPhysics(dt);
engine.render(player);
}
return 0;
}
The engine is modular: GameObject with physics, Camera with follow behavior, Light and Material for shading, Ground checkerboard and Sky gradient, Scene compositing with a WGSL raymarching shader, Input for keyboard, Time for frame timing, and Renderer tying it all together. See examples/game/.
make gpu && make examples-game
./examples/game/gamebtrc understands most C syntax. You can mix btrc and C freely in the same file.
#include <math.h>
struct Vec2 {
float x;
float y;
};
float dot(struct Vec2* a, struct Vec2* b) {
return a->x * b->x + a->y * b->y;
}
int main() {
struct Vec2 a = {3.0f, 4.0f};
struct Vec2 b = {1.0f, 0.0f};
float d = dot(&a, &b);
printf("dot = %f, sqrt = %f\n", d, sqrt(d));
return 0;
}
btrcpy --freestanding emits C with no hosted-libc includes — every runtime
symbol is routed through a single retargetable seam (btrc_rt.h) so a btrc
program can target a kernel module, firmware, or bootloader. The pure subset and
core stdlib (strings, collections, integer math) compile to an object with
zero libc dependencies against the shipped reference runtime. See
docs/design/freestanding.md.
btrc includes a standard library written in btrc itself (src/stdlib/). Strict
imports are the default, so programs import the modules they use explicitly;
implicit whole-stdlib composition is available only through the legacy
--relaxed-imports mode.
import std.math;
double pi = Math.PI();
int abs = Math.abs(-5);
int clamped = Math.clamp(x, 0, 100);
double root = Math.sqrt(2.0);
int fact = Math.factorial(10);
int gcd = Math.gcd(12, 8);
bool prime = Math.isPrime(17);
double sin = Math.sin(Math.PI() / 2.0);
import std.datetime;
DateTime now = DateTime.now();
string date = now.dateString(); // "2025-01-15"
string time = now.timeString(); // "14:30:00"
Timer t = Timer();
t.start();
// ... work ...
t.stop();
float elapsed = t.elapsed(); // seconds
import std.random;
Random rng = Random();
rng.seedTime();
int n = rng.randint(1, 100);
float f = rng.random(); // [0, 1)
rng.shuffle(myVector); // in-place Fisher-Yates
import std.io;
File f = File("data.txt", "r");
if (f.ok()) {
string content = f.read();
f.close();
}
File out = File("output.txt", "w");
out.writeLine("hello");
out.close();
// Static helpers
bool exists = Path.exists("data.txt");
string content = Path.readAll("data.txt");
Path.writeAll("output.txt", "hello");
import std.console;
Console.log("message"); // stdout + newline
Console.error("problem"); // stderr + newline
import std.result;
Result<int, string> divide(int a, int b) {
if (b == 0) { return Result.err("division by zero"); }
return Result.ok(a / b);
}
Result<int, string> r = divide(10, 0);
if (r.isErr()) {
print(f"error: {r.unwrapErr()}");
}
Import std.error to use Error, ValueError, IOError, TypeError,
IndexError, and KeyError; each provides .toString().
btrc compiles through six stages. Two formal specs drive the front-end: src/language/grammar.ebnf defines all keywords, operators, and syntax rules; src/language/ast.asdl defines all AST node types using Zephyr ASDL. A structured IR separates lowering from emission.
src/language/grammar.ebnf (single source of truth: keywords, operators, syntax)
src/language/ast.asdl (single source of truth: AST node types)
|
.btrc source
|
[Lexer] --> tokens grammar-driven (keywords + operators from EBNF)
|
[Parser] --> typed AST ASDL-generated node classes
|
[Analyzer] --> checked AST scopes, types, generic instance collection
|
[IR Gen] --> IR tree structured nodes (IRIf, IRCall, IRFor, ...)
|
[Optimizer] --> optimized IR typed reachability + normalization
|
[C Emitter] --> .c file simple tree walk -- no lowering logic
|
gcc/clang --> native binary any C11 compiler works
For core CPU programs, generated C is self-contained apart from the ordinary C
and platform libraries selected by the program. It includes the needed static
inheritance/member lowering, monomorphized generic structs, collection and
string helpers, threading wrappers, and exception handling via
setjmp/longjmp. GPU, GUI, tray, and similar native features link their
documented backend runtimes.
btrc compiles itself. Alongside the reference compiler in Python, the same
six-stage pipeline is implemented in btrc under
src/compiler/btrc/: lexer, parser, analyzer, structured
IR lowering, optimizer, and C emitter. Its front-end resolves directed
import dependencies and textual #include composition with strict imports
enabled by default. Implicit whole-stdlib composition exists only behind the
explicit --relaxed-imports compatibility mode. The compiler is bootstrapped
by transpiling its own source with the reference compiler (a C compiler does
the rest); from then on btrcc compiles btrc programs on its own.
Because btrc has no dynamic dispatch, the AST and IR are fat tagged nodes -- one struct per layer carrying a kind tag and the union of every field, dispatched with if (n.kind == ...). The checked-in btrc AST node layer is generated from the same ast.asdl contract by gen_btrc_ast.py; make ast-generate-btrc is the canonical regeneration command. The self-hosted AST tooling consumes the same schema and is verified against that generated contract.
The self-hosted compiler is held to a strict bar: across the entire language test suite, the C it emits must compile under gcc -std=c11 (and clang) and produce byte-identical program output to the reference compiler. It also reaches a bootstrap fixed point -- the self-built btrcc compiles its own source, and that output, recompiled, is byte-identical (the compiler reproduces itself bit-for-bit). Run the bootstrap-parity suite and the fixed-point check with:
make test-btrc-selfhost # build btrcc, then run the whole corpus through it
make bootstrap # prove btrcc reproduces itself bit-for-bit (fixed point)src/
language/
grammar.ebnf # Formal EBNF grammar (lexical + syntactic rules)
ast.asdl # Algebraic AST spec (Zephyr ASDL) -- single source of truth
compiler/
python/ # Reference compiler (Python)
__init__.py # Stable Compiler/Options/Result API
compiler.py # Compiler application object
main.py # Thin process entry point
cli/ # CLI parsing, diagnostics, and output
pipeline/ # Ordered six-stage orchestration + result models
frontend/ # Resolution, provenance, strict visibility, stdlib
ast/ # ASDL parser/generators and LSP builtin generator
parser/ # Recursive-descent source parser
analyzer/
semantic_analyzer.py # SemanticAnalyzer composition root
analysis_context.py # Per-analysis diagnostics and provenance state
declarations/ # Declaration registry and owned policies
ir/
nodes.py # Compatibility import surface for typed IR nodes
module.py # IRModule and top-level declarations
optimizer.py # Optimization composition root
reachability.py # Program/runtime/declaration reachability owners
optimizer_walk.py # IRTree and identifier-reference owners
emitter.py # C emission composition root
gen/
lowerer.py # IRLowerer composition root
lowering_context.py # Per-lowering mutable state
managed_values.py # Managed classification and runtime identity
cycle_metadata.py # Cycle graph and visitor metadata owner
cleanup_slots.py # Typed cleanup/access adapter registry
ownership_lifetime.py # Context-bound retain/release/cleanup owner
arc.py # Explicit release-statement lowering owner
generics/ # User/builtin monomorphization lowering
helpers/ # Authored runtime-helper definitions and registry
artifacts/ # Cache, stdlib, self-host bundle, publication
btrc/ # Self-hosted compiler (written in btrc)
btrcc_main.btrc # Thin process entry point
compiler.btrc # Compiler and BtrccDriver application objects
pipeline/ # CompilerPipeline, options, and result models
lexer/stage.btrc # Ordered lexer stage manifest
frontend/stage.btrc # Resolution + strict visibility stage manifest
parser/stage.btrc # Parser stage manifest
analyzer/stage.btrc # Semantic stage manifest
ir/stage.btrc # Structured IR/optimization/emission manifest
generated/hosted_abi/ # Generated hosted ABI tables under one owner
ast/
asdl.btrc # ASDL parser (btrc-native, zero-dependency)
gen_node.btrc # ASDL --> node.btrc generator
node.btrc # GENERATED fat-node AST
stdlib/ # Standard-library modules (explicit in strict mode)
vector.btrc # Vector<T> (dynamic array)
list.btrc # List<T> (doubly-linked list)
array.btrc # Array<T> (fixed-size)
iterable.btrc # Iterable<T> interface
map.btrc # Map<K,V> (hash map)
set.btrc # Set<T> (hash set)
strings.btrc # Strings static utilities
math.btrc # Math static utilities
datetime.btrc # DateTime + Timer
random.btrc # Random number generation
io.btrc # File + Path I/O
console.btrc # Console output
error.btrc # Error class hierarchy
result.btrc # Result<T,E> type
gpu/ # GPU runtime (WebGPU/wgpu-native)
gpu.btrc # GPU btrc types
btrc_gpu.h # C header for GPU compute functions
btrc_gpu.c # Strict-C11 implementation (wgpu-native backend)
btrc_gpu_compute_singleton.h # Atomic compute-context publication
btrc_gpu_surface_macos.m # macOS Cocoa/Metal surface bridge
tests/ # Test suite — one framework for both compilers
runner.py # Unified runner: each .btrc test through BOTH the
# Python and self-hosted compilers (--compilers)
generate_expected.py # Regenerate golden .stdout files
conftest.py # --compilers option + shared fixtures
python/ # Python reference-compiler unit tests
btrc/ # Self-hosted-compiler-specific tests
<category>/ # Shared language tests (.btrc), run on both
basics/ # Types, vars, print, nullable, casting, sizeof
control_flow/ # if/for/while/switch/try-catch, range
classes/ # Classes, inheritance, interfaces, abstract
collections/ # Vector, List, Map, Set, Array, iteration
strings/ # String methods, f-strings, conversions
functions/ # Default params, lambdas, forward decl, recursion
generics/ # User-defined generics, Result<T,E>
enums/ # Simple enums, rich enums, toString
tuples/ # Tuple creation, access, multi-element
memory/ # ARC: keep/release, cycle detection, exceptions
threads/ # spawn, Thread<T>, Mutex<T>, ARC captures
gpu/ # @gpu kernels, WGSL generation, dispatch
stdlib/ # Math, DateTime, Random
algorithms/ # Quicksort, BST, hash table, linked list
devex/
ext/ # VS Code extension (syntax highlighting + LSP client)
lsp/ # Language server (completions, diagnostics, hover, go-to-def)
examples/
game/ # 3D game engine -- Unity-inspired, WGSL raymarching
engine/ # Engine modules: Camera, Light, Material, Ground, Sky, Scene, Input, Time, GameObject, Renderer
game.btrc # The ball game (WASD + space to jump)
todo/ # Todo board -- classes, generics, collections
sgd/ # GPU-accelerated SGD -- @gpu, classes, Vector
triangle/ # WebGPU triangle -- raw WGSL render pipeline
make all # Build and verify the complete developer tree
make build # Create bin/btrcpy wrapper script (Python reference compiler)
make package # Build the Python sdist, then its installable wheel
make wheel # Build only the installable Python wheel
make btrcc # Build the self-hosted compiler for THIS machine -> bin/btrcc
make test # Everything: unit + LSP + debugger + language on BOTH compilers
make test-unit # Python compiler unit and code-generation tests
make test-lsp # Language-server tests
make test-debug # Debug-adapter tests (requires lldb + a C compiler)
make test-selfhost # Self-hosted lexer parity
make test-btrc # Language corpus through the Python reference compiler
make test-btrc-selfhost # Language corpus through the self-hosted compiler (btrcc)
make bootstrap # Prove the self-hosted compiler's byte-stable fixed point
make test-c11 # Strict, warning-free C11: gcc + clang at -O0 through -O3
make generated-check # Verify every committed generated source is current
make hosted-abi-check # Verify generated hosted-ABI policy tables
make lint # Run ruff linter
make format # Format with ruff
make format-check # Check formatting without modifying files
make test-generate-goldens # Regenerate golden .stdout files
make stubs-generate # Regenerate built-in type stubs
make extension # Package VS Code extension (.vsix)
make extension-install # Install VS Code extension (dev)
make examples # Build and run examples
make gpu # Install WebGPU + GLFW and build GPU runtime
make gpu-required # Build GPU runtime and fail if production deps are absent
make gui # Build the GUI runtime
make examples-game # Build the 3D engine game
make examples-triangle # Build the GPU triangle example
make examples-sgd # Build the GPU SGD example
make examples-todo # Build the todo example
make examples-gui # Build and run the headless GUI example
make bench # Build and run transpile/compile/runtime benchmarks
make devcontainer # Generate .devcontainer/ and build image
make clean # Remove build artifactsbtrcc is btrc source transpiled to C (by btrcpy) and then compiled by a C
toolchain. make btrcc builds the source-tree developer executable for the
current machine. The cross targets use zig cc and
publish relocatable, checksummed distributions in dist/:
make btrcc # native build for this machine -> bin/btrcc
make btrcc-macos-arm64 # -> dist/btrcc-macos-arm64.tar.gz{,.sha256}
make btrcc-macos-x64 # -> dist/btrcc-macos-x64.tar.gz{,.sha256}
make btrcc-linux-x64 # -> dist/btrcc-linux-x64.tar.gz{,.sha256}
make btrcc-linux-arm64 # -> dist/btrcc-linux-arm64.tar.gz{,.sha256}
make btrcc-windows-x64 # -> dist/btrcc-windows-x64.zip{,.sha256}
make btrcc-dist # all five distributionsEach archive has one self-contained layout:
btrcc-<target>/
bin/btrcc[.exe]
LICENSE
share/btrc/language/grammar.ebnf
share/btrc/stdlib/...
share/btrc/manifest.json
The executable resolves this data relative to its real path, including when it
is launched through an absolute PATH entry or symlink, so the bundle works
from any current directory. btrcc --stdlib-dir prints the selected stdlib.
BTRC_HOME may explicitly select another data root containing language/ and
stdlib/; when set, it is authoritative and an invalid value is an error.
Verify a release before extracting it with sha256sum -c <archive>.sha256
(shasum -a 256 -c <archive>.sha256 on macOS).
For generated C that imports a native module, add the reported stdlib path and
the module subdirectory to the C compiler include path—for example,
stdlib="$(btrcc --stdlib-dir)" followed by
cc -I "$stdlib" -I "$stdlib/gui" .... Link the corresponding bundled/runtime
source or library and the platform dependencies documented by gpu/, gui/,
or tray/.
Windows uses a small compat layer in src/stdlib/win/
(applied only to Windows builds, via -I + -include) that fills the handful of
POSIX headers/symbols MinGW-w64 omits. This gets btrcc and ordinary btrc
programs building and running on Windows; the POSIX-only stdlib modules
(Process, raw-mode Terminal, sockets, Regex) don't have real Win32 backends
yet, so programs that call into them aren't supported on Windows. Most
filesystem and compiler I/O still uses the narrow C runtime or Win32 A APIs,
so paths outside the active Windows code page are not consistently supported;
realpath is the exception and uses UTF-16 internally. removeRecursive
removes files and final reparse points, but deliberately returns -1 for an
ordinary directory until a handle-relative NT deletion backend exists. Test it
with:
make test-windows # cross-build btrcc.exe + a sample; run under wine if presentmake test-windows cross-builds on any host and runs the sample under
wine/wine64 when available (Linux/CI), skipping execution gracefully
otherwise. A Windows CI workflow builds and
runs the binaries natively on windows-latest for pushes and pull requests
targeting main.
The developer bin/btrcc discovers src/language/grammar.ebnf and src/stdlib
from its executable-relative checkout. Release bundles instead discover the
matching files under share/btrc, so they do not require a repository checkout.
All dependencies are managed by flake.nix. If using the devcontainer or nix develop, everything is set up automatically.
Manual install requires:
- Python 3.13+
- gcc and/or clang
- pytest + pytest-xdist (for tests)
- ruff (for linting)
- pygls + lsprotocol (for a source-tree LSP server; vendored in the VSIX)
- Node.js + npm (for VS Code extension)
- wgpu-native + GLFW (optional for compiler use; required by
make test/make test-c11so GPU cases cannot be skipped)
GitHub Actions (.github/workflows/ci.yml) runs on every push and PR to main:
- Validates the Nix flake and builds the devcontainer image
- Checks lint and formatting
- Builds the Python distribution and VS Code extension
- Runs the reference, self-hosted, LSP, debugger, and shared language suites
- Re-runs the shared language suite across the strict GCC/Clang C11 matrix
CI builds the GPU runtime as a required gate before both corpus matrices; a missing backend dependency fails the job instead of silently skipping GPU runtime cases.
btrc ships with a VS Code extension (src/devex/ext/) and a Language Server Protocol implementation (src/devex/lsp/) that reuses the compiler's own lexer, parser, and analyzer. Diagnostics match exactly what the compiler reports -- there is no separate linting pass.
The packaged extension vendors the LSP's pure-Python dependencies. Its bundled
server/compiler fallback still requires Python 3.13 or newer; the launcher
probes the configured interpreter and will use an installed btrc-lsp command
instead of starting the bundled payload with an unsupported Python. Nix and the
devcontainer provide the supported interpreter automatically.
The LSP server maintains a two-tier cache: the current analysis (which may have parse errors while you type) and the last fully successful analysis. Features like go-to-definition and hover fall back to the good cache during transient errors, so intelligence keeps working while you edit.
| Feature | Description |
|---|---|
| Syntax highlighting | TextMate grammar + semantic tokens for rich classification |
| Diagnostics | Real-time errors and warnings from the compiler's lexer, parser, and analyzer |
| Code completion | Keywords, types, member access (., ?., ->), stdlib static methods, snippets |
| Hover | Type information for variables, fields, methods, classes, and built-in types |
| Go to definition | Classes, functions, methods, fields, properties, variables, enums, typedefs |
| Find references | All usages of a symbol across the document with scope-aware matching |
| Rename | Symbol rename across all references |
| Signature help | Parameter hints for functions, constructors, methods, and stdlib calls |
| Document symbols | Outline view with class hierarchy (fields, methods as children) |
| Debugging | Source-level debugging in .btrc files: breakpoints (incl. conditional + logpoints), stepping, call stack, and btrc-aware variable inspection |
Press F5 on a .btrc file to compile it with debug info and debug it
natively in VS Code -- breakpoints, step over/into/out, the call stack, and
variables shown as btrc values (a string shows its text, Vector<int> shows
[1, 2, 3], a class shows its fields). The compiler emits #line directives
under --debug so the binary's DWARF points back at btrc source, and a Debug
Adapter (src/devex/debug/) drives lldb to present it.
See docs/design/debugger.md. Requires lldb and a C
compiler.
# Install the VS Code extension (builds + installs)
make extension-install
# Or open the project in the devcontainer for automatic setupThe extension auto-discovers the LSP server and Python interpreter. Configure btrc.pythonPath or btrc.serverPath in VS Code settings if needed.
Planned but not yet implemented:
- Known language gaps -- the remaining unsupported forms and their regression status are tracked in docs/known-language-gaps.md
- Module system -- imports already resolve and compose declarations textually; namespaced modules and separate compilation remain planned
- Pattern matching --
matchexpressions for rich enums with exhaustiveness checking - Weak references --
weakkeyword for intentional non-owning references - Incremental compilation -- only recompile changed files
