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.
| Tier | Annotation | Memory | Async | Use case |
|---|---|---|---|---|
| HIGH | @tier(high) (default) | Garbage collected | Yes | Business logic, I/O |
| MID | @tier(mid) | Arena allocated | No | Parsers, hot paths |
| LOW | @tier(low) | Manual, borrow-checked | No | Systems, 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:
| Language | Bet | Cost |
|---|---|---|
| Go, Java, C# | GC everywhere | Pauses, GC pressure, heap bloat |
| Rust | Ownership everywhere | Steep learning curve, slow compiles |
| C, C++ | Manual everywhere | Safety 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.
| Phase | Status | Covers |
|---|---|---|
| 1 | Done | Core design, memory model, lexer, arena AST, parser |
| 2 | Done | Semantic analysis: name resolution, type inference, tier enforcement |
| 3 | Done | Tree-walking interpreter, the current execution model |
| 4 | Not started | LLVM backend, native binaries |
| 5 | Not started | Standard 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.
| Tier | Annotation | Memory | await | Typical use |
|---|---|---|---|---|
| HIGH | @tier(high) (default) | Garbage collected | Allowed | Business logic, I/O |
| MID | @tier(mid) | Arena allocated | Not allowed | Parsers, hot paths |
| LOW | @tier(low) | Manual, borrow-checked | Not allowed | Systems 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.
| Caller | Callee | Allowed |
|---|---|---|
| HIGH | MID | Yes (callback/view patterns encouraged, not required) |
| HIGH | LOW | Yes |
| MID | HIGH | No, an arena lifetime could escape |
| MID | LOW | Yes |
| LOW | HIGH | No |
| LOW | MID | No |
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
| Phase | Status | Covers |
|---|---|---|
| 1 | Done | Core design, memory model, lexer, arena AST, the recursive-descent/Pratt parser |
| 2 | Done | Semantic analysis: name resolution, type inference, tier enforcement, generics, the three-tier escape-boundary checker |
| 3 | Done | Tree-walking interpreter, the current execution model |
| 4 | Not started | LLVM backend, native binaries |
| 5 | Not started | Standard 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@deriveforPartialEq, withEq,Hash,Ord/PartialOrd, andClonefollowing- Lifetime well-formedness checking: declared lifetime names must exist,
whereclause bounds can only reference declared names, no outlives cycles, for both function signatures andedge structfields - Method dispatch through
Unique<T>/Shared<T>/SyncShared<T>ownership wrappers
Active work
- Connecting
edge struct’sis_edgemarker 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 structis 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 arenablocks 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.