DEXA language logo

DEXA

Unified Compute Language

Devlog

Building DEXA from the inside out

Notes from the implementation side — what’s actually shipped in the compiler and runtime, not just marketing copy.

Dev Log · 0.0.8

IR validation firewall + record invariants

2025-12-17 · DX

This update is about discipline: DEXA now has an explicit IR validation pass that runs after lowering and before interpretation. If the compiler pipeline ever emits inconsistent IR, it fails immediately — no “mystery runtime behaviour”, no papering over bugs.

The record rules are now explicit and enforced:

# Record literals (no base) are fully materialized in lowering:
model Wallet { owner: address; balance: int = 0; }

fn main() -> int {
    let w: Wallet = Wallet { owner: address("alice") };
    return w.balance; # == 0
}

Record updates are intentionally conservative in v0.x: the parser currently supports a base-only update form. Overrides are coming later, but the semantics are already guarded:

# Record update (implemented grammar today):
# Type { ..base }

model A { x: int = 0; }
model B { x: int = 0; }

fn main() -> int {
    let b: B = B { x: 1 };
    let a: A = A { ..b }; # hard error (wrong nominal type)
    return 0;
}

The validator also learned how to infer record type names from locals in the common case, not just from inline literals, so these checks don’t disappear as soon as you assign to a variable.

Net result: fewer “cool features” on the surface, but much stronger guarantees underneath. This is the kind of boring work that keeps a language from turning into a haunted house later.

Dev Log · 0.0.7

ProgramResult, prints, trace & gas

2025-12-11 · DX

The interpreter now returns something more honest than “here’s an int, good luck”. The runtime has grown a real execution model: every run of main produces a structured ProgramResult with the final value, captured prints, a call trace, and a gas-like step counter.

ProgramResult::Ok {
    value: Value,
    prints: Vec<String>,
    trace: ExecutionTrace,
    gas: GasMeter,
}

ProgramResult::RequireFail {
    message: String,
    trace: ExecutionTrace,
    gas: GasMeter,
}

require(cond, msg?) is now classified instead of being “just another runtime error”. When the condition is false, execution aborts immediately and the interpreter returns ProgramResult::RequireFail with the message, plus the trace/gas accumulated so far. When it’s true, require is a no-op that evaluates to unit.

fn main() -> int {
    require(balance > 0, "balance must be positive");
    print(balance);
    return balance;
}

At the spec level this locks in a contract: “same source, same ProgramResult for a given input”. Future backends (DX-VM, GPU, whatever) have to uphold that, so this interpreter becomes the semantic oracle.

There are also new tests around traces and gas: checking that main shows up in the trace, nested calls appear in order, and “heavier” programs consume more steps than trivial ones. It’s not glamorous work, but this is the layer everything else is going to stand on.

Dev Log · 0.0.6

Records, address type, and a real wallet

2025-12-05 · DX

The core runtime finally understands something more interesting than raw ints and floats. This drop adds nominal records, a first-class address type, and a tiny wallet example that runs end-to-end through DX-IR.

The current reference sample looks like this:

model Wallet {
    owner: address
    balance: int
}

fn new_wallet(owner: address) -> Wallet {
    let w: Wallet = Wallet {
        owner: owner,
        balance: 0,
    };
    require(w.balance == 0, "new wallet should start at 0");
    return w;
}

fn credit(w: Wallet, amount: int) -> Wallet {
    require(amount > 0, "amount must be positive");

    let mut w2: Wallet = w;
    let new_balance: int = w2.balance + amount;

    w2 = Wallet {
        owner: w2.owner,
        balance: new_balance,
    };

    return w2;
}

fn debit(w: Wallet, amount: int) -> Wallet {
    require(amount > 0, "amount must be positive");
    require(w.balance >= amount, "insufficient funds");

    let new_balance: int = w.balance - amount;

    let w2: Wallet = Wallet {
        owner: w.owner,
        balance: new_balance,
    };

    return w2;
}

fn test_new_wallet_zero_balance() {
    let w: Wallet = new_wallet("addr:test");
    require(w.balance == 0, "new wallet should start at 0");
    return;
}

fn test_credit_increases_balance() {
    let w: Wallet = new_wallet("addr:test");
    let w2: Wallet = credit(w, 10);
    require(w2.balance == 10, "credit should increase balance by amount");
    return;
}

fn test_debit_decreases_balance() {
    let w: Wallet = new_wallet("addr:test");
    let w2: Wallet = credit(w, 10);
    let w3: Wallet = debit(w2, 3);
    require(w3.balance == 7, "debit should decrease balance by amount");
    return;
}

To run this from the repo root right now:

cargo run -- examples/wallet.dexa

The tour page now shows this program directly, so the website and the interpreter stop lying about what the language can actually do.

Dev Log · 0.0.5

Loops, models/contracts, and real errors

2025-12-04 · DX

The core language finally behaves like something you can reason about over time: structured loops, typechecked models/contracts, and error messages that don’t feel like a slap in the dark.

The canonical “does this even work?” sample right now is a tiny loop harness:

fn main() -> int {
    let mut i: int = 0;
    let mut acc: int = 0;

    while i < 10 {
        acc = acc + i;
        i = i + 1;
    }

    require(acc == 45, "expected 45 from 0..9");

    let mut j: int = 0;
    let mut hits: int = 0;

    loop {
        if j == 3 {
            break;
        }

        hits = hits + 1;
        j = j + 1;
    }

    require(hits == 3, "loop/break broken");
    return 0;
}

On top of this, models/contracts get their own typechecked bodies — you can’t sneak an int into a field that declared as bool anymore without the compiler yelling.

Dev Log · 0.0.4

Mutability, block scopes, and a real IR

2025-12-01 · DX

The compiler finally grew up a bit. Locals aren’t just nameless slots hanging off an AST anymore — they live in real scopes, can be let or let mut, and lower into a small IR the interpreter can actually execute.

The following program runs end-to-end and demonstrates let mut, shadowing, and the IR interpreter:

fn main() -> int {
    let mut x: int = 0;
    x = x + 10;

    if x > 5 {
        let mut x: int = 1;
        x = x + 1;
    }

    return x;
}

This returns 10 from the outer x. The inner x is a separate binding that lives and dies inside the if block — which is exactly what you want before you start doing anything serious with state.

From pretty syntax to a real typechecked core

2025-12-01 · DX

The first version of the DEXA site talked a big game about AI, smart contracts and deterministic compute. Underneath, the compiler was basically a parser that printed ASTs. Cute, but useless.

The last few iterations turned DEXA into an actual language core:

Example of what currently runs end-to-end:

fn max(a: int, b: int) -> int {
    if a > b {
        return a;
    } else {
        return b;
    }
}

fn main() -> int {
    let mut x: int = 0;
    x = max(1 + 2, 3 * 4); // 12
    return x;
}

The CLI is still intentionally minimal:

cargo run -- ../examples/hello.dexa

Next steps: extend the type system toward tensors and asset types, and start mapping this IR to a verifiable VM instead of just an in-process interpreter.

More devlogs will land as the compiler grows (IR passes, DX-VM, GPU backend, tensors, contracts).