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

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
4StartedLLVM 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)
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 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
  • @derive for PartialEq, with Eq, Hash, Ord/PartialOrd, and Clone following
  • Outlives/subset enforcement: lifetime well-formedness (declared names must exist, where bounds reference declared names, no outlives cycles), plus boundary checks at a function call and at edge struct construction, with multi-lifetime constraint propagation
  • edge struct now participates in arena-escape checking: a named reference field on an edge struct is checked against its declared lifetime rather than the general arena boundary rule
  • Method dispatch on user-defined structs through extend and inherent impl blocks, both instance methods and Type.method() static calls, with self now 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 a Unique<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 const items are evaluated before main runs, in any declaration order, and assigning to one is a compile error
  • type aliases are transparent: with type Score = int, a Score and an int are 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 extend block 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> against List<string>) now points at the offending expression instead of the top of the file
  • Mutable globals: static NAME: Type = expr (the type is required, and pub static exports 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, a const cannot 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 before main, in any declaration order, and the declared type types a bare literal (static HP: u8 = 250). A @tier(...) written on a const is 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 ubel command line (check, run, debug-tokens, with --json output and stable exit codes) and an ubel-lsp language 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 u64 range (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 a match pattern all give the literal the type the position expects, with the same out-of-range check (let x: u8 = 300 is an error, not a silent wrap). A literal with nothing to constrain it stays a plain int. An integer literal never becomes a float on its own (let f: float = 5 needs 5.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/match heads 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 Type works and is checked (missing and extra methods, signature mismatches, overlap), default methods are inherited or overridden, Self resolves, an inherent method beats a trait method, two traits supplying one name are told apart with Trait.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, Ord and Clone exist without being declared, with an Ordering enum, and work as bounds and as methods on built-in and derived types. Hash and a Hasher struct exist too, and a Dictionary key must be Hash. 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 on B also gives what A and C provide, 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 take extend methods, and a prelude trait can be written by hand where the type lacks it (Hash for double). Impls and extend blocks also work for generic types (impl<T: Show> Show for Box<T>, impl Show for Box<int>, extend<T> List<T>), for List, Dictionary, Queue, Stack and InlineList, 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: a LOW or MID function cannot call a method that is HIGH. A method has the tier written on it, on its impl, 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-written eq, hash or clone is LOW. A struct or enum can implement Display (fn to_string(self) string) to decide how println, {x} and to_string show it, at any depth; a struct prints its fields in declaration order. A struct or enum can implement Iterator<T> (fn next(self) T?) so that for x in value calls next until it returns null; Queue, Stack and Dictionary can be looped over too, and a for over 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, Neg and BitNot), so a + b, a *= b, -a and ~a call it; a type can have one impl per kind of right operand (Vec2 * double and Vec2 * 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 implement Index and IndexSet, so a[i], a[i] = v and a[i] += v call it (one impl per kind of index, and a List or a Dictionary satisfies the same bounds); List, Dictionary, string and tuple indexing is typed, and indexing what the run time cannot index is a compile error. A dyn Trait type (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 no self, a generic method, or Self in 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 a List<u8> stores a plain int and a later list[0] + 100 fails when the program runs
  • Format specs for sign, zero padding and numeric base ({x:+}, {x:05}, {x:x}) work on plain int only, not on the fixed-width integers
  • Constructing through an alias to a type that is not a struct or enum (type Score = int, then Score { x = 1 }) is not rejected at compile time
  • pub written on a const or type item, and @tier(...) written on a type item, 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 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.