A token-efficient intermediate representation and compiler for LLM-generated interfaces.
A model writes 25 lines of markup. The compiler writes the fetch, the cancellation, the loading and empty states, the optimistic update, the rollback, and the accessible names — so the model never spends tokens on them and cannot get them wrong.
Both columns render the same application. The right-hand one is what the compiler emits.
page Tasks │ import { useState, useCallback, useEffect, useMemo } from "react";
│
type Task {id, title, done:bool} │ type Task = { id: string; title: string; done: boolean };
data tasks:Task[] GET /api/tasks │
add POST /api/tasks {title} │ export default function Tasks() {
optimistic:prepend │ const [draft, setDraft] = useState("");
save PATCH /api/tasks/{id} {done} optimistic │ const [tasks, setTasks] = useState<Task[]>([]);
drop DELETE /api/tasks/{id} optimistic │ const [tasksLoading, setTasksLoading] = useState(true);
│ const [tasksError, setTasksError] = useState<string|null>(null);
state draft="" │
state filter=all|open|done │ const tasksList = useCallback(() => {
│ const controller = new AbortController();
head Tasks — {tasks.open.count} open │ retrying("/api/tasks", { signal: controller.signal })
│ .then(…).catch(…).finally(…);
form >tasks.add{title:draft}; draft="" │ return () => controller.abort();
input draft aria="New task" │ }, []);
btn Add primary disabled={!draft.trim()} │
│ const tasksAdd = useCallback(async (body) => {
tabs filter │ const snapshot = tasks;
│ setTasks((prev) => [optimistic, ...prev]);
list tasks where={filter} │ try { … } catch { setTasks(snapshot); }
check {done} >tasks.save │ }, [tasks]);
text {title} strike={done} │
btn Delete quiet aria="Delete {title}" │ … ~150 more lines: two more mutations, the filter
empty Nothing here yet. │ memo, and every branch of the render
│ }
25 lines · 178 tokens │ 187 lines · 1,441 tokens
7.8× fewer estimated tokens to write than the React it compiles to, across all seven fixtures —
measured against the compiler's own output, so no second author is involved. node scripts/measure.mjs
prints it, and CI fails if it drifts. (~3.6 chars/token estimate, not a tokenizer count.)
Compression is the visible claim. This is the load-bearing one.
Six applications, generated by a hosted model, scored in bench/gen:
32 of 37 functional requirements met · 3 of 6 compiled
The model understood the applications — the right resources, mutations, aggregates, enumerated
filters and bound disabled attributes — and failed on surface rules. That gap is the whole argument
for a compiler: those failures have codes, spans and suggestions, and some are repaired with no model
call at all. Broken React is broken silently until someone loads the page.
Two of the failures were the compiler refusing information it already held — a select whose options
were written as option children rather than a state domain, and a field whose accessible name was
sitting in the binding beside it. Fixing both moved the same six generations from 1 of 6 compiling
to 3 of 6, with no regeneration: identical model output, a compiler that stopped saying no.
The two that still fail are GUML0023 — the expression language, not the vocabulary. **, ternaries
and .where(status=new).count are what a model reaches for and the parser does not cover yet.
Pre-Phase-0. The compiler works end to end and is tested. The research question it exists to
answer — can a model actually produce correct GUML, and does that help or hurt? — is still
open, and is blocked on funded API access: the sweep needs 90 generations across three models, and
an attempt returned Your credit balance is too low. Zero generations have run.
Everything that does not need a model is done and runs in CI — the frozen spec, ten tasks, ten paired React
references, the prompt assembly, the scoring harness, the blind scoresheet, and a scoring self-test over
synthetic generations (just phase0-verify). What is missing is the answer, not the instrument.
So every token figure in this repository is an authored-artifact measurement, labelled that way at
each site. The one place a model appears is the bench/gen run above, and it is exploratory, not
Phase 0: six applications from one hosted model, unblinded, scored after the fact. It is reported
because a negative result is worth reporting, not because it settles anything — the gate needs the
pre-registered protocol, and that has not run. See spec/PHASE0.md for the protocol
and ROADMAP.md for what [⛔] marks.
561 Rust tests · 42 runtime tests · 61 conformance cases · 49 diagnostic codes
16 emitted components typecheck under --strict · 7 fixtures render and pass their accessibility rules
15 tree-sitter corpus cases · 19 documents parse with no error node
1M fuzz iterations with no panic · 95.5% of injected mutations detected, 99.0% without desync
| Front end | Lexer, parser, AST, diagnostics. Every error in one pass — a repair loop pays a full generation per round. |
| Analysis | Resolver, accessibility rules, static validator, type inference over expressions and aggregates. |
| Backends | React + TS, Svelte 5, static HTML, Web Components, JSON UI tree, plus A2UI and MCP-UI emitters. One design-system table, one element table, one expression lowering and one liveness answer between all seven. |
| Language | 49 primitives, def user components, js/raw escape hatches, core/app conformance levels, declared effects. |
| Theme | shadcn/ui by default — shadcn's own tokens (--primary, --muted, --border, --ring, --radius, in oklch) and its own class strings, so a host already running shadcn drops it in and its palette applies unchanged. A theme is data: --theme swaps it, and one is refused unless it declares a focus treatment and a contrast floor, which is what keeps a themeable compiler able to promise accessible output. |
| Registry packages | guml add installs a design system; an entry declares its own children, capabilities, accessibility contract and what it lowers to — including <YourComponent> with a generated import. guml.json states the project's vocabulary once, so the editor and CI cannot disagree about it. |
| Repair | guml repair — unwrap a code fence, drop trailing commentary, format, apply every unambiguous fix. Bounded, no model call, and it reports which layer did the work. |
| Tooling | fmt (idempotent, formats invalid input), highlight, validate, fix, registry --docs, source maps, language server, VS Code extension. |
| Distribution | Five npm packages under @guml — see below. The compiler as WebAssembly drives the live playground. |
| Runtime | Generated per resource: retry with backoff, response cache with stale-while-revalidate, in-flight deduplication, invalidation on mutation, optimistic apply and rollback — and an error boundary, but only for a document using an escape hatch. Inlined per file by default; --runtime shared emits one module every page imports, which is what makes invalidation hold across pages rather than only within one. |
| Security | guml capabilities emits a per-document manifest — the exact origins it will contact, whether it contains script, its escape-hatch rate — and a Content-Security-Policy derived from it. --assert-inert is the safe-render gate for an untrusted document; --max-escapes is the CI ratchet. |
| Benchmark | bench/guml-bench — the Phase 6 harness, runnable, with 12 of the 150 tasks it specifies and a report that prints the coverage beside every figure. Plus an edit-locality benchmark measured against diff-based React editing rather than regeneration. |
| Not started | Playwright/axe/Lighthouse — so the emitted output is proven to typecheck and to server-render, and not proven to work when clicked. That is the largest gap in what is verified here. |
| Blocked, not started | Everything needing a model, a grader or participants: the Phase 0 sweep (funded API access), blind semantic scoring (a human grader — the author scoring their own output would invalidate it), grammar-constrained decoding (a local model, since hosted APIs expose no client-side CFG masking), and the two human studies. 20 roadmap items, each with its blocker named on the line. |
Why the compiler owns presentation
Four invariants hold the reliability claim together, and this is the load-bearing one: no class
strings, colours, spacing or ARIA plumbing appear in GUML source. That is the token lever and the
correctness guarantee in the same move — a model that cannot write a class name cannot write the wrong
one, and a btn with no accessible name is GUML0050, a compile error rather than a lint warning.
The others: the parser collects every error in one pass; diagnostic codes are append-only, because the repair loop keys on them; and an unsupported construct produces a warning and a visible marker, never approximate code. A quietly wrong compiler would destroy the claim the whole project rests on.
GUML has no training data, so using it meant ~3,000 tokens of spec in every system prompt. guml mcp
removes that, and adds the thing no prompt can provide:
{ "mcpServers": { "guml": { "command": "guml", "args": ["mcp"] } } }→ guml_registry(["card", "metric", "btn"]) 175 chars, not 3,808
→ guml_check(source) DOES NOT COMPILE — 1 error(s)
error [GUML0030] line 4: unknown tag `crad`
replace with: card
→ guml_repair(source) Applied 1 fix(es). COMPILES.
A prompt can carry a vocabulary. It cannot tell a model whether what it just wrote is correct — that needs the compiler that will build it. Works in Claude Desktop, Claude Code, Cursor and anything else speaking MCP; stdio, no key, no network.
Five on npm, under the @guml scope. Which one you want depends on how much of the compiler you need —
the split follows the compiler's own shape, not packaging convenience.
| package | for | unpacked | Node |
|---|---|---|---|
@guml/core |
Compile, render, diagnose, repair | 959 KB | no |
@guml/fmt |
Formatter, canonical form, classification | 231 KB | yes |
@guml/highlight |
Highlighting, no WebAssembly | 49 KB | yes |
@guml/widgets |
chart, calendar, date, upload, command |
22 KB | yes |
@guml/shadcn |
26 tags over all 61 shadcn/ui components | 257 KB |
And on PyPI:
| package | for |
|---|---|
guml |
Python: render HTML from Flask/FastAPI/Django, and drive an LLM repair loop |
pnpm add @guml/core # compile and render in the browser
pnpm add @guml/fmt # just the formatter — and it runs in Node
cargo install guml-cli # the `guml` command
pip install guml # the compiler for Python, and the CLI without a Rust toolchainguml-fmt sits below the parser — lexer, registry and diagnostics, no codegen — so a formatter-only
build is 178 KB of wasm rather than 787. @guml/highlight has no wasm at all: a hand-written tokeniser
held to the compiler's own classifier by a parity test over every fixture.
@guml/core is the one that does not run in Node; its wasm is built for the web target and loads
itself with fetch. Use the CLI to compile from a shell.
The unscoped name
gumlis not this project. npm's similarity check refuses it againstgulp,gm,xml,tomlandyaml.
cargo test --workspace
cargo run -q -p guml-cli -- build fixtures/b.guml # React + TS
cargo run -q -p guml-cli -- build fixtures/b.guml --backend svelte
cargo run -q -p guml-cli -- build fixtures/b.guml --backend wc # no framework, no build step
cargo run -q -p guml-cli -- build fixtures/b.guml --backend a2ui # the agent-UI wire format
cargo run -q -p guml-cli -- check fixtures/invoices.guml --format json
cargo run -q -p guml-cli -- capabilities fixtures # what each document will do
cargo run -q -p guml-cli -- explain GUML0065
cargo run -q -p guml-cli -- registry
# What a model actually returns, repaired with no model call:
printf 'Here you go:\n\n```guml\npage P\ndiv\n span Hi\n```\n' |
cargo run -q -p guml-cli -- repair
node bench/guml-bench/edit-locality.mjs # the cost of a *change*, not a first draftThe distinction is kept sharp everywhere in this repo, because the project is a study before it is a language.
Hand-authored fixtures, cl100k_base, GUML-Research-Report.md §1.5:
| Fixture | React + TS + Tailwind | GUML | Reduction |
|---|---|---|---|
| Counter card | 368 | 64 | 82.6% |
| Task CRUD | 1,441 | 178 | 87.6% |
| Landing page | 1,648 | 376 | 77.2% |
Also measured: GUML is 45% smaller than a minified JSON UI IR for the same application. And 232 of the landing page's 376 tokens are the prose itself — so compression is bounded by content, not by the language. Structure-heavy artifacts approach 8×; content-heavy ones asymptote at 2–3×.
The caveat travels with every number above. Both sides of those comparisons were written by the same person, and they are authored artifacts rather than model generations.
Whether a model can produce correct GUML, and whether correctness improves or degrades against a React baseline. Those are hypotheses H1–H6 in the report, and they are labelled as hypotheses throughout — never as findings.
Kept in the README on purpose. GUML has zero training data by construction, and the low-resource-DSL literature consistently reports degradation on unfamiliar languages. Against that, Anka (arXiv:2512.23214) reports a constrained DSL beating Python by +40pp on multi-step tasks — but that is one paper in one narrow domain. Reconciling the two findings is the research contribution. Phase 0 is the two-week experiment that decides which side GUML lands on. Full analysis: report §12.
| Path | Contents |
|---|---|
crates/ |
The Rust compiler: syntax, AST, parser, registry, codegen, driver, CLI, LSP, wasm, formatter |
spec/ |
The language spec, the conformance suite (the normative definition), stability and registry contracts |
fixtures/ |
Paired GUML / React / JSON-IR artifacts — the source of every token figure |
packages/ |
The five npm packages — compiler, formatter, highlighter, and two component vocabularies |
docs/ |
The documentation site (Next.js). Code samples are generated from fixtures/ and from the compiler, never retyped. |
bench/phase0/ |
The kill-or-continue experiment: ten tasks, paired references, prompt assembly, blind rubric |
bench/gen/ |
Generation test: applications through a live model, scored on parse, validation and requirements |
editors/ |
VS Code extension (LSP client + generated TextMate grammar) and a tree-sitter grammar |
GUML-Research-Report.md |
Feasibility study: landscape, literature, novelty, benchmark design, critical review |
ROADMAP.md |
Phased build plan with gates |
Not "a new Markdown" — MDX, Markdoc, A2UI and Vega-Lite already occupy that framing. GUML is an intermediate representation plus a compiler, and the contribution is the measurement: where the token/accuracy frontier sits for LLM-generated UI, and when a purpose-built DSL beats a high-resource general-purpose language. Google's A2UI is a compile target, not a rival.
The mark is the compiler's job description. Output arrives amorphous — the blob, which has no edge you could measure — and leaves as a laid-out interface: rounded bars on a grid, the same shape every time. The necks reaching rightward are the compiler, and one bar stays accent-coloured because that is where the transition happens.
| Asset | Use |
|---|---|
assets/logo-mark.svg |
Canonical. The bars are currentColor, so the mark follows the surrounding text colour. |
assets/logo-light.svg · logo-dark.svg |
Generated from the mark with currentColor pinned, for renderers that strip inheritance. |
Signal Orange #f73b20 is the only accent. Ink Roast #360802 is body text — a warm near-black, so
copy and accent read as one temperature.
MIT