Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ubel Stratum

The right memory model for every function.

Ubel Stratum is a statically-typed systems and game-development language. Its defining feature is a tier system: every function declares which memory strategy it uses, and the compiler enforces the rules statically.

TierAnnotationMemoryAsyncUse case
HIGH@tier(high) (default)Garbage collectedYesBusiness logic, I/O
MID@tier(mid)Arena allocatedNoParsers, hot paths
LOW@tier(low)Manual, borrow-checkedNoSystems, FFI, packets

Functions without a @tier annotation default to HIGH. Tiers are opted into for performance, not opted out of for convenience.

Why a tier system

Most languages make one memory bet for an entire program:

LanguageBetCost
Go, Java, C#GC everywherePauses, GC pressure, heap bloat
RustOwnership everywhereSteep learning curve, slow compiles
C, C++Manual everywhereSafety bugs, undefined behavior

Ubel Stratum lets each function make its own bet: garbage collection where that is the easier choice, arenas where speed matters, manual ownership where correctness under tight constraints matters most. The compiler guarantees the bets never clash at runtime, and cross-tier calls follow an explicit set of rules covered in The Tier Model.

Current status

Ubel Stratum is early and under active development. The end goal is native machine code through an LLVM backend; today the language runs on a tree-walking interpreter, a deliberate stepping stone rather than the final architecture. Building the interpreter first proves out the language’s semantics completely before the much heavier lift of LLVM integration begins.

PhaseStatusCovers
1DoneCore design, memory model, lexer, arena AST, parser
2DoneSemantic analysis: name resolution, type inference, tier enforcement
3DoneTree-walking interpreter, the current execution model
4Not startedLLVM backend, native binaries
5Not startedStandard library, tooling, package manager

Source files use the .ubl extension. There is no installable compiler or package manager yet; running Ubel Stratum today means building the interpreter from source, covered in Getting Started.

The source, issue tracker, and full engineering documentation live at github.com/MidManStudio/ubel_stratum. A browser playground runs the tokenizer, parser, semantic analyzer, and interpreter on arbitrary .ubl source without installing anything, and the CI results page tracks the fixture suite and benchmark history that back every change to the language.

Getting Started

There is no installable compiler or package manager yet (that is Phase 5). Running Ubel Stratum today means building the interpreter from source and driving it through the pipeline example, or trying source directly in the browser playground without installing anything.

Prerequisites

Rust 1.75 specifically. The project targets free GitHub Actions runners and older local hardware, so the toolchain is pinned rather than tracking stable. A handful of dependencies need pinning to versions that still support 1.75 after a fresh cargo generate-lockfile:

cargo update -p owo-colors  --precise 4.0.0
cargo update -p backtrace   --precise 0.3.69
cargo update -p proptest    --precise 1.4.0
cargo update -p tempfile    --precise 3.14.0
cargo update -p clap        --precise 4.4.18
cargo update -p rayon       --precise 1.10.0
cargo update -p rayon-core  --precise 1.12.1
cargo update -p half        --precise 2.4.1
cargo update -p textwrap    --precise 0.16.0

LALRPOP is not required. crates/parser is an inactive reference implementation, not a default workspace member; it is only relevant when working on that crate specifically.

Build and test

cargo build --workspace --all-targets
cargo test  --workspace --lib --bins
cargo bench   # crates/core and crates/rd_parser each have benches/

Running the pipeline

# Run the full pipeline against every fixture
cargo run -p ubel_stratum_rd --example pipeline -- tests/fixtures

# Against a single file
cargo run -p ubel_stratum_rd --example pipeline -- path/to/file.ubl

This is the same command the CI pipeline runs on every push; its output backs the CI Results page.

A first program

fn main() {
    println("Hello, Stratum!")
}

Functions default to the HIGH tier, so this needs no @tier annotation. Saved as hello.ubl and run through the pipeline above, it prints its one line and exits. The Language Tour covers the rest of the surface syntax; The Tier Model covers what @tier actually changes about how a function compiles and runs.

The Tier Model

Every function in Ubel Stratum declares which memory strategy it uses. The compiler enforces the rules statically, at every tier, regardless of whether the program runs through the interpreter or, eventually, compiles to native code.

TierAnnotationMemoryawaitTypical use
HIGH@tier(high) (default)Garbage collectedAllowedBusiness logic, I/O
MID@tier(mid)Arena allocatedNot allowedParsers, hot paths
LOW@tier(low)Manual, borrow-checkedNot allowedSystems code, FFI, packet handling

A function with no @tier annotation is HIGH. Lower tiers are opted into for performance, never opted out of by default.

The core constraint

A MID-tier function allocates data in an arena. Once that arena is freed, every pointer into it becomes invalid, so HIGH-tier code must never hold a live pointer into a freed arena. The type system enforces this at compile time: a value living in arena A carries a type parameterized by the arena’s lifetime (&'a T), and the compiler rejects any program where an &'a T appears in a type that outlives arena A.

Three patterns cross the MID to HIGH boundary safely. These are illustrative of the pattern the tier checker enforces, not a claim that these exact function names ship in a standard library yet; there is no standard library today.

Pattern 1: callback

MID parses into an arena, calls a HIGH-tier closure with a borrow into the arena, the closure produces a GC-owned result, then the arena frees. The closure’s borrow never outlives the arena.

@tier(mid)
fn parse_json_with<F, R>(input: string, callback: F) R
    where F: fn(&JsonView) R   // R must contain no arena references
{
    with arena(1MB) {
        let view = build_json_view(input)
        return callback(&view)
    }
}

@tier(high)
fn handle_request(req: Request) Response {
    parse_json_with(req.body, fn(json) {
        let user_id = json.get("user_id").as_int()
        return fetch_user(user_id)
    })
}

Pattern 2: iterator

For processing large datasets without materializing the whole result at once. MID drives the iteration; HIGH only ever sees GC-owned values, one at a time.

@tier(mid)
fn transform_each<R>(items: &[Item], f: fn(&TransformedItem) R) List<R> {
    with arena(10MB) {
        let mut results = List.new()
        for item in items {
            let transformed = expensive_transform(item)
            results.push(f(&transformed))
        }
        return results
    }
}

Pattern 3: view

Syntactic sugar over the callback pattern for read-only access, following the same rules.

@tier(high)
fn parse_config(path: string) Config {
    let config_view = read_toml_view(path)
    using let v = config_view {
        let host = v.get("host").to_owned()
        let port = v.get("port").as_int()
        return Config { host, port }
    }
}

Rejected at compile time

// storing an arena reference in a GC-managed struct
@tier(high)
struct BadCache {
    data: &JsonView   // error: contains an arena lifetime
}

// HIGH tier constructing an arena directly
@tier(high)
fn bad() {
    with arena(1MB) { }   // error: 'with arena' is MID-tier only
}

// a cross-tier function returning an arena reference
@tier(mid)
fn bad_leak(input: string) &JsonView {
    with arena(1MB) {
        return &parse(input)   // error: return type carries an arena lifetime
    }
}

The cross-tier call matrix

Not simply “LOW cannot call HIGH”: every direction is a separate rule.

CallerCalleeAllowed
HIGHMIDYes (callback/view patterns encouraged, not required)
HIGHLOWYes
MIDHIGHNo, an arena lifetime could escape
MIDLOWYes
LOWHIGHNo
LOWMIDNo

What is enforced today

The cross-tier call matrix above, arena-escape checking for the patterns shown, and lifetime well-formedness (declared lifetime names must exist, where clauses can only reference declared names, no outlives cycles) are real, running checks in the semantic analysis pass, independent of which backend eventually executes the program.

Two things are still in progress rather than complete:

LOW-tier borrow checking. The syntax and structural-type layer for references (&, ref, &mut, ref mut, *, deref) exists, along with the first piece of the borrow checker itself, a control-flow graph builder. Real loan and liveness enforcement, the part that actually rejects a use-after-move or a conflicting borrow, is not built yet. The target is NLL-style liveness precision rather than a coarser lexical-scope approximation.

The interpreter’s memory model. The tree-walking interpreter runs every tier on the same reference-counted representation. with arena blocks are recognized and validated by the tier checker but do not yet allocate or free real memory in the interpreter; genuine bump-allocation arrives with the LLVM backend. The static rules above are enforced regardless, since tier checking happens independently of execution, but running a MID-tier function today does not yet exercise real arena memory pressure or reclamation timing.

Language Tour

A tour of the surface syntax. Every example below is real syntax the project documents; none is invented for this page.

Tier annotations

// Default: HIGH, no annotation needed for most code
fn handle_request(req: Request) Response {
    let user = fetch_user(req.user_id)
    return Response.ok(user.to_json())
}

// Opt into MID for a hot path
@tier(mid)
fn parse_payload(body: string) ParsedData {
    with arena(1MB) { }
}

// Opt into LOW for systems code
@tier(low)
fn write_packet(buf: &mut [u8]) usize {
    // raw ownership; borrow checker enforcement is still in progress
}

Collections

Collection names follow C# convention rather than Rust’s.

let mut numbers = List.new()
numbers.push(1)
numbers.push(2)

let mut scores = Dictionary<string, int>.new()
scores.set("Alice", 100)   // set/get, not insert, for symmetry

let items = [1, 2, 3, 4, 5]
let names = ["Alice", "Bob"]   // inferred as List<string>

Unified member access

There is no ::. Type-level calls and instance calls both use ..

summon std.collections.List
let list = List.new()   // type-level call
list.push(42)            // instance call

Error handling

A trailing ! on a return type means the function may fail; ? propagates a failure out of the current function.

fn parse_int(s: string) int! {
    // returns a Result-like value
}

fn process(s: string) int! {
    let n = parse_int(s)?
    return n * 2
}

Async

Async is HIGH tier only. MID and LOW are synchronous by design: arenas have lexical lifetimes, which do not compose with the way an async function suspends and resumes across await points.

@tier(high)
async fn fetch_user(id: int) Task<User>! {
    let resp = await http_get($"/users/{id}")?
    return await parse_user(resp.body)?
}

Structs and methods

struct Rectangle {
    width: int,
    height: int

    pub fn new(w: int, h: int) Rectangle {
        return Rectangle { width = w, height = h }
    }

    pub fn area(self) int {
        return self.width * self.height
    }
}

Pattern matching

match response {
    Ok(data) where data.status == 200 => process_success(data),
    Ok(data) => log_warning($"Status: {data.status}"),
    Err(NetworkError(extract { code, message })) => {
        log_error($"Network error {code}: {message}")
    }
    Err(e) => log_error($"Unknown: {e}"),
}

Pipe operator

let result = data
    |> parse?
    |> validate?
    |> transform
    |> save

Extension functions

extend int {
    fn is_even(self) bool { return self % 2 == 0 }
}

if 42.is_even() { println("Even!") }

References and lifetimes (LOW tier)

&/ref and &mut/ref mut are dual spellings of the same borrow operator, */deref likewise for dereference, the same relationship and/&& and or/|| already have. Either spelling compiles to the identical AST node; named lifetimes are only needed for cross-function borrows complex enough that inference cannot resolve them alone.

// inferred, no annotation needed
fn first(list: &List<int>) &int {
    return &list[0]
}

// same thing, keyword spelling
fn first(list: ref List<int>) ref int {
    return ref list[0]
}

// explicit lifetime for a genuinely ambiguous case
fn longest[lifetime L](x: &L str, y: &L str) &L str {
    if x.len() > y.len() { return x } else { return y }
}

The syntax and structural typing for references are complete. The borrow checker that actually verifies a program’s borrows are sound (loan tracking, liveness, move checking) is still in progress; see The Tier Model for exactly where that line sits today.

RAII with using

using let file = File.open("data.txt") {
    let content = file.read()
    process(content)
}   // file.close() runs automatically

Query pipelines

Linqerizer<T> is a lazy, chainable query pipeline over any collection, HIGH tier only. .query() snapshots the source once; nothing runs until a terminal call (.to_list(), .first(), .count()) walks the pipeline, and each chained call returns a new pipeline rather than mutating in place.

@tier(high)
fn active_adult_names(users: List<User>) List<string> {
    return users.query()
        .where(fn(u) u.age >= 18 and u.status == "active")
        .order_by(fn(u) u.name)
        .select(fn(u) u.name)
        .to_list()
}

.group_by(...) produces a real Dictionary<Key, List<Value>>. An earlier query-comprehension grammar (from x in ... where ... select, closer to C#’s LINQ syntax) was removed outright rather than kept alongside this: it was eager, List-only, group_by was a stub, and it had no fixture coverage. Linqerizer<T> needs no dedicated grammar at all; it parses as ordinary method calls.

Project Status

Ubel Stratum is early and under active development. This page tracks what exists today at a language level; the CI Results page tracks the fixture suite and benchmark history behind these claims.

Phases

PhaseStatusCovers
1DoneCore design, memory model, lexer, arena AST, the recursive-descent/Pratt parser
2DoneSemantic analysis: name resolution, type inference, tier enforcement, generics, the three-tier escape-boundary checker
3DoneTree-walking interpreter, the current execution model
4Not startedLLVM backend, native binaries
5Not startedStandard library, tooling, package manager

Phase 2 being marked done covers full generics for structs and enums, enum discriminants and payloads, arena and pool escape checking, and generational handles through Pool<T>/Handle<T>. It does not mean every corner of semantic analysis is finished: LOW-tier borrow checking specifically has its syntax and structural typing in place along with a control-flow-graph builder, but no loan or liveness enforcement yet. See The Tier Model for the exact line between what parses and type-checks versus what is actually verified safe.

Recently landed

  • _ as both a match wildcard and a parameter placeholder
  • @derive for PartialEq, with Eq, Hash, Ord/PartialOrd, and Clone following
  • Lifetime well-formedness checking: declared lifetime names must exist, where clause bounds can only reference declared names, no outlives cycles, for both function signatures and edge struct fields
  • Method dispatch through Unique<T>/Shared<T>/SyncShared<T> ownership wrappers

Active work

  • Connecting edge struct’s is_edge marker to the arena-escape checker, its documented purpose today has no effect on that checker
  • Real outlives and loan-tracking enforcement for the LOW-tier borrow checker, a substantially larger piece of work than the well-formedness checking already in place

Known gaps, tracked rather than hidden

  • edge struct is parsed and stored on the AST but not yet consulted by the arena-escape checker, so it does not yet do what its name implies
  • The interpreter runs every tier on the same reference-counted values; with arena blocks are validated by the tier checker but do not yet allocate or free real memory, that lands with the LLVM backend
  • No package manager, no installable compiler, no standard library beyond the built-in collection and instance methods documented in the Language Tour

Source and deeper documentation

The GitHub repository carries the full engineering documentation this site draws from: MEMORY_MODEL.md, PARSER_RULES.md, DIAGNOSTICS_RULES.md, ENUM_RULES.md, GENERICS_RULES.md, DATASTRUCTURES.md, and PARKED_IDEAS.md among others, each maintained alongside the code it describes rather than as a separate, drifting reference.