The language runs

Milestone log, v0.0.1 – v0.0.10. The design these versions serve is in DESIGN.md; the whole log is indexed here.

Exact decimals with a declared scale, rounding contracts, functions, checked arithmetic, FFI, strings, structs, arrays. By the end of these ten, a Burxt program was a real native program.

v0.0.1: the first vertical slice

The smallest program that proves the thesis: exact decimal arithmetic with a declared scale, printed exactly. Integers supported too, as the simplest path to “it runs”. Decimals are represented as scaled i64 (value * 10^scale) — exact, no float.

let price: Decimal<2> = 19.99;
let qty: Int = 3;
let total: Decimal<2> = price * qty;
print(total);        // 59.97  — exact, never 59.970000000001

v0.0.2: rounding contracts

A rounding contract is an optional second type argument:

Decimal<2>                 // no contract: only exact arithmetic
Decimal<2, RoundHalfEven>  // ties to the even neighbor (banker's)
Decimal<2, RoundHalfUp>    // ties away from zero (commercial)

Rules:

v0.0.3: functions, control flow, Bool

Burxt is now a real programming language: recursion makes it computationally complete without needing mutation yet.

fn total(price: Decimal<2>, qty: Int) -> Decimal<2> {
    return price * qty;
}
print(total(19.99, 3));    // 59.97

Rules:

v0.0.4: mutation and loops

Immutable is the default; mutation is opt-in and visible at the declaration:

let mut b: Decimal<2, RoundHalfEven> = 1000.00;
let mut m: Int = 0;
while m < 12 {
    b = b * 1.01;      // contract applies at every step
    m = m + 1;
}

Rules:

v0.0.5: checked arithmetic — no silently wrong numbers, ever

Every +, -, * (including the internal double-scale products behind Decimal*Decimal and division) goes through @burxt.checked.<op>, built on LLVM’s llvm.s{add,sub,mul}.with.overflow intrinsics. On overflow the program prints

burxt runtime error: arithmetic overflow — the exact result no longer
fits in the value range

to stderr and exits with code 70. Division by zero (and the lone i64::MIN / -1 quotient) gets the same treatment — a named error instead of a raw SIGFPE. This closes the last “silently wrong number” hole in the i64 representation; a wider representation can come later, but wraparound was never acceptable.

v0.0.6: FFI — call into C

The key unlock for the platform roadmap: every platform API Burxt will ever touch goes through this door.

extern fn llabs(x: Int) -> Int;
print(llabs(0 - 42));    // 42

Rules:

v0.0.7: strings — literals only, no heap, no lies

A String is a pointer to an immutable, NUL-terminated byte array. In this slice every String is a literal living in .rodata, so there is NO allocation — and therefore no free/GC question to answer dishonestly. Operations that need allocation are loud, advice-style refusals until the allocation story exists:

What works: literals with exactly four escapes (\\ \" \n \t — no \0, so interior NULs are unrepresentable), print, let / let mut rebinding, user-fn params and returns (every value is ‘static by construction), and the FFI widening: an extern String parameter passes a borrowed, read-only const char*. Extern returns stay Int-only — Burxt cannot yet track who owns memory a C function returns.

Codegen grew up for this: gen_expr now returns BasicValueEnum, variable slots and function signatures are typed per Burxt type, and String is LLVM’s opaque ptr — never an integer, so pointer width stays the target’s business (wasm32-safe). User bytes are always a printf ARGUMENT, never the format string.

v0.0.8: structs — the OOP substrate

struct LineItem {
    price: Decimal<2>,
    qty: Int,
}
let mut item: LineItem = LineItem { qty: 3, price: 19.99 };
print(item.price * item.qty);   // 59.97
item.price = 12.50;

Rules:

The OOP direction: SOLID by construction

Burxt is OOP by default, and the object model is decided now (keywords interface, is, self are already reserved):

v0.0.9: hardening — findings from the adversarial review

An agent review of the strings release confirmed one serious bug and three sharp edges; all fixed:

Plus one language rule from the design review: shadowing is refused — a second let x is an error naming the first declaration, not a quiet new variable.

v0.0.10: arrays — fixed-size, always bounds-checked

let splits: [Decimal<2>; 3] = [10.00, 5.99, 4.01];
let mut sum: Decimal<2> = 0.00;
let mut i: Int = 0;
while i < len(splits) { sum = sum + splits[i]; i = i + 1; }
print(sum == 20.00);   // true — A5's refinement, checked by hand for now

Rules: