U Overview Learn Playground Snippets Spec Status

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:

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 (ncount, xxPos) 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:

MilestoneExamples emitting reference-valid U
As delivered0 (page did not load)
After load + variable-bug fixes8 / 64 (measured)
After parameter-type fallback13 / 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 passthrough57 / 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 generics62 / 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 parser40 / 64 — JS 17/22, Python 8/22, TS 15/20
After constructor fields, Math.* mapping, and string-aware Python renaming50 / 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 detection55 / 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 eliminated58 / 64 — JS 21/22, Python 18/22, TS 19/20
Async lowering (asyncf+A, await auto-dropped) — and a false-pass correction56 / 64 — JS 20/22, Python 18/22, TS 18/20
After fixing param-type inference (key bug + chain recursion) and simple regex literals57 / 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.