For teams with a large Python codebase
Exact fractional arithmetic. Vectorized loops that use every core. Lock-free shared state with no interpreter lock. Native machine code with no runtime to ship.
Every claim below carries a marker saying whether it has been run and checked, partly works, or doesn't work yet. The markers are the point of the page — a language project is only worth your time if it tells you where the holes are.
01 — The problem
None of these are news to anyone who has shipped Python at scale. They're listed because they're what U is actually aimed at — not at replacing Python everywhere.
0.1 + 0.2 == 0.3 is False. Decimal and
Fraction fix it and cost roughly 67× a float add in our measurement.
Money, billing, and accounting code pays that tax on every line.
CPU-bound threads serialize on the interpreter lock. The workaround is
multiprocessing: separate heaps, pickled messages, and memory
multiplied by core count.
Type annotations are documentation. Nothing checks them at runtime and nothing compiles against them, so a whole class of errors survives to production and every hot loop stays interpreted.
When a loop is too slow the answer is to leave Python — write C, learn the
C-API or ctypes, and maintain a build. The performance escape hatch
is a different language and a different toolchain.
02 — Exact arithmetic
U has a first-class rational type. Q holds a numerator and a
denominator, reduces to canonical form on every operation, and compares without
ever dividing — so no rounding is introduced anywhere in the chain.
Q is a pair of N64 doubles used strictly as integers. IEEE-754 doubles
represent every integer with absolute value up to 253 exactly, so
ordinary hardware floating point gives exact integer arithmetic across that whole
range — no bignum library, no allocation, two machine words.
That last line is the honest bound. Q is exact while the cross-products
stay under 253. For currency, tax splits, ratios, and unit conversion
that ceiling is far away. For number theory on large integers it isn't, and you
want a bignum rational instead.
>>> 0.1 + 0.2 == 0.3 False >>> 0.1 + 0.2 0.30000000000000004 >>> 0.1 * 3 == 0.3 False # the fix, and its price >>> from fractions import Fraction as F >>> F(1,10) + F(2,10) == F(3,10) True # ~67x slower than a float add # (200k iterations, measured)
f main() -> I xx: Q = Q(1,10) yy: Q = Q(2,10) zz: Q = xx + yy ww: Q = Q(3,10) zz == ww ? r => 1 r => 0 // exit 1 — exact, no library, // two doubles, inlined C
0.1 + 0.2 == 0.3 is true in U, False in Python
f main() -> I xx: Q = Q(6,8) r => xx.den // 6/8 → 3/4
f main() -> I xx: Q = Q(1,2) yy: Q = Q(1,4) zz = xx / yy r => zz.num // (1/2)/(1/4) = 2
< ordering all exact
Q(n,d) was emitting a
bare Q(1, 3) into the C — not a real function — so every Q program died
at gcc with invalid initializer. Separately, the Q comparison table was
nested inside the arithmetic-operator branch, making its == and
< entries unreachable. Both are now wired, with the compiler's 639
codegen tests still passing.
03 — Parallelism
+V on a list is a claim about the data: these elements are independent,
so a map or reduce over them has no cross-lane dependency. The compiler is then
free to emit SIMD and, above a threshold, fork across cores.
import numpy as np vals = np.array([1,2,3,4,5,6,7,8,9,10]) sq = vals ** 2 total = sq.sum() # Fast — but only because the work # left Python entirely. Step outside # what NumPy vectorizes and you are # back to an interpreted loop, or # writing C.
f main() -> I vals: [I] +V = [1,2,3,4,5,6,7,8,9,10] sq = vals.map(v => v * v) r => sq.sum() // +V is the whole change. // The lambda is ordinary U — it is // compiled, not dispatched into a // separate array runtime.
-DU_VEC_ENABLE -pthread(fork-join across cores)
| Concern | Python | U |
|---|---|---|
| Expressing intent | Implicit — rewrite the loop as an array op and hope it stays on the fast path | +V at the declaration site |
| Escaping the fast path | Silent: a non-vectorized op drops you to interpreted speed with no warning | The linter tells you a list needs +V |
| Threading | multiprocessing: separate heaps, pickling | Fork-join over one heap above U_VEC_THRESHOLD (1024 elements) |
| Dependency | NumPy plus its wheels | Generated C; SIMD helpers compile with no pthread at all |
04 — Concurrency
Python's interpreter lock exists partly to make reference counting safe. U reference-counts
per object, so there's no global lock to remove — and for shared mutable state it offers
+M(MVCC), which compiles to a lock-free compare-and-swap retry loop.
import threading
class Counter:
def __init__(self):
self.value = 0
self._lock = threading.Lock()
def bump(self):
with self._lock:
self.value += 1
# The lock is manual, and correctness
# depends on every call site taking it.
# Threads still serialize on the GIL
# for the CPU-bound part anyway.
d Counter
value: I +M = 0
f main() -> I
cc: Counter +M(MVCC) = Counter({ value: 0 })
cc << { value: 42 }
r => cc.value
// +M(MVCC) at the declaration.
// Readers never block. Writers CAS
// and retry. No lock to forget.
_Atomic(Counter_Version*) headandu_mvcc_cas_retry
The << operator is deliberate. You cannot assign a field on an MVCC object
directly — the linter rejects it — because a bare field write can't be made atomic with
respect to the rest of the update. You submit a patch, and the runtime builds a new version
and swaps it in.
-pthread, and it produces the right answer. A
contended multi-writer benchmark has not been run, so treat throughput claims as unproven.
05 — Side by side
An LRU cache with eviction — the standard systems screen. Below is the Python you'd write and the U that compiles to C and runs. Both are complete; neither is a sketch.
from collections import OrderedDict
class LRU:
def __init__(self, cap):
self.cap = cap
self.store = OrderedDict()
def get(self, key):
if key not in self.store:
return -1
self.store.move_to_end(key)
return self.store[key]
def put(self, key, val):
if key in self.store:
self.store.move_to_end(key)
elif len(self.store) >= self.cap:
self.store.popitem(last=False)
self.store[key] = val
d Node
key: S +M = ""
val: I +M = 0
hits: I +M = 0
d LRU
cap: I +M = 2
keys: [S] +M = []
store: {S: Node} +M = {}
f get(kk: S) -> I
t.store.has(kk) == false ? r => 0 - 1
nd = t.store[kk]
nd.hits = nd.hits + 1
t.touch(kk)
r => nd.val
f touch(kk: S) -> none
fresh: [S] +M = []
keep = kk
t.keys.on(ex => ex != keep ? fresh.push(ex) ! none)
fresh.push(keep)
t.keys = fresh
none
f evict() -> none
t.keys.len == 0 ? r => none
victim = t.keys[1]
rest: [S] +M = []
gone = victim
t.store.delete(gone)
t.keys.on(ex => ex != gone ? rest.push(ex) ! none)
t.keys = rest
none
f put(kk: S, vv: I) -> none
t.store.has(kk) == false & t.keys.len >= t.cap ? t.evict()
t.store = t.store.set(kk, Node({ key: kk, val: vv, hits: 0 }))
t.touch(kk)
none
The Python is shorter, and for an interview whiteboard that's the right answer. What the
U buys is that every field carries its contract: keys: [S] +M says this list
is mutable and holds strings, store: {S: Node} +M pins both key and value type.
Change +M to +M(MVCC) on the declaration and the same cache becomes
safe for concurrent readers without touching a method body. That's the argument for
annotations carrying semantics rather than documentation.
keys: [S] +M = [] emitted u_list_new_int32_t(0)
for the empty default regardless of the declared element type, handing a list-of-int to a
list-of-string parameter. Local variables already routed through the typed path; class
construction did not. Fixed, 639 codegen tests still green.
06 — The hard part
Worth stating plainly, because it's the reason no one has "just done it" and the reason our own transpiler is limited.
| Feature | Why it defeats static compilation |
|---|---|
| Duck typing | def area(s): return s.area() — the callee's type is unknown until runtime, so no call can be resolved statically |
| Rebinding | A name can hold an int, then a string, then a list. There is no single machine representation to pick |
__getattr__ | Attribute access is arbitrary user code, not a struct offset |
| Monkey-patching | Classes and modules are mutable at runtime, so no layout or method table can be frozen |
eval / exec | The program isn't fully known until it runs |
| Metaclasses | Class construction itself is programmable |
| Decorators | @app.route("/x") rewrites the function at import; dropping it silently deletes behaviour |
| Generators | yield needs a suspendable frame, not a C stack frame |
globals(), inspect | The program reads and edits its own symbol table |
Every serious project in this space handles the list the same way: by defining a subset of Python that excludes most of it, and being rigorous about the boundary. The interesting engineering isn't translating the easy 80% — it's refusing the other 20% loudly instead of emitting something that looks fine and isn't.
07 — Current state
This section corrects a number we published earlier. It's here rather than buried because an inflated benchmark is worse than a low one.
We reported 24/24 Flask 3.1 files clean. Here is what "clean" was measuring, verbatim from the old batch checker:
var hasRawPy = u.split('\n').some(function(l){
var lt = l.trim();
return !lt.startsWith('//') && !l.startsWith('\t\t\t')
&& (/\bdef\b/.test(lt) || /\bself\./.test(lt));
});
It greps the output for the strings def and self.. No lint,
no compile, no execution. "Clean" meant the text no longer looked like Python. The
transpiler now refuses what it cannot translate, so the number means something:
| Rung | Flask 3.1, 24 files | |
|---|---|---|
Doesn't contain def | 24 / 24 | the old number — meaningless |
| Accepted (no refusal) | 5 / 24 | 19 refused, each with a code and a line |
| Accepted and lints | 0 / 5 | 1, 1, 9, 34, 86 errors — mostly unresolved cross-module names |
| Lints and compiles | 0 | blocked by the rung above |
| Passes Flask's own tests | 0 | not attempted |
Five out of twenty-four is a much worse headline than twenty-four out of twenty-four, and it is the first number here that can survive someone checking it.
A file that cannot be faithfully translated now says so in its own first line, with the construct, the source line, and what to do about it. There is no way to read the output and mistake it for a working translation.
// ═══════════════════════════════════════════════════════════════ // TRANSLATION INCOMPLETE — 1 construct could not be carried into U. // The code below is NOT equivalent to the Python it came from. // ═══════════════════════════════════════════════════════════════ // ✗ PY007 line 1 Decorator dropped — it rewrites the function at // import time and that effect is not carried into U. // → Inline what the decorator does, or register it explicitly. f handler(str: Tree) r => str.upper()
Thirty codes, nineteen of them fatal. Every one names a Python construct whose meaning has no U equivalent, and every one carries a suggested rewrite. These are the counts across the Flask source:
| Code | Construct | Hits in Flask | Why it can't carry over |
|---|---|---|---|
| PY007 | Decorator dropped | 99 | Rewrites the function at import; @app.route is the routing table |
| PY030 | **kwargs | 53 | Keyword dict expanded at the call site; U has fixed arity |
| PY031 | *args | 43 | Variadic parameters; U parameters are fixed |
| PY040 | Monkey-patching | 32 | Rebinding a module or class attribute; U layouts are fixed at compile time |
| PY004 | getattr/setattr | 23 | Attribute name is a runtime value, not a struct offset |
| PY020 | yield | 16 | Needs a suspendable frame; U uses a C stack frame |
| PY013 | __enter__/__exit__ | 6 | No with-statement lifecycle in U |
| PY010 | __getattr__ | 4 | Attribute access as arbitrary user code |
| PY012 | Multiple inheritance | 3 | U has single inheritance, no MRO |
| PY034 | Type rebinding | 2 | One name holding two literal types; U bindings have one type |
| PY001 / PY002 | eval / exec | 1 / 1 | The program isn't fully known until it runs |
| PY025 | global/nonlocal | 1 | U closures capture by declared mutability, not scope escape |
Refusing more is only half of it. These were producing U that could not parse, and now translate correctly:
| Python | Was emitted | Now emitted |
|---|---|---|
def area(s): return s.area() | f area(str: Tree) over a body calling ss.area() | f area(str: Tree) / str.area() |
x in items | xx in items — no such operator | items.contains(xx) |
x not in items | xx ! in items | items.contains(xx) == false |
0 < x < 10 | 0 < xx < 10 — parse error | 0 < xx & xx < 10 |
assert x > 0, "msg" | passed through verbatim | (xx > 0) == false ? x Error({ message: "msg" }) |
pass | pass | none |
a, b = b, a | passed through verbatim | three statements through a temp |
if TYPE_CHECKING: block | import inside a parenthesised block | dropped; it is type-only and never runs |
function-local import | import inside a function body | hoisted to file level |
The first row was the worst of them. uName() mapped a single-letter
parameter through a stock table (s → str) while the body
renamer doubled the letter (s → ss), so every function
with a one-letter parameter emitted a signature and a body that disagreed. Both paths now
share one renamer.
The structural defect is fixed: the transpiler previously had no concept of
"I cannot translate this," so it always succeeded — which is exactly why the
metric read 24/24. It now returns ok: false and a list of coded diagnostics,
and the batch checker reports acceptance rather than the absence of the word
def.
08 — Prior art
Static-subset compilation of Python is a real, established category. Placing U honestly inside it is more useful than claiming a breakthrough.
| Project | Approach | Accepts | Escape hatch |
|---|---|---|---|
| Cython | Python superset with C type declarations, compiled to a C extension | All Python, but only annotated code gets fast | Falls back to CPython objects |
| mypyc | Compiles type-annotated Python using mypy's inference | Typed subset; interops with untyped Python | Falls back to CPython objects |
| Nuitka | Whole-program compile to C, keeping CPython semantics | Essentially all Python | Embeds CPython; speedup is modest |
| Codon | Ahead-of-time to native via LLVM with its own type system | A deliberately restricted Python-like subset | Explicit Python interop bridge |
| PyPy | Tracing JIT, not ahead-of-time | Nearly all Python | n/a — it is a runtime |
| U | Source-to-source into a separate language that compiles to portable C | A named subset — 30 coded refusal rules, reported with construct and line | None: a refused file is refused |
The column that matters is the last one. Every mature project either falls back to CPython or names its subset and rejects the rest. U now does the second: 30 coded rules, 19 of them fatal, each naming the construct and the line. What it still lacks is the fallback — there is no CPython to hand the hard cases to, so a refused file is simply refused.
Where U differs on purpose: the target isn't "Python, faster." It's a language whose type
annotations decide memory residence, mutability, concurrency strategy, and vectorization —
things Python annotations can't express at all, because they aren't checked and aren't
compiled against. Q, +V, and +M(MVCC) have no Python
equivalent to translate from.
09 — Recommendation
Transpiling is the wrong instinct for most of what U is good at, and it's worth saying so on a page about Python.
There is no Python source construct that means +V, +M(MVCC),
or +R. A transpiler can only produce the defaults, so you get U's syntax
without U's actual advantages.
A Python class translated into a U class inherits decisions made for an interpreter: dictionary-backed attributes, per-object allocation, dynamic dispatch. Rewriting the hot module directly is usually less work than fighting that.
The loop that actually needs to be fast is rarely more than a few hundred lines. Writing it in U and calling it is the same integration story as a C extension, without the C-API.
Moving money code from float to Q means revisiting every
arithmetic site regardless of language. If you're touching it all, translate the intent,
not the syntax.
The realistic shape today: keep Python for orchestration, glue, and everything served by its library ecosystem. Write the numerically exact parts, the vectorized parts, and the shared-state parts in U, compile them to C, and call across. That's a claim about a boundary, not a migration.
10 — The ledger
Everything on this page, and what backs it. If a row says not yet, it means we ran it and it didn't work — not that we haven't looked.
| Claim | State | Evidence |
|---|---|---|
| Q exact addition and equality | verified | 0.1+0.2==0.3 → exit 1; Python → False |
| Q multiply, divide, reduce, order | verified | Q(1,3)×Q(3,1)=1; (1/2)÷(1/4)=2; Q(6,8)→3/4; 1/3<1/2 |
+V map and reduce | verified | Sum of squares 1..10 = 385, serial and -DU_VEC_ENABLE |
+M(MVCC) lock-free update | verified | exit 42; emits _Atomic head + u_mvcc_cas_retry |
| MVCC under write contention | not measured | No multi-writer benchmark has been run |
| LRU cache in U | verified | 0 lint errors → gcc → exit 40, correct eviction |
| U compiler test suite | verified | 1,127 passing: 120 parser, 368 linter, 639 codegen |
| Single- and multi-file U → C → binary | verified | return, recursion, closures, classes all run correctly |
| Python → U transpiler: refusal | verified | 30 coded rules; 19 of 24 Flask files refused with construct + line |
| Python → U transpiler: output quality | not ready | 0 of the 5 accepted Flask files lint clean |
| Reference webserver | partial | 16 modules, 0 lint errors, 254 KB C — 116 gcc errors, no binary |
| Standard library | partial | 23 of 75 modules compile |
| Self-hosted compiler | not ready | 0 of 14 transpiled modules compile; 5 crash the compiler |
The honest summary: the language and its compiler are real and tested, and the four
capabilities this page is about — exact rationals, vectorization, lock-free shared state,
native output — all work today. The Python on-ramp now refuses what it cannot
translate instead of emitting broken U silently, which turns an unfalsifiable 24/24 into a
measurable 5/24 with named reasons. Getting those five to lint, and then raising the
acceptance rate, is the work. The reference webserver and self-hosting are blocked on a
different gap: the code generator emits void* where it should propagate a
known type.