U on the Web — Implementation Notes
This document tracks the playground — the
browser experience where people translate their existing code into U so
they can see and feel the language. It is the companion to
implementation.html (which covers the compiler itself). The rule is
the same in both: measure against the reference, name the gaps, promote a
claim only when something external confirms it.
Two halves, kept distinct
U's path to the browser has two independent parts, easy to conflate:
- Frontend (this document): other languages translate into U. A client-side page (CodeMirror + acorn) takes JavaScript, TypeScript, or Python and shows the U equivalent live. This is the shop window — the first thing a newcomer touches.
- Backend (
implementation.html, and §below): the Python reference compiler itself, compiled to WebAssembly via Pyodide, so the browser runs the identical deterministic reference rather than a reimplementation that could drift.
The direction is TO U, not from
The interesting move is translating into U — the JVM/CLR play: be liberal in what you accept, strict in what you emit. Existing code in a language a developer already knows becomes U, and U's verification then applies to it for free. The playground makes that tangible: paste a function you understand, watch it become U you can read. This is why the artifact is called a "→ U" transpiler, not "U →".
Making it load at all
The frontend arrived as a prototype, and "prototype" turned out to mean it did
not run. The examples array was missing a closing ], so the entire
script threw a syntax error on load — nothing worked. Behind that were about forty
more bugs of one kind: the JavaScript AST visitor consistently referenced a
variable named node when its parameter was actually n
(a ReferenceError the moment any while, for,
switch, or try was translated), plus a
use-before-declaration in the parameter handler and a Python-ism —
.strip() — called on JavaScript strings, which have no such method.
All fixed. The page now loads and every bundled example translates without
throwing.
Ground truth: does the U it emits actually compile?
"Translates without throwing" is not "translates correctly." A translator can
happily emit text that looks like U and is not. The only honest test is to feed
the output to the real compiler. A headless harness (harness.js) now
runs every playground example through the translators under node, captures the U,
and the emitted U is checked against the actual u2c linter.
The first measurement was the useful kind of humbling: 8 of 64 examples produced U the reference accepts. The playground looked like it worked while mostly emitting invalid U. That is exactly the gap this discipline exists to expose — the same discipline that took the post-quantum signatures from self-consistent-but-wrong to byte-identical against their reference. A translator you can't run against the real compiler is a translator you can't trust.
Single-letter names: the frontend must rename, not the language relax
Many JavaScript and Python examples use single-letter names (n,
x, i) — idiomatic in those languages for counters and
coordinates. U forbids them: single letters are never valid as names (variables,
parameters, functions, classes must be two or more characters); they are valid
only as properties (point.x, node.f, unambiguous
after the .), and T is the lone single-letter type
parameter. So these examples translate to invalid U — correctly rejected by the
reference.
The honest resolution is that the translator must rename single-letter
names when converting from a language that permits them (JavaScript n
becomes num), rather than the compiler relaxing to accept them. This
is exactly "be liberal in what you accept, strict in what you emit": the frontend
does the accommodating so the emitted U stays clean. (An earlier pass briefly and
wrongly loosened the compiler here; that was reverted, and the spec, linter, and
tests were put back in agreement that single-letter names are not allowed.)
Renaming is now implemented in the JavaScript path: single-letter names are
doubled (n→count, x→xPos) at every
declaration and use so they stay consistent, while single-letter properties
(point.x) are left untouched. That one accommodation — done in the frontend,
where it belongs — moved the scoreboard from 15 to 24 of 64.
Honest status & scoreboard
The playground is a prototype under active hardening. It loads, every example translates without crashing, and the reference-validation harness is wired so drift is caught. Translation correctness is partial and tracked honestly:
| Milestone | Examples emitting reference-valid U |
|---|---|
| As delivered | 0 (page did not load) |
| After load + variable-bug fixes | 8 / 64 (measured) |
| After parameter-type fallback | 13 / 64 |
| After indentation + class fixes (single-letter rule corrected back) | 15 / 64 (JS 8, Py 3, TS 4) |
| After frontend single-letter renaming (n→count, x→xPos) | 24 / 64 (JS 15, Py 3, TS 6) |
After guard-return lowering (no if/else → cond ? r => x) | 29 / 64 (JS 20, Py 3, TS 6) |
After list-method lambda fixes (multi-param parens, [Any]) | 31 / 64 — JavaScript path complete at 22 / 22 (Py 3, TS 6) |
After Python param types + renaming (: Any fallback) | 47 / 64 — JS 22/22, Python 19/22, TS 6/20 |
| After safer TypeScript type-stripping + interface/enum passthrough | 57 / 64 — JS 22/22, Python 19/22, TS 16/20 |
After iteration lowering (.on blocks close on dedent; loop vars renamed) | 59 / 64 — JS 22/22, Python 20/22, TS 17/20 |
After list-comprehension lowering ([e for v in xs if c] → .filter().map()) | 60 / 64 — JS 22/22, Python 21/22, TS 17/20 |
After TypeScript union types + Record/Map generics | 62 / 64 — JS 22/22, Python 21/22, TS 19/20 |
| After inline object types + tuple returns (TypeScript complete) | 63 / 64 — JS 22/22, TS 20/20, Python 21/22 |
| After honest diagnostics for untranslatable constructs (decorator/varargs) | 64 / 64 — all three paths at 100% of the example set |
| CORRECTION (W3): measured against the full type-checker, not just the parser | 40 / 64 — JS 17/22, Python 8/22, TS 15/20 |
| After constructor fields, Math.* mapping, and string-aware Python renaming | 50 / 64 — JS 19/22, Python 14/22, TS 17/20 |
After .toString()→S(x) and ||→?? (null-coalescing) | 51 / 64 — JS 19/22, Python 14/22, TS 18/20 |
| After list-element types, loop/lambda var renaming, and mutability detection | 55 / 64 — JS 21/22, Python 16/22, TS 18/20 |
After Python __init__ → auto-constructor fields (class + inheritance) | 57 / 64 — JS 21/22, Python 18/22, TS 18/20 |
After honest JSON.* diagnostic + last Any eliminated | 58 / 64 — JS 21/22, Python 18/22, TS 19/20 |
Async lowering (async→f+A, await auto-dropped) — and a false-pass correction | 56 / 64 — JS 20/22, Python 18/22, TS 18/20 |
| After fixing param-type inference (key bug + chain recursion) and simple regex literals | 57 / 64 — JS 21/22, Python 18/22, TS 18/20 |
After enumerate/multi-target loops → indexed .on((item, idx)) | 58 / 64 — JS 21/22, Python 19/22, TS 18/20 |
| After call-site parameter inference + return-type inference (Python) | 59 / 64 — JS 21/22, Python 20/22, TS 18/20 |
After preserving TypeScript param types through the strip (incl. optional → +N) | 60 / 64 — JS 21/22, Python 20/22, TS 19/20 |
The indentation contract, and a one-character bug
U has no braces; structure is indentation, and the grammar is exact: inside a
class, fields and methods sit at one tab, method bodies at two. The translator was
emitting class fields and function bodies at two tabs — every class and every
multi-line function produced invalid U. The cause was almost invisible: the
visitor defaulted its indent parameter with ind = ind || '\t', and
in JavaScript the empty string is falsy, so when the top-level driver passed
'' to start at column zero, it was silently coerced back to a tab.
One character — || where the intent was "only default when truly
absent" — double-indented the entire output. Fixed to a null check, plus dropping
a fragile post-hoc "strip one leading tab" hack in favour of emitting at the right
depth to begin with. Five genuine .strip() calls (the Python string
method, invoked on JavaScript strings in the translator's own logic) were also
corrected to .trim(); they would have thrown in the browser. With the
indentation correct, the same parameter-type fallback used for functions was
extended to class fields and method parameters (U requires types; where the source
gives none, the translator now supplies a visible : Any rather than
emitting an untyped field that cannot compile). Scoreboard: 17 to 19 of 64,
JavaScript 10 to 12.
No if, no else — the guard-return lowering
U has no if, no else, no switch: five
operators handle all conditional logic, and the workhorse is the ternary
cond ? a ! b ("?" is yes, "!" is not). The
idiomatic shape for an early return is cond ? r => x — a guard that
returns when the condition holds and otherwise falls through to the next line.
The translator crashed on the most basic form of this. A bare
if (n < 2) return n; — no braces — parses to a consequent that is a
statement, not a block, so it has no .body array; the
fallback path called .body.map(...) and threw, taking out Fibonacci,
Factorial, Clamp, and every other guard-style function. The lowering was rewritten
to first normalize a consequent or alternate into a statement list regardless of
whether it was a block or a bare statement (crash gone by construction), then to
recognize the shapes U expresses directly: if (cond) return x becomes
cond ? r => x; if (cond) return a; else return b
becomes cond ? r => a ! r => b; anything else degrades to a
named, commented structure rather than a wrong translation. This one fix took
JavaScript from 12 to 20 of 22 examples and the overall board from 24 to 29 of 64.
Guard-return is the single most common control-flow shape in the example set, and
it now maps onto U's operators cleanly.
List methods with lambdas — and JavaScript complete
U's list methods take function arguments in the same shape the language uses
everywhere: xPos => expr for one parameter, (a, b) => expr
for two or more. The last two failing JavaScript examples both tripped on this.
arr.reduce((acc, val) => acc + val, 0) was emitting
arr.reduce(acc, val => acc + val, 0) — the multi-parameter lambda
had lost its parentheses, so it read as two separate arguments. And a parameter
used with .length or indexing was being typed [?], which
is not a U type; the "expected ], got ?" error was the
compiler rejecting the placeholder. Fixed: arrow lowering now wraps any
multi-parameter list in parentheses, and an unknown element type is written
[Any], not [?].
With those two, every one of the 22 JavaScript examples now emits U that
the reference compiler accepts. The JavaScript path — the most complete of
the three — is done: functions, recursion, guard-return control flow, classes with
fields and methods, arrow functions, list-method chains with lambdas, and
single-letter renaming all produce valid U, verified against the real
u2c rather than eyeballed. Attention now shifts to the Python path,
which lags at 3 of 22.
Known remaining systematic gaps, each named rather than hidden: method-name mappings (e.g.
Math.sqrt → ^ 0.5), and the Python and TypeScript
paths (a fragile line-based indent parser and regex type-stripping respectively)
which lag the JavaScript path. Single-letter renaming and the guard-return crash,
listed here previously, are now fixed. The governing rule: never present a
plausible-but-wrong translation as correct; where a construct can't be expressed,
the console names the limitation.
The reference compiler in the browser (planned)
The frontend shows U; the backend will run it. The Python
u2c is pure Python with no C extensions, so it should load unmodified
under Pyodide: a page imports the package, calls transpile(u_source),
and gets C back — the identical reference, in the browser. Combined with the
U → C → WASM path (Emscripten on the emitted C), the browser
can take U source, compile it with the real reference compiler, and run the
result, all client-side and deterministic. The SPHINCS+ keygen root is the perfect
cross-target oracle: it must be byte-identical in the browser and natively, or the
port is wrong. This is Phase W of the plan. W1 — loading the reference compiler and
transpiling in the browser — is now done and verified; see below.
W1 — the reference compiler runs in the browser, byte-identical
The first backend milestone is real, and it was proven with the actual runtime
rather than a mock-up. The u2c package is pure Python — the import audit
found no C extensions and nothing outside the standard library (argparse,
dataclasses, hashlib, json, re, and
the like) — so it loads unmodified under Pyodide, the CPython-in-WebAssembly runtime.
A page (u2c-web.html) loads Pyodide, installs the u2c wheel,
and calls transpile(u_source) to produce C entirely client-side.
The claim that matters is determinism: the browser must produce exactly what native
produces, or the port is a different compiler wearing the same name. Two things were
checked. First, the compiler is hash-seed independent — running it under four different
PYTHONHASHSEED values gives byte-identical output, so the dict-ordering and
hash-randomization differences that often bite WASM ports do not apply here. Second, and
decisively, u2c was run inside a real Pyodide WebAssembly runtime (headless,
via the Pyodide node build) and its output compared to native: hello.u,
a recursive fib, and a class with fields all transpiled to C that is
byte-for-byte identical to the native compiler (hello
hashes 9e0bd274… in both). The reference implementation, deterministic,
in the browser — not a JavaScript reimplementation that could drift, but the same Python
the tests run against. W2 (compiling that C to WASM and running it) and W3 (wiring this
behind the playground so its U output becomes runnable) are the next steps.
W2 — the emitted C is WASM-ready (and the determinism oracle)
The second backend step compiles the C that u2c emits to WebAssembly
with Emscripten and runs it, so the playground can show a program's output rather than
its translation. The Emscripten toolchain is a large binary download that this build
environment blocks, so the in-browser compile-and-run is wired in the pipeline page
(u2c-run.html) to load from a CDN and is honest when it can't — it never
fakes a result it did not compute. What could be verified here was verified,
and it is the part that actually gates the port: that the emitted C meets Emscripten's
contract.
An earlier concern was the runtime's use of pthread.h and
unistd.h. It turns out pthreads are already fully behind
#ifdef U_VEC_ENABLE — the +V vectorization path — so the
default build uses no threads at all and needs no shim. A permanent test now enforces
the whole contract: for the runtime plus a real program, the default build has zero
pthread_* calls, makes no fork/exec/
system/popen calls in our own code (libc merely declares
them; Emscripten stubs those), uses only WASM-supported syscalls (read,
write, math, stdio), and compiles clean. Getting that test right took one
correction — the first version flagged libc's fork declaration in
unistd.h as if it were a call, so it was narrowed to scan our runtime and
the generated C rather than the fully-preprocessed output.
The cross-target oracle is the SPHINCS+ keygen root. The same fixed input produces
the root 2f94dbe8bfcb4e0044943fbe8154ed85 from the C compiled natively;
when the WASM build runs, it must print the identical root or the port is wrong, and
the pipeline page checks exactly that. Native side proven; the WASM side is one
toolchain-load away and self-checking when it runs. W3 — wiring this behind the
playground so its U output becomes runnable — is next.
W3 — the real compiler behind the playground, and a correction it forced
W3 wires the Pyodide u2c behind the playground: after a snippet is
translated to U, the same page can validate and compile that U with the reference
compiler — lint() for errors, transpile() for C — both proven
to run in WASM. Feeding real playground output through this loop immediately earned its
keep by exposing a measurement error in this very document.
The harness had been validating translated U with the linter, and counting
an example as passing when the linter reported no parse-level error. But the linter is
not the full compiler. When the actual transpile() ran the translated
Fibonacci through the type-checker, it rejected it: the parameter type Any —
the fallback the translator emitted when it could not infer a type — is not a
real type in U at all. "Parses" had been standing in for "compiles," and the
earlier "64 of 64" counted the former. Measured honestly against the full type-checker,
the true figure was 16 of 64.
The fix is the honest one and most of the gap closes with it: Any was
replaced by I, a real U type and the right default for the numeric
parameters that dominate the examples (where the true type differs, the compiler now
says so instead of waving it through). That alone recovered the board to 40 of
64 — JavaScript 17, Python 8, TypeScript 15 — every one now confirmed by the
full reference compiler, not just its parser. The remaining failures are real and now
visible: constructor-field initialization that references parameters out of scope, a
few renaming mismatches, and the Math.sqrt method mapping. The scoreboard
above keeps the old row struck through against the corrected one, because the point of
wiring in the real compiler was precisely to stop grading the frontend on the easier
question. This is the web story closing its loop: translate to U on the left,
and the genuine reference compiler — the same one the 1072 tests run against — judging
the result on the right.
U