Learn

Learn U

From a function to a concurrent web server with database transactions. Every snippet is valid U — paste any into the playground to see the C it produces.

Functions

A function is f, a name, typed parameters, and a return type. The body is indented with tabs, and r => returns. Return type is optional for void functions.

U
f add(first: I, second: I) -> I
	r => first + second

f greet(name: S) -> S
	r => "Hello, {{name}}!"

f log_it(msg: S)
	System.stderr(msg)

Scalar types

I integer, N float, S string, L boolean, Q rational, D date, B byte. Sized variants: I8 I16 I32 I64 U8 U16 U32 U64 N32 N64.

U
f area(width: N, height: N) -> N
	r => width * height

f is_even(num: I) -> L
	r => num % 2 == 0

Lists and maps

[T] is a list, {K: V} is a map. Iteration uses .on() — no for loops. Pipelines chain, and the compiler fuses them into one pass.

U
f even_squares(nums: [I]) -> [I]
	r => nums.filter(num => num % 2 == 0).map(num => num * nn)

// Accumulator needs +M — locals are immutable by default
f total(prices: [N]) -> N
	sum: N +M = 0.0
	prices.on(price => (
		sum = sum + price
		none
	))
	r => sum

Classes

d declares a type. Fields are +M by default — the class's own methods (t) can mutate them. Mark -M for frozen-after-construction fields.

U
d Point
	x: N             // +M by default — instance field
	y: N
	id: I -M          // frozen after construction

	f move(dx: N, dy: N)
		t.x = t.x + dx   // t = self
		t.y = t.y + dy

	f distance() -> N
		r => Math.sqrt(t.x * t.x + t.y * t.y)

Enums — there is no enum keyword

U has no enum, no switch, no match. Instead, variants are subtypes, and dispatch is .on() with typed handlers — the same .on() that iterates lists. One mechanism, two uses.

U — define variants as subtypes
d Shape
	color: S

d Circle : Shape
	radius: N

d Rect : Shape
	width: N
	height: N

d Triangle : Shape
	base: N
	height: N

Circle, Rect, and Triangle are subtypes of Shape. They inherit color and add their own fields. This IS the enum — each subtype is a variant.

U — dispatch with .on()
f area(shape: Shape) -> N
	shape.on(
		(sh: Circle) => r => 3.14159 * sh.radius * sh.radius,
		(sh: Rect) => r => sh.width * sh.height,
		(sh: Triangle) => r => sh.base * sh.height / 2
	)

The compiler checks exhaustiveness. If you add d Pentagon : Shape tomorrow, every .on() that dispatches on Shape will fail to compile until you add the Pentagon handler. This is the guarantee: add a variant, every handler site is flagged. No runtime surprise.

Compare with other languages:

TypeScript — switch (fragile)
switch (shape.kind) {
  case "circle": return Math.PI * shape.radius ** 2;
  case "rect":   return shape.width * shape.height;
  // forgot triangle — compiles fine, crashes at runtime
}
U — .on() (exhaustive)
// forgot Triangle — compile error:
// "Triangle not handled in .on() dispatch on Shape"
shape.on(
	(sh: Circle) => r => 3.14159 * sh.radius * sh.radius,
	(sh: Rect) => r => sh.width * sh.height
)  // ✗ error

Why arity, not keywords. The handler's parameter type IS the pattern. (sh: Circle) => matches when the value is a Circle. (sh: Rect) => matches when it's a Rect. No case, no match, no is. The function's arity — the number and types of its parameters — determines which handler fires. This is the same mechanism that .on() uses for iteration:

U — same .on(), two uses
// Iteration: .on() with one handler, one param
items.on(item => process(item))

// Dispatch: .on() with multiple handlers, typed params
result.on(
	(val: Success) => log(val.data),
	(err: Failure) => log(err.message)
)

Similarly, ? replaces if and ! (in the signature) replaces throws. U doesn't have control-flow keywords — it has operators that compose with the type system:

U — operators replace keywords
// ? replaces if:
val < 0 ? x NegativeError()    // guard: if val < 0, throw
val < 0 ? r => 0               // guard: if val < 0, return 0

// The "else" is just the next line:
f abs(val: I) -> I
	val < 0 ? r => 0 - val     // if negative, return negated
	r => val                    // otherwise, return as-is

// ! in the signature replaces throws/try/catch:
f parse(input: S) -> Config ! ParseError ! IoError
	// callers MUST handle both errors or declare them in their own !

// :: replaces instanceof/is:
val :: Circle ? log(val.radius)  // narrows type in scope

The pattern: where other languages add keywords (enum, switch, match, if, else, instanceof, throws), U uses operators (.on(), ?, !, ::) that compose with the type system. Fewer keywords, more safety — the compiler checks exhaustiveness, not the programmer's memory.

x and ?? — errors and missing values

U has two operators for "this might not work": x for errors and ?? for none. They're different things and U keeps them separate.

U — x for errors, ?? for none
// ?? is for missing values (map lookup, optional field)
name = req.query.get("name") ?? "World"    // get() returns S +N
port = config.get_int("port") ?? 8080

// x is for operations that THROW on failure
data = file.read("config.json") x "{}"     // file.read throws FileError

// x chains right-to-left: try each, use first that succeeds
config = file.read("a.json") x file.read("b.json") x "{}"

// x.on() dispatches by error type — terminal (no x after it)
user = fetch(id) x.on(
    (err: Timeout) => cached_user,
    (err: NotFound) => guest_user()
)   // if AuthError thrown, it propagates up

?? handles none — something was simply absent. x handles errors — something tried and failed. x chains right-to-left. x.on() is terminal: if its handlers throw, the error propagates to the caller.

+R — where the value lives

Without +R, values live on the stack. +R moves them to the refcounted heap — the compiler emits retain/release. No garbage collector, no manual free.

U
d Session
	token: S
	user: S

f open(tok: S, user: S) -> Session +R
	r => Session({ token: tok, user: user })

+R(parent) — no garbage collector needed

Garbage collectors exist because reference cycles keep objects alive forever. U eliminates cycles at compile time: the compiler builds the type reference graph, detects potential cycles, and requires +R(parent) on back-edges. Strong +R references must form a DAG.

U — the compiler catches this
d Element
	children: [Element +R]    // strong — parent owns children
	parent: Element +R +N     // ✗ compile error!
	// "Element.parent is a back-pointer to Element.
	//  Mark it +R(parent) to prevent cycles."
U — the fix
d Element
	children: [Element +R]              // strong — parent owns children
	parent: Element +R(parent) +N       // weak — doesn't hold parent alive
	tag: S

+R(parent) is a weak reference — it doesn't increment the refcount. When the parent is freed, the weak ref becomes none. This is why +R(parent) implies +N (nullable).

The compiler checks cross-type cycles too:

U — cross-type cycle
d Author
	books: [Book +R]            // strong — author owns books

d Book
	author: Author +R           // ✗ error: Author → Book → Author cycle!
	// Fix: author: Author +R(parent) +N

The full set of +R qualifiers:

Reference
+R              strong, downstream — slab-allocated, ARC
+R(parent)      weak, upstream — no retain, implies +N
+R(pool)        region/arena — bump-allocated, freed with pool
+R(GPU)         GPU device memory

Why this eliminates GC. When the root reference dies, its children's refcount drops, their children's refcount drops — the tree is freed deterministically. +R(parent) weak refs can't prevent deallocation. The DAG guarantee means every object has a clear owner. This is the same principle that makes the DOM's ownerDocument work — U just enforces it at the type level.

±M — who can mutate what

The core safety rule: +M defaults to things owned by an instance. Everything else is -M. Parameters are -M — the callee borrows read-only. -M propagates inward through fields and elements.

U
// -M parameter: can't modify the Point through it
f describe(pt: Point) -> S
	// pt.x = 5.0  ← compile error: -M propagates
	r => "(" + pt.x.__string__() + ", " + pt.y.__string__() + ")"

// +M parameter: declares intent to modify
f zero_out(pt: Point +M)
	pt.x = 0.0
	pt.y = 0.0

// Element +M hole-punch: container frozen, elements writable
f normalize(pts: [Point +M])
	// pts.push(...)  ← compile error: container is -M
	[1..pts.len].on(idx => (
		pts[idx].x = pts[idx].x / 100.0  // ok — element is +M
		pts[idx].y = pts[idx].y / 100.0
		none
	))

x — error handling without try/catch

x works in three positions. Prefix: throw. Postfix: capture or provide fallback. Statement: register reusable policies. No try/catch. No catch(Throwable). No silent swallowing.

U — throwing and declaring
d InsufficientFunds
	required: Q -M
	available: Q -M

f withdraw(account: Account +R, amount: Q) -> Q ! InsufficientFunds
	account.balance < amount ? x InsufficientFunds({
		required: amount, available: account.balance
	})
	account << { balance: account.balance - amount }
	r => account.balance

The ! in the signature is the contract. Callers must handle it.

U — two ways to handle
// 1. Fallback value — catches any error, provides substitute
balance = withdraw(acct, amount) x 0        // on error: balance = 0

// 2. Typed dispatch — per-error fallback values
balance = withdraw(acct, amount) x.on(
	(err: InsufficientFunds) => err.available   // return what's left
)
// No "capture and check later" — handle it HERE or propagate it

Reusable policies go at the top of a function. They're middleware: retry, log, observe. They return L +N (true=handled, none=propagate) and are generic — define once, use across many functions.

U — policies + fallbacks together
f process(input: S) -> Dashboard ! AuthError
	x.on(retry_policy)                           // retry timeouts (reusable)
	x.on(logging_policy)                         // log all errors (reusable)

	// Happy path — clean
	user = fetch_user(input) x guest_user()      // typed fallback
	data = db.get(user.id) x default_data()      // typed fallback
	r => Dashboard({ user: user, data: data })

Policies fire first (retry, log). If the error still reaches the expression, postfix x provides the fallback value. Two layers that compose.

+G — static and global state

+G fields belong to the class, not the instance. They are -M by default — global constants. Mutable globals need +G +M and all writes must use <<.

U
d Config
	MAX_RETRIES: I +G = 3        // global constant — no annotation needed
	APP_NAME: S +G = "MyApp"     // global constant

d Counter
	val: I +G +M = 0             // global mutable — MVCC-managed

	f+G increment()              // +G = static method (no t)
		Counter << { val: Counter.val + 1 }

	// Counter.val = 5  ← compile error: use << for +G +M

MVCC — the << operator

<< is the atomic write — patches one or more fields in a single operation. No lock, no mutex, automatic retry on conflict.

U
d Metrics
	requests: I +G +M = 0
	errors: I +G +M = 0
	last_path: S +G +M = ""

f on_request(path: S)
	// Patch two of three fields atomically
	Metrics << { requests: Metrics.requests + 1, last_path: path }

f on_error(path: S)
	Metrics << { errors: Metrics.errors + 1, last_path: path }

// Instance MVCC — t can use << too
d Account +R
	balance: Q
	holder: S
	status: S

	f deposit(amount: Q)
		// Patch balance and status, leave holder unchanged
		t << { balance: t.balance + amount, status: "active" }

Transactions — << ( ... )

Multiple << patches, one atomic commit. Only pure, deterministic code inside. Retries automatically on conflict. x Rollback(...) exits cleanly.

U
d AccA
	bal: Q +G +M = 0.0
	name: S +G +M = ""
	last_tx: I +G +M = 0
d AccB
	bal: Q +G +M = 0.0
	name: S +G +M = ""
	last_tx: I +G +M = 0

f transfer(amount: Q, tx_id: I) ! InsufficientFunds
	rate = get_rate()             // +D — before the block
	<< (
		fee = compute_fee(AccA.bal, rate)    // -E -D only ✓
		AccA.bal < amount + fee ? x Rollback("insufficient")
		AccA << { bal: AccA.bal - amount - fee, last_tx: tx_id }
		AccB << { bal: AccB.bal + amount, last_tx: tx_id }
		// name is NOT patched — stays unchanged
	)                             // all-or-nothing
	log("transferred")            // +E — after the block

+A — async fibers

a before a call makes it async — a fiber. The scheduler handles concurrency on one thread with epoll. No ThreadPoolExecutor, no GIL.

U
a f fetch(url: S) -> S
	resp = a HTTP.get(url)
	r => resp.text()

// Fan-out: fetch all URLs concurrently
f fetch_all(urls: [S]) -> [S]
	tasks = urls.map(url => a fetch(url))
	r => tasks

Events — e(obj).on()

e(obj) wraps an object as an event emitter. .on() subscribes. owner defaults to t inside methods — cleanup is automatic.

U
d ClickEvent
	x: I -M
	y: I -M

d Button +R
	label: S
	f click(x: I, y: I)
		e(t).emit(ClickEvent({ x: x, y: y }))

d Page +R(RAII)
	f setup(btn: Button +R)
		// owner defaults to t — auto-removed on Page cleanup
		e(btn).on(click => Log.info("clicked"))

		// need the source? second param is EventContext
		e(btn).on((click, ctx) => (
			Log.info(ctx.target.label + " at " + click.x.__string__())
		))

HTTP server

The handler is a pure function: Request in, Response out. The runtime handles TCP, epoll, fibers, keep-alive.

U
f list_users(req: Request) -> Response
	users = Database.Query({ store: "users" })
		.select(["name", "email"])
		.orderBy("name", "ASC")
		.limit(50)
		.fetchAll()
	r => Response.json(users)

f main()
	serve(8080, {
		"GET /users": list_users,
		"POST /users": create_user
	})

Database queries

The query builder produces parameterized SQL — dialect-aware ($1 for Postgres, ? for MySQL). No injection, ever.

U
d User : Database.Row
	name: S
	email: S
	score: I

f top_users(min_score: I) -> [User]
	r => Database.Query({ store: "users" })
		.where("score", ">=", min_score)
		.orderBy("score", "DESC")
		.limit(10)
		.fetchAll()

// Transactions work with database rows too
f award(from_id: I, to_id: I, points: I) ! NotEnough
	<< (
		sender = Database.Query({ store: "users" }).where("id", "=", from_id).fetchRow()
		recipient = Database.Query({ store: "users" }).where("id", "=", to_id).fetchRow()
		sender.score < points ? x Rollback("not enough")
		sender << { score: sender.score - points }
		recipient << { score: recipient.score + points }
	)

Templates

Template literals with compile-time validation. SQL templates lower to prepared statements automatically.

U
// HTML template — injection-safe
f page(title: S, body: S) -> S
	r => HTML`<html><head><title>{{title}}</title></head>
		<body>{{body}}</body></html>`

// SQL template — compiles to prepared statement
f find_user(email: S) -> Tree
	r => SQL`SELECT * FROM users WHERE email = {{email}}`

±E ±D — effects and determinism

Two orthogonal axes that track what a function does: +E = has side effects (I/O, logging, network). +D = nondeterministic (random, clock, external input). The defaults are -E -D — pure and deterministic. This is what makes << ( ) transactions safe: the compiler only allows -E -D code inside.

U
// Pure, deterministic — safe for transactions, memoization, vectorization
f compute_fee(balance: Q, rate: Q) -> Q
	r => balance * rate * 0.01

// Effectful — writes to the outside world
f+E save_log(msg: S)
	File.append("/var/log/app.log", msg)

// Nondeterministic — reads from unpredictable source
f+D roll_dice() -> I
	r => Random.int(1, 6)

// Both — effectful AND nondeterministic
f+E+D record_timestamp()
	ts = Time.now()                  // +D: reads clock
	File.append("log.txt", ts.__string__())  // +E: writes file

// This is why transactions work:
<< (
	fee = compute_fee(bal, rate)    // -E -D ✓ — allowed
	// save_log("hi")              // +E — compile error!
	// dice = roll_dice()          // +D — compile error!
	Account << { balance: bal - fee }
)

Generators and +W streams

e value yields a value from a generator — the function suspends and resumes when the consumer pulls the next value. +W marks the return type as an infinite stream. [1..w] is a built-in infinite range.

U
// Fibonacci generator — infinite stream
f fib() -> I +W
	prev = 1
	curr = 1
	[1..w].on(idx => (
		e curr                    // yield current value
		next = prev + curr
		prev = curr
		curr = next
		none
	))

// Consume: take first 10 Fibonacci numbers
f first_ten() -> [I]
	r => fib().take(10)

// Pipelines work on infinite streams
f even_fibs() -> [I]
	r => fib().filter(num => num % 2 == 0).take(5)

z f — compile-time evaluation

z f declares a function that runs at compile time. The compiler evaluates it, inlines the result, and emits no runtime call. Must be -E -D (pure, deterministic) — the compiler can't do I/O.

U
// Computed at compile time — no runtime cost
z f factorial(num: I) -> I
	num <= 1 ? r => 1
	r => num * factorial(num - 1)

// The compiler evaluates this and emits: result = 120
f main() -> I
	result = factorial(5)
	r => result

// z f __load__() runs at module load time
z f __load__()
	Config << { root: load_config_tree() }
	r => none

c — explicit copies

c expr produces a deep copy. The compiler doesn't copy implicitly — when you want a snapshot of mutable data, you say so.

U
d State +R
	data: [I]

f snapshot(state: State +R) -> State +R
	r => c state             // deep copy — independent of original

// c+R promotes a stack value to the heap
f make_heap(val: I) -> [I] +R
	local_list = [val, val * 2, val * 3]
	r => c+R local_list      // copy to heap, return +R

u — AI-managed functions

u f marks a function whose body is generated by an LLM. The compiler feeds the LLM the signature, modifiers, comments, and call sites across the project. The generated code is type-checked and modifier-checked like any other code.

U
/// Rank products by relevance to the query.
/// Prefer exact title matches. Boost recent listings.
u f search(query: S, catalog: [Product]) -> [Product]

/// Detect fraudulent transactions from user history.
/// False positive rate must stay under 1%.
u f fraud_check(txn: Transaction, history: [Transaction]) -> RiskScore

// No comment needed — the signature says everything:
f total(prices: [Q]) -> Q
	r => prices.reduce((acc, price) => acc + price, 0)

// The human writes the wiring. The AI writes the logic.
f main()
	serve(8080, {
		"GET /search": (req) => Response.json(
			search(req.query["q"], load_catalog())
		)
	})

Add u — the AI takes over. Remove u — the generated body inlines into your source and you own it. One letter toggles the human-AI boundary. When an LLM generates a U file from scratch, every function is u f by default — honest about its provenance, ready for a human to claim.

+V — vector lanes

+V on data means vector-ready layout. On a function, it means vectorizable computation. The emitted C uses SIMD intrinsics — SSE/AVX on x86, NEON on ARM.

U
f scale_all(arr: [I +V] +R) -> [I +V] +R
	r => arr.map(val => val * 3)

f dot_product(values: [N], weights: [N]) -> N
	sum: N +M = 0.0
	[1..values.len].on(idx => (
		sum = sum + values[idx] * weights[idx]
		none
	))
	r => sum
	// -M default on sum forces local accumulator
	// → GCC/Clang auto-vectorizes into SIMD

+R(GPU) — device memory

+R(GPU) places data in device memory. A .map() over it becomes a WGSL compute shader.

U
f brighten(pixels: [I] +R(GPU)) -> [I] +R(GPU)
	r => pixels.map(px => px * 3 + 1)

Where to go next

The specification is the complete reference. The interview page compares U to Python on five real problems. The snippets page has copy-paste examples. The feature status tells you honestly which of it runs today.