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 | Started | LLVM backend, native binaries. Hand-written IR measured the three tiers, and an emitter turns a growing subset of Ubel into LLVM IR that is checked against the interpreter at every step: int, bool and string values, structs, enums and match, closures, arenas and the garbage-collected heap, Unicode string methods. The target program examples/arena_demo/ast_builder.ubl compiles. Collections, floats and sized integers, generics, traits and a ubel build command are not there yet (docs/LLVM_BACKEND_STATUS.md) |
| 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
has real, CFG-based loan and liveness enforcement in place (genuinely
non-lexical — a borrow’s last actual use determines when it stops
conflicting, not the enclosing block) and move checking alongside it,
but the loan and move checking itself is intra-function. Declared
lifetime parameters ([lifetime L]) are checked for well-formedness,
and outlives enforcement verifies that a reference crossing a function
call or an edge struct field actually respects the declared
relationship, with a few documented scope limits (methods, references
nested inside generics, closures capturing references) — 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- Outlives/subset enforcement: lifetime well-formedness (declared names
must exist,
wherebounds reference declared names, no outlives cycles), plus boundary checks at a function call and atedge structconstruction, with multi-lifetime constraint propagation edge structnow participates in arena-escape checking: a named reference field on anedge structis checked against its declared lifetime rather than the general arena boundary rule- Method dispatch on user-defined structs through
extendand inherentimplblocks, both instance methods andType.method()static calls, withselfnow type-checked inside those bodies - Method dispatch through
Unique<T>/Shared<T>/SyncShared<T>ownership wrappers, for builtin collections and for user-defined struct methods, and calling a user-declared method on aUnique<T>local no longer counts as moving it, so several calls in a row are accepted while a genuine second move is still rejected - Global
constitems are evaluated beforemainruns, in any declaration order, and assigning to one is a compile error typealiases are transparent: withtype Score = int, aScoreand anintare the same type, with no cast. That holds for aliases to primitives, collections, generic struct instances and function types, for generic aliases (type Wrapped<T> = Box<T>), for an alias of an alias, and for an alias declared after its use. An alias to a struct or enum also works where the name is built or matched (P { x = 1 },P.origin(),C.Red, patterns), and an alias cycle is an error- Methods on enums: an
extendblock on an enum adds instance methods and associated functions (Color.make()), an unknown method on an enum value is a compile error, and a struct field of function type can be called directly (config.callback(4)) - A type mismatch inside a generic type (
List<int>againstList<string>) now points at the offending expression instead of the top of the file - Mutable globals:
static NAME: Type = expr(the type is required, andpub staticexports once modules exist). A static is one value shared by every function: assign to it in one function and read it in another. It lives in the HIGH tier, so only@tier(high)code can read or write it, aconstcannot read it, and a local or parameter of the same name shadows it. Pass the value into a MID or LOW function as a parameter instead. Initializers run once beforemain, in any declaration order, and the declared type types a bare literal (static HP: u8 = 250). A@tier(...)written on aconstis now an error rather than ignored - Nested generic arguments (
List<List<int>>,Dictionary<string, List<int>>) parse, and annotated dictionaries type-check - A return type annotation on a lambda is one clear parse error instead of two unrelated name errors, and an unknown method on a struct is reported once instead of twice
- Tooling stubs: a
ubelcommand line (check,run,debug-tokens, with--jsonoutput and stable exit codes) and anubel-lsplanguage server that publishes the compiler’s diagnostics as you type. Both call the same one-function check, so an editor and the command line agree - Fixed-width integers with real wrapping arithmetic and the full
u64range (u8,i8,u16,i16,u32,u64, and the rest), numeric literal suffixes (255u8), and a literal-out-of-range diagnostic - Unsuffixed integer literals take their type from context, the way Rust
does:
let x: u8 = 250, a struct field, a call argument, a return value, an assignment, the other operand of+or<, and amatchpattern all give the literal the type the position expects, with the same out-of-range check (let x: u8 = 300is an error, not a silent wrap). A literal with nothing to constrain it stays a plainint. An integer literal never becomes a float on its own (let f: float = 5needs5.0). Ordering comparisons now work on the fixed-width integers - A parser ambiguity fix: a bare identifier condition immediately
followed by a block whose first statement was a plain assignment
(
if x == y { hit_count = hit_count + 1 }) could misparse as a struct literal;if/while/for/matchheads no longer read a struct literal unless it is parenthesized
Active work
- The trait system is being built in slices (design in
docs/TRAITS_DESIGN.md). Built:impl Trait for Typeworks and is checked (missing and extra methods, signature mismatches, overlap), default methods are inherited or overridden,Selfresolves, an inherent method beats a trait method, two traits supplying one name are told apart withTrait.method(value), and a call made through a trait runs the trait’s method. Trait bounds on generic functions, structs and enums are enforced: a call or construction with a type that lacks the impl is an error, and a method call on a type parameter with no bound is an error.PartialEq,Eq,PartialOrd,OrdandCloneexist without being declared, with anOrderingenum, and work as bounds and as methods on built-in and derived types.Hashand aHasherstruct exist too, and aDictionarykey must beHash. Hand-written impls of those traits are accepted and called by==, the ordering operators,contains, dictionary lookups, sorting and grouping, and by derived comparison, hashing and cloning inside other values. A trait can build on others (trait B: A + C): an impl needs the impls beneath it, a bound onBalso gives whatAandCprovide, and a trait that builds on itself is an error. A trait can take type parameters (trait Convert<T>), a bound can name them (T: Convert<int>), and a trait can build on a generic one. A method can declare generic parameters of its own, with bounds, and a trait can declare generic methods that an impl repeats; a bound on a type alias’s parameter holds at every use of the alias. A primitive, a tuple or an array can implement a trait and takeextendmethods, and a prelude trait can be written by hand where the type lacks it (Hashfordouble). Impls andextendblocks also work for generic types (impl<T: Show> Show for Box<T>,impl Show for Box<int>,extend<T> List<T>), forList,Dictionary,Queue,StackandInlineList, and an impl can be conditional on a bound of its parameter. A call on a value (value.method()), on a type (Type.make()) or through a trait obeys the tier rule: aLOWorMIDfunction cannot call a method that isHIGH. A method has the tier written on it, on itsimpl,extend, struct or trait, or on a@tier(...) { }block around them. A trait method has a tier too, and an impl method has to equal it (a method that writes none takes the trait’s). The prelude traits are@tier(low), so a hand-writteneq,hashorcloneisLOW. A struct or enum can implementDisplay(fn to_string(self) string) to decide howprintln,{x}andto_stringshow it, at any depth; a struct prints its fields in declaration order. A struct or enum can implementIterator<T>(fn next(self) T?) so thatfor x in valuecallsnextuntil it returnsnull;Queue,StackandDictionarycan be looped over too, and aforover anything that cannot be walked is a compile error instead of a run-time panic. A struct or enum can implement the operator traits (Add,Sub,Mul,Div,Rem,BitAnd,BitOr,BitXor,Shl,Shr,NegandBitNot), soa + b,a *= b,-aand~acall it; a type can have one impl per kind of right operand (Vec2 * doubleandVec2 * Vec2), a number can take a struct on its right (2.0 * v), and an operator with no impl, or on an unbounded type parameter, is a compile error instead of a run-time panic. A struct or enum can also implementIndexandIndexSet, soa[i],a[i] = vanda[i] += vcall it (one impl per kind of index, and aListor aDictionarysatisfies the same bounds);List,Dictionary, string and tuple indexing is typed, and indexing what the run time cannot index is a compile error. Adyn Traittype (dyn Shape,&dyn Shape,Unique<dyn Shape>) holds a value of any type that implements the trait, in every tier: a list can hold a circle and a square, a call on an element runs its own impl, a trait that cannot work that way (a method with noself, a generic method, orSelfin a signature) is a compile error, and a value whose type does not implement the trait is a compile error instead of a run-time panic. Next: associated types - Still to decide: whether struct fields get default values. The literal typing it depended on has landed, so it can be taken up next
- The language server publishes diagnostics only; hover, go to definition and completion are not built
Known gaps, tracked rather than hidden
- Arguments to built-in collection methods are not type-checked, so
list.push(200)on aList<u8>stores a plainintand a laterlist[0] + 100fails when the program runs - Format specs for sign, zero padding and numeric base (
{x:+},{x:05},{x:x}) work on plainintonly, not on the fixed-width integers - Constructing through an alias to a type that is not a struct or enum
(
type Score = int, thenScore { x = 1 }) is not rejected at compile time pubwritten on aconstortypeitem, and@tier(...)written on atypeitem, are parsed and then ignored; constants are global to the file and readable from every tier- 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.