{"id":6110,"date":"2026-10-16T02:35:47","date_gmt":"2026-10-16T05:35:47","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/10\/16\/guml-let-the-model-write-24-lines-and-the-compiler-write-the-other-229\/"},"modified":"2026-10-16T02:35:47","modified_gmt":"2026-10-16T05:35:47","slug":"guml-let-the-model-write-24-lines-and-the-compiler-write-the-other-229","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/10\/16\/guml-let-the-model-write-24-lines-and-the-compiler-write-the-other-229\/","title":{"rendered":"GUML: Let the Model Write 24 Lines and the Compiler Write the Other 229"},"content":{"rendered":"<div>\n<div><\/div>\n<p>Read it once and you know the whole app: a typed resource with three mutations (two of them optimistic), a text draft, a three-way filter, a live count in the heading, a form, tabs, and a list with an empty state.<\/p>\n<p>Here it is compiled in the browser. This is the docs playground running the Rust compiler as WebAssembly:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwkmj4r3y0y1ibcjcbuk.png\"><\/a><\/p>\n<p>Notice the small line under the input: <code>tabs \u2014 not lowered by this compiler version<\/code>. The in-browser preview renderer doesn&#8217;t draw <code>tabs<\/code> yet, and it says so instead of drawing something approximate. That&#8217;s one of the project&#8217;s four invariants, and it&#8217;ll come up again.<\/p>\n<h2> <a name=\"a-tour-of-the-language\" href=\"#a-tour-of-the-language\"> <\/a> A tour of the language <\/h2>\n<p>The whole syntax fits in a paragraph, which is the point: the full spec plus the vocabulary costs a model about <strong>2,935 estimated tokens<\/strong> of prompt, and the spec file states a budget of 3,000.<\/p>\n<p><strong>Lines are structure.<\/strong> Indentation is nesting, 2 spaces per level. No closing tags, no braces for blocks, no imports. A tab is an error (<code>GUML0001<\/code>).<\/p>\n<p><strong>Directives<\/strong> declare the data model: <\/p>\n<div>\n<pre><code>page Counter title=\"Clicks\" \/\/ name -&gt; component type Task {id, title, done:bool} \/\/ fields default to string state count=0 \/\/ type inferred from the initial value state filter=all|open|done \/\/ an enumerated domain; first value is initial data tasks:Task[] GET \/api\/tasks \/\/ a resource add POST \/api\/tasks {title} optimistic:prepend on {filter} &gt;tasks.list \/\/ an effect: re-runs when filter changes <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>Elements<\/strong> are <code>tag [positionals] [name=value] [&gt;action]<\/code>. Positionals can be a label, a modifier, a <code>{binding}<\/code>, a <code>\/route<\/code> or an <code>#anchor<\/code>. The action comes last because <code>&gt;<\/code> swallows the rest of the line.<\/p>\n<p><strong>Bindings<\/strong> are read-only expressions: paths, comparison, boolean logic, arithmetic, and a fixed set of aggregates (<code>.count .sum .open .done .trim .lower .upper<\/code>). <code>{tasks.open.count}<\/code> is &#8220;how many rows have the bool field unset&#8221;. It works on invoices too, where the bool is <code>paid<\/code>, because the compiler reads the flag from the row type rather than from a name it hoped for.<\/p>\n<p><strong>Actions<\/strong> are statements separated by <code>;<\/code>: <code>&gt;count++<\/code>, <code>&gt;count=0<\/code>, <code>&gt;tasks.add{title:draft}; draft=\"\"<\/code>.<\/p>\n<p><strong>The vocabulary is closed.<\/strong> There are 49 primitives in a registry (<code>card row col section nav hero form tabs list table btn input select metric modal toast<\/code>\u2026), 24 modifiers (<code>primary ghost quiet danger sm lg center between<\/code>\u2026), and 12 global attributes. There is <strong>no <code>class<\/code> attribute<\/strong>: presentation belongs to the theme.<\/p>\n<p><strong><code>def<\/code><\/strong> declares a user component. It&#8217;s a compile-time macro with parameters and one <code>slot<\/code>, and by the time codegen runs there&#8217;s no trace of it.<\/p>\n<p><strong>Two conformance levels.<\/strong> <code>core<\/code> is markup only: no I\/O, no state, meant to be safe to render from an untrusted agent. <code>app<\/code> adds <code>state<\/code>, <code>data<\/code>, actions and the repeaters. An app construct at the core level is <code>GUML0091<\/code>.<\/p>\n<p><strong>Escape hatches.<\/strong> <code>js<\/code> and <code>raw<\/code> blocks pass through verbatim and unchecked. Each one emits a <code>GUML0090<\/code> note, so the escape-hatch <em>rate<\/em> stays countable. That rate is one of the things the research measures.<\/p>\n<h2> <a name=\"inside-the-compiler\" href=\"#inside-the-compiler\"> <\/a> Inside the compiler <\/h2>\n<p>The compiler is a Rust workspace of 14 crates, about 33,000 lines. The heaviest are <code>guml-compiler<\/code> (about 10.7k lines, the analysis passes) and <code>guml-codegen<\/code> (about 9.1k lines, the backends).<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01xdhitd4xrxfffmj2l9.png\"><\/a><\/p>\n<p>The driver is short enough to quote. From <code>crates\/guml-compiler\/src\/lib.rs<\/code>: <\/p>\n<div>\n<pre><code><span>let<\/span> <span>parsed<\/span> <span>=<\/span> <span>guml_parser<\/span><span>::<\/span><span>parse<\/span><span>(<\/span><span>src<\/span><span>,<\/span> <span>reg<\/span><span>);<\/span> <span>\/\/ ...<\/span> <span>expand<\/span><span>::<\/span><span>expand<\/span><span>(<\/span><span>&amp;<\/span><span>mut<\/span> <span>program<\/span><span>,<\/span> <span>&amp;<\/span><span>mut<\/span> <span>diagnostics<\/span><span>);<\/span> <span>\/\/ def macros<\/span> <span>sema<\/span><span>::<\/span><span>analyse<\/span><span>(<\/span><span>&amp;<\/span><span>program<\/span><span>,<\/span> <span>reg<\/span><span>,<\/span> <span>&amp;<\/span><span>mut<\/span> <span>diagnostics<\/span><span>);<\/span> <span>\/\/ scope + accessibility<\/span> <span>validate<\/span><span>::<\/span><span>validate<\/span><span>(<\/span><span>&amp;<\/span><span>program<\/span><span>,<\/span> <span>reg<\/span><span>,<\/span> <span>&amp;<\/span><span>mut<\/span> <span>diagnostics<\/span><span>);<\/span> <span>\/\/ structure, methods, unused<\/span> <span>types<\/span><span>::<\/span><span>check<\/span><span>(<\/span><span>&amp;<\/span><span>program<\/span><span>,<\/span> <span>&amp;<\/span><span>mut<\/span> <span>diagnostics<\/span><span>);<\/span> <span>\/\/ expression types, aggregates<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqwvzq6586q4lgmbsosx.webp\"><\/a><\/p>\n<p><em>Diagram from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<p>All the analysis passes always run, even after an error, so diagnostics accumulate. Codegen runs only on an error-free program. There&#8217;s no separate IR: the AST, <code>guml_ast::Program<\/code>, is what every backend consumes, and each backend lowers expressions through one shared function.<\/p>\n<p>The type checker is the newest stage and, according to the docs, the one that paid for itself most directly. It found a live bug in a published example where <code>{invoices.open.count}<\/code> had been compiling to a filter on a field the type didn&#8217;t declare: always zero, and no test noticed.<\/p>\n<h2> <a name=\"a-lexer-that-cannot-decide-alone\" href=\"#a-lexer-that-cannot-decide-alone\"> <\/a> A lexer that cannot decide alone <\/h2>\n<p>GUML has an ambiguity no lexer can resolve on its own. The comment at the top of <code>crates\/guml-syntax\/src\/lib.rs<\/code> puts it in two lines: <\/p>\n<div>\n<pre><code>btn Decrement ghost &gt;count-- \/\/ Decrement is a label, ghost is a modifier p Press the buttons to change. \/\/ the whole remainder is prose content <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Same shape, opposite meanings. Whether the rest of a line is <strong>structure<\/strong> or <strong>prose<\/strong> depends on the tag, and the tag&#8217;s kind lives in the registry. So the lexer produces <em>both readings<\/em> of every line: a token list and the raw text. Then the parser, which knows the registry, picks one.<\/p>\n<p>The rule is subtle: a text tag stays prose <em>unless<\/em> the line contains an <code>=<\/code> whose name the registry accepts for that tag. So <code>p Set x=1 to enable<\/code> is prose, and <code>text {title} strike={done}<\/code> is structure. Step through it:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frwrzuxb2a7ro9u4qm2pt.webp\"><\/a><\/p>\n<p><em>A frame from the interactive sketch in <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a> \u2014 <a href=\"https:\/\/www.ravikishan.me\/api\/media\/devto\/p5-fam1dkxgfgc4-motion.webm\" target=\"_blank\" rel=\"noopener noreferrer\">watch it run<\/a>.<\/em><\/p>\n<p>Two more lexer decisions I like:<\/p>\n<ul>\n<li> <strong>Actions terminate the line by construction.<\/strong> Everything after <code>&gt;<\/code> is one token, so actions are lexable in a single pass with no lookahead.<\/li>\n<li> <strong>URLs are an allow-list.<\/strong> A route is <code>\/path<\/code> at token start, so <code>$24\/mo<\/code> stays one word, and absolute URLs are limited to <code>http:\/\/<\/code> and <code>https:\/\/<\/code>. As the comment says, a general &#8220;scheme followed by <code>:\/\/<\/code>&#8221; rule would make <code>javascript:<\/code> and <code>data:<\/code> lexable as request targets. <em>The allow-list is the security boundary.<\/em> <\/li>\n<\/ul>\n<h2> <a name=\"every-error-in-one-pass\" href=\"#every-error-in-one-pass\"> <\/a> Every error in one pass <\/h2>\n<p>This is the invariant I&#8217;d defend hardest, and the project&#8217;s own <code>CLAUDE.md<\/code> says it plainly: <em>&#8220;Each repair-loop round is a full LLM generation; single-error reporting is a product defect, not a style preference.&#8221;<\/em><\/p>\n<p>A compiler that stops at the first error is fine for a human, who fixes one thing and presses save. For a model it&#8217;s a disaster: each round trip is a full generation, billed at output rates. So the parser recovers from everything:<\/p>\n<ul>\n<li>An unknown tag is reported, with a suggestion, and then <strong>parsed as a container anyway<\/strong>, so errors in its children still surface.<\/li>\n<li>A missing attribute value is reported and parsing continues.<\/li>\n<li>Bad indentation is reported <strong>once per run, not once per line<\/strong>. Before that fix, one mis-indented <code>section<\/code> produced 15 stray <code>GUML0011<\/code>s, and a repair loop handed sixteen errors will edit sixteen lines, fifteen of which were correct.<\/li>\n<\/ul>\n<p>Here&#8217;s the playground&#8217;s deliberately broken sample, three mistakes from three different passes:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxjbs1pyus89vwcs7pl4.png\"><\/a><\/p>\n<p>And the same file from the CLI, exactly as <code>guml check<\/code> printed it on my machine: <\/p>\n<div>\n<pre><code>error[GUML0030]: unknown tag `crad` --&gt; broken.guml:4:1 | 4 | crad sm center | ^^^^ = help: did you mean `card`? = suggestion: card error[GUML0033]: binding refers to `kount`, which is not declared --&gt; broken.guml:6:3 | 6 | metric {kount} | ^^^^^^^^^^^^^^ = help: did you mean `count`? = suggestion: count error[GUML0050]: `btn` has no accessible name --&gt; broken.guml:7:3 | 7 | btn quiet &gt;count++ | ^^^^^^^^^^^^^^^^^^ = help: give the control a text label, or add `aria=\"\u2026\"` describing what it does = suggestion: btn aria=\"\u2026\" <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>(Look closely at the carets under <code>metric {kount}<\/code>. They&#8217;ll matter later.)<\/p>\n<p>Notice the third error. <strong>A button with no accessible name is a compile error, not a lint warning.<\/strong> The <code>explain<\/code> text says why: <em>&#8220;an inaccessible interface is a broken interface, and the compiler is the last place able to notice.&#8221;<\/em><\/p>\n<h3> <a name=\"diagnostic-codes-are-an-api\" href=\"#diagnostic-codes-are-an-api\"> <\/a> Diagnostic codes are an API <\/h3>\n<p>There are <strong>49 codes<\/strong>, <code>GUML0001<\/code> to <code>GUML0104<\/code>, grouped in decades: lexical, layout, syntax, resolution, semantics, accessibility, types, structure, domains, escape hatches. They&#8217;re <strong>append-only<\/strong>: a test enforces it, and the Rust enum is <code>#[non_exhaustive]<\/code>. The reason is that a repair loop keys on them, so <code>GUML0033<\/code> has to mean the same thing forever. <code>0060<\/code> is deliberately unallocated, because <em>&#8220;shipping a code with no emit site is how a diagnostic surface starts lying about what it can detect.&#8221;<\/em><\/p>\n<p>Every diagnostic is machine-actionable JSON: <\/p>\n<div>\n<pre><code><span>{<\/span><span>\"code\"<\/span><span>:<\/span><span>\"unknown_tag\"<\/span><span>,<\/span><span>\"id\"<\/span><span>:<\/span><span>\"GUML0030\"<\/span><span>,<\/span><span>\"severity\"<\/span><span>:<\/span><span>\"error\"<\/span><span>,<\/span><span>\"message\"<\/span><span>:<\/span><span>\"unknown tag `crad`\"<\/span><span>,<\/span><span> <\/span><span>\"span\"<\/span><span>:{<\/span><span>\"start\"<\/span><span>:<\/span><span>27<\/span><span>,<\/span><span>\"end\"<\/span><span>:<\/span><span>31<\/span><span>,<\/span><span>\"line\"<\/span><span>:<\/span><span>4<\/span><span>,<\/span><span>\"col\"<\/span><span>:<\/span><span>1<\/span><span>},<\/span><span>\"help\"<\/span><span>:<\/span><span>\"did you mean `card`?\"<\/span><span>,<\/span><span>\"suggestion\"<\/span><span>:<\/span><span>\"card\"<\/span><span>}<\/span><span> <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><code>suggestion<\/code> is present <strong>only when the fix is unambiguous<\/strong>, which is what lets a machine apply it without asking anyone.<\/p>\n<h2> <a name=\"did-you-mean-card\" href=\"#did-you-mean-card\"> <\/a> Did you mean card? <\/h2>\n<p>How does the compiler know <code>crad<\/code> means <code>card<\/code>? The obvious answer is edit distance, and the obvious answer is wrong. Plain Levenshtein scores <code>crad \u2192 card<\/code> as <strong>2<\/strong> (two substitutions), which is too far for a four-letter word. Typos are very often <em>swapped neighbours<\/em>, so GUML uses <strong>Optimal String Alignment<\/strong>, which adds one more case to the recurrence:<\/p>\n<p>$$<br \/> d(i,j)=\\min\\begin{cases}<br \/> d(i-1,j)+1 &amp; \\text{delete}\\<br \/> d(i,j-1)+1 &amp; \\text{insert}\\<br \/> d(i-1,j-1)+[a_i \\neq b_j] &amp; \\text{substitute}\\<br \/> d(i-2,j-2)+1 &amp; \\text{if } a_i=b_{j-1} \\text{ and } a_{i-1}=b_j<br \/> \\end{cases}<br \/> $$<\/p>\n<p>Watch the two tables fill, and see where the transposition (highlighted) saves an edit:<\/p>\n<blockquote>\n<p>A frame from the interactive sketch in <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a> \u2014 it does not render here.<\/p>\n<\/blockquote>\n<p>The threshold is at most 1 edit for names of four characters or fewer, and at most 2 above that. But edit distance can&#8217;t help with the most common mistake a model makes, which is writing HTML out of habit: <code>button \u2192 btn<\/code> is three edits. So before any distance is computed, the registry checks a table of <strong>39 HTML habits<\/strong> (<code>div\u2192col<\/code>, <code>span\u2192text<\/code>, <code>button\u2192btn<\/code>, <code>hr\u2192divider<\/code>, <code>ul\u2192menu<\/code>, <code>dialog\u2192modal<\/code>\u2026), and each entry carries a note explaining <em>why<\/em> GUML doesn&#8217;t have that element.<\/p>\n<p>Modifiers are stricter still: exactly one edit, on a word of at least four characters. That rule exists because of a real data-loss bug, where <code>btn Click me<\/code> was &#8220;fixed&#8221; to <code>btn Click md<\/code>.<\/p>\n<h2> <a name=\"repair-without-a-model\" href=\"#repair-without-a-model\"> <\/a> Repair without a model <\/h2>\n<p>Models don&#8217;t return clean files. They return <code>Here you go:<\/code>, a code fence, the code, another fence, and <code>Let me know if you want changes!<\/code>. <code>guml repair<\/code> strips that packaging and applies every unambiguous fix, <strong>with no model call, ever<\/strong>:<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>Layer<\/th>\n<th>Fixes<\/th>\n<th>Cost<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>sanitize<\/code><\/td>\n<td>code fences, markdown rules, trailing commentary<\/td>\n<td>free<\/td>\n<\/tr>\n<tr>\n<td><code>format<\/code><\/td>\n<td>indentation, tabs, spacing<\/td>\n<td>free<\/td>\n<\/tr>\n<tr>\n<td><code>fix<\/code><\/td>\n<td>every unambiguous diagnostic suggestion, up to 3 rounds<\/td>\n<td>free<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8043r1jviwz7a90jlnga.webp\"><\/a><\/p>\n<p><em>Diagram from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<p>Two safety rules make this trustworthy. First, <strong>a layer that would increase the error count is discarded rather than kept<\/strong>: &#8220;deterministic&#8221; is not the same as &#8220;always an improvement&#8221;. Second, a suggestion containing <code>\u2026<\/code> is a <em>template<\/em> for a human (<code>btn aria=\"\u2026\"<\/code>), not a replacement, so it&#8217;s shown and never applied. Splicing it in would put a literal ellipsis in the accessible name, which is worse than the original error.<\/p>\n<p>I fed it the broken sample wrapped in a fence and some chat filler. The JSON report: <\/p>\n<div>\n<pre><code><span>{<\/span><span>\"applied\"<\/span><span>:[<\/span><span>\"GUML0030\"<\/span><span>],<\/span><span>\"changed\"<\/span><span>:<\/span><span>true<\/span><span>,<\/span><span>\"errorsBefore\"<\/span><span>:<\/span><span>7<\/span><span>,<\/span><span>\"errorsAfter\"<\/span><span>:<\/span><span>2<\/span><span>,<\/span><span>\"rounds\"<\/span><span>:<\/span><span>2<\/span><span>,<\/span><span> <\/span><span>\"sanitize\"<\/span><span>:{<\/span><span>\"fence\"<\/span><span>:<\/span><span>true<\/span><span>,<\/span><span>\"rules\"<\/span><span>:<\/span><span>0<\/span><span>,<\/span><span>\"trailing\"<\/span><span>:<\/span><span>0<\/span><span>}}<\/span><span> <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Seven errors down to two with zero tokens spent. The fence and the commentary are gone and <code>crad<\/code> became <code>card<\/code>. What&#8217;s left are the two that genuinely need a decision: is <code>kount<\/code> a typo or a missing <code>state<\/code>, and what should this button <em>say<\/em>?<\/p>\n<h2> <a name=\"what-24-lines-become\" href=\"#what-24-lines-become\"> <\/a> What 24 lines become <\/h2>\n<p>Here&#8217;s the part the model never writes. These excerpts are real output from <code>guml build fixtures\/b.guml<\/code>, 253 lines of React and TypeScript in total.<\/p>\n<p><strong>A retry helper that knows what&#8217;s safe to retry.<\/strong> It uses exponential backoff (300 ms, then 600 ms), only for idempotent methods, only on a network failure or a 5xx, and never after an abort: <\/p>\n<div>\n<pre><code><span>async<\/span> <span>function<\/span> <span>retrying<\/span><span>(<\/span><span>url<\/span><span>:<\/span> <span>string<\/span><span>,<\/span> <span>init<\/span><span>:<\/span> <span>RequestInit<\/span> <span>=<\/span> <span>{},<\/span> <span>tries<\/span> <span>=<\/span> <span>3<\/span><span>):<\/span> <span>Promise<\/span><span>&lt;<\/span><span>Response<\/span><span>&gt;<\/span> <span>{<\/span> <span>const<\/span> <span>idempotent<\/span> <span>=<\/span> <span>!<\/span><span>init<\/span><span>.<\/span><span>method<\/span> <span>||<\/span> <span>[<\/span><span>\"<\/span><span>GET<\/span><span>\"<\/span><span>,<\/span> <span>\"<\/span><span>HEAD<\/span><span>\"<\/span><span>,<\/span> <span>\"<\/span><span>PUT<\/span><span>\"<\/span><span>,<\/span> <span>\"<\/span><span>DELETE<\/span><span>\"<\/span><span>].<\/span><span>includes<\/span><span>(<\/span><span>init<\/span><span>.<\/span><span>method<\/span><span>);<\/span> <span>let<\/span> <span>wait<\/span> <span>=<\/span> <span>300<\/span><span>;<\/span> <span>for <\/span><span>(<\/span><span>let<\/span> <span>attempt<\/span> <span>=<\/span> <span>1<\/span><span>;<\/span> <span>;<\/span> <span>attempt<\/span><span>++<\/span><span>)<\/span> <span>{<\/span> <span>const<\/span> <span>last<\/span> <span>=<\/span> <span>attempt<\/span> <span>&gt;=<\/span> <span>tries<\/span> <span>||<\/span> <span>!<\/span><span>idempotent<\/span><span>;<\/span> <span>try<\/span> <span>{<\/span> <span>const<\/span> <span>res<\/span> <span>=<\/span> <span>await<\/span> <span>fetch<\/span><span>(<\/span><span>url<\/span><span>,<\/span> <span>init<\/span><span>);<\/span> <span>if <\/span><span>(<\/span><span>res<\/span><span>.<\/span><span>ok<\/span> <span>||<\/span> <span>res<\/span><span>.<\/span><span>status<\/span> <span>&lt;<\/span> <span>500<\/span> <span>||<\/span> <span>last<\/span><span>)<\/span> <span>return<\/span> <span>res<\/span><span>;<\/span> <span>}<\/span> <span>catch <\/span><span>(<\/span><span>err<\/span><span>)<\/span> <span>{<\/span> <span>if <\/span><span>(<\/span><span>last<\/span> <span>||<\/span> <span>(<\/span><span>err<\/span> <span>instanceof<\/span> <span>Error<\/span> <span>&amp;&amp;<\/span> <span>err<\/span><span>.<\/span><span>name<\/span> <span>===<\/span> <span>\"<\/span><span>AbortError<\/span><span>\"<\/span><span>))<\/span> <span>throw<\/span> <span>err<\/span><span>;<\/span> <span>}<\/span> <span>await<\/span> <span>new<\/span> <span>Promise<\/span><span>((<\/span><span>resolve<\/span><span>)<\/span> <span>=&gt;<\/span> <span>setTimeout<\/span><span>(<\/span><span>resolve<\/span><span>,<\/span> <span>wait<\/span><span>));<\/span> <span>wait<\/span> <span>*=<\/span> <span>2<\/span><span>;<\/span> <span>}<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>A cache with stale-while-revalidate and in-flight deduplication.<\/strong> It caches GETs only, treats entries as stale after 30 seconds (a constant, deliberately &#8220;not a knob&#8221;), serves the last good data if a refresh fails, and lets mutations invalidate by URL prefix, so <code>PATCH \/api\/tasks\/{id}<\/code> invalidates <code>\/api\/tasks<\/code>: <\/p>\n<div>\n<pre><code><span>async<\/span> <span>function<\/span> <span>cached<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span><span>(<\/span><span>url<\/span><span>:<\/span> <span>string<\/span><span>,<\/span> <span>init<\/span><span>:<\/span> <span>RequestInit<\/span> <span>=<\/span> <span>{}):<\/span> <span>Promise<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span> <span>{<\/span> <span>const<\/span> <span>method<\/span> <span>=<\/span> <span>init<\/span><span>.<\/span><span>method<\/span> <span>??<\/span> <span>\"<\/span><span>GET<\/span><span>\"<\/span><span>;<\/span> <span>const<\/span> <span>key<\/span> <span>=<\/span> <span>`<\/span><span>${<\/span><span>method<\/span><span>}<\/span><span> <\/span><span>${<\/span><span>url<\/span><span>}<\/span><span>`<\/span><span>;<\/span> <span>if <\/span><span>(<\/span><span>method<\/span> <span>!==<\/span> <span>\"<\/span><span>GET<\/span><span>\"<\/span><span>)<\/span> <span>return<\/span> <span>read<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span><span>(<\/span><span>url<\/span><span>,<\/span> <span>init<\/span><span>);<\/span> <span>const<\/span> <span>hit<\/span> <span>=<\/span> <span>GUML_CACHE<\/span><span>.<\/span><span>get<\/span><span>(<\/span><span>key<\/span><span>);<\/span> <span>const<\/span> <span>fresh<\/span> <span>=<\/span> <span>hit<\/span> <span>&amp;&amp;<\/span> <span>Date<\/span><span>.<\/span><span>now<\/span><span>()<\/span> <span>-<\/span> <span>hit<\/span><span>.<\/span><span>at<\/span> <span>&lt;<\/span> <span>GUML_STALE_MS<\/span><span>;<\/span> <span>if <\/span><span>(<\/span><span>hit<\/span> <span>&amp;&amp;<\/span> <span>!<\/span><span>fresh<\/span><span>)<\/span> <span>{<\/span> <span>void<\/span> <span>refresh<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span><span>(<\/span><span>key<\/span><span>,<\/span> <span>url<\/span><span>,<\/span> <span>init<\/span><span>).<\/span><span>catch<\/span><span>(()<\/span> <span>=&gt;<\/span> <span>{});<\/span> <span>}<\/span> <span>if <\/span><span>(<\/span><span>hit<\/span><span>)<\/span> <span>return<\/span> <span>hit<\/span><span>.<\/span><span>data<\/span> <span>as<\/span> <span>T<\/span><span>;<\/span> <span>const<\/span> <span>pending<\/span> <span>=<\/span> <span>GUML_INFLIGHT<\/span><span>.<\/span><span>get<\/span><span>(<\/span><span>key<\/span><span>);<\/span> <span>if <\/span><span>(<\/span><span>pending<\/span><span>)<\/span> <span>return<\/span> <span>pending<\/span> <span>as<\/span> <span>Promise<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span><span>;<\/span> <span>return<\/span> <span>refresh<\/span><span>&lt;<\/span><span>T<\/span><span>&gt;<\/span><span>(<\/span><span>key<\/span><span>,<\/span> <span>url<\/span><span>,<\/span> <span>init<\/span><span>);<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>A list fetch that can be cancelled<\/strong>, with an <code>alive<\/code> guard so a late response never writes into an unmounted component: <\/p>\n<div>\n<pre><code><span>const<\/span> <span>tasksList<\/span> <span>=<\/span> <span>useCallback<\/span><span>(()<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>const<\/span> <span>controller<\/span> <span>=<\/span> <span>new<\/span> <span>AbortController<\/span><span>();<\/span> <span>let<\/span> <span>alive<\/span> <span>=<\/span> <span>true<\/span><span>;<\/span> <span>setTasksLoading<\/span><span>(<\/span><span>true<\/span><span>);<\/span> <span>setTasksError<\/span><span>(<\/span><span>null<\/span><span>);<\/span> <span>cached<\/span><span>&lt;<\/span><span>Task<\/span><span>[]<\/span><span>&gt;<\/span><span>(<\/span><span>\"<\/span><span>\/api\/tasks<\/span><span>\"<\/span><span>,<\/span> <span>{<\/span> <span>signal<\/span><span>:<\/span> <span>controller<\/span><span>.<\/span><span>signal<\/span> <span>})<\/span> <span>.<\/span><span>then<\/span><span>((<\/span><span>rows<\/span><span>)<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>if <\/span><span>(<\/span><span>alive<\/span><span>)<\/span> <span>setTasks<\/span><span>(<\/span><span>rows<\/span><span>);<\/span> <span>})<\/span> <span>.<\/span><span>catch<\/span><span>((<\/span><span>err<\/span><span>:<\/span> <span>unknown<\/span><span>)<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>if <\/span><span>(<\/span><span>!<\/span><span>alive<\/span> <span>||<\/span> <span>(<\/span><span>err<\/span> <span>instanceof<\/span> <span>Error<\/span> <span>&amp;&amp;<\/span> <span>err<\/span><span>.<\/span><span>name<\/span> <span>===<\/span> <span>\"<\/span><span>AbortError<\/span><span>\"<\/span><span>))<\/span> <span>return<\/span><span>;<\/span> <span>setTasksError<\/span><span>(<\/span><span>err<\/span> <span>instanceof<\/span> <span>Error<\/span> <span>?<\/span> <span>err<\/span><span>.<\/span><span>message<\/span> <span>:<\/span> <span>\"<\/span><span>Unknown error<\/span><span>\"<\/span><span>);<\/span> <span>})<\/span> <span>.<\/span><span>finally<\/span><span>(()<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>if <\/span><span>(<\/span><span>alive<\/span><span>)<\/span> <span>setTasksLoading<\/span><span>(<\/span><span>false<\/span><span>);<\/span> <span>});<\/span> <span>return <\/span><span>()<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>alive<\/span> <span>=<\/span> <span>false<\/span><span>;<\/span> <span>controller<\/span><span>.<\/span><span>abort<\/span><span>();<\/span> <span>};<\/span> <span>},<\/span> <span>[]);<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>And the optimistic update with rollback<\/strong>, all from the word <code>optimistic:prepend<\/code>: <\/p>\n<div>\n<pre><code><span>const<\/span> <span>tasksAdd<\/span> <span>=<\/span> <span>useCallback<\/span><span>(<\/span> <span>async <\/span><span>(<\/span><span>body<\/span><span>:<\/span> <span>Partial<\/span><span>&lt;<\/span><span>Task<\/span><span>&gt;<\/span><span>)<\/span> <span>=&gt;<\/span> <span>{<\/span> <span>const<\/span> <span>snapshot<\/span> <span>=<\/span> <span>tasks<\/span><span>;<\/span> <span>setTasksSaving<\/span><span>(<\/span><span>true<\/span><span>);<\/span> <span>const<\/span> <span>optimistic<\/span> <span>=<\/span> <span>{<\/span> <span>id<\/span><span>:<\/span> <span>`tmp-<\/span><span>${<\/span><span>Date<\/span><span>.<\/span><span>now<\/span><span>()}<\/span><span>`<\/span><span>,<\/span> <span>...<\/span><span>body<\/span> <span>}<\/span> <span>as<\/span> <span>Task<\/span><span>;<\/span> <span>setTasks<\/span><span>((<\/span><span>prev<\/span><span>)<\/span> <span>=&gt;<\/span> <span>[<\/span><span>optimistic<\/span><span>,<\/span> <span>...<\/span><span>prev<\/span><span>]);<\/span> <span>try<\/span> <span>{<\/span> <span>const<\/span> <span>res<\/span> <span>=<\/span> <span>await<\/span> <span>retrying<\/span><span>(<\/span><span>\"<\/span><span>\/api\/tasks<\/span><span>\"<\/span><span>,<\/span> <span>{<\/span> <span>method<\/span><span>:<\/span> <span>\"<\/span><span>POST<\/span><span>\"<\/span><span>,<\/span> <span>headers<\/span><span>:<\/span> <span>{<\/span> <span>\"<\/span><span>Content-Type<\/span><span>\"<\/span><span>:<\/span> <span>\"<\/span><span>application\/json<\/span><span>\"<\/span> <span>},<\/span> <span>body<\/span><span>:<\/span> <span>JSON<\/span><span>.<\/span><span>stringify<\/span><span>(<\/span><span>body<\/span><span>),<\/span> <span>});<\/span> <span>if <\/span><span>(<\/span><span>!<\/span><span>res<\/span><span>.<\/span><span>ok<\/span><span>)<\/span> <span>throw<\/span> <span>new<\/span> <span>Error<\/span><span>(<\/span><span>`Request failed: <\/span><span>${<\/span><span>res<\/span><span>.<\/span><span>status<\/span><span>}<\/span><span>`<\/span><span>);<\/span> <span>invalidate<\/span><span>(<\/span><span>\"<\/span><span>\/api\/tasks<\/span><span>\"<\/span><span>);<\/span> <span>const<\/span> <span>created<\/span> <span>=<\/span> <span>(<\/span><span>await<\/span> <span>res<\/span><span>.<\/span><span>json<\/span><span>())<\/span> <span>as<\/span> <span>Task<\/span><span>;<\/span> <span>setTasks<\/span><span>((<\/span><span>prev<\/span><span>)<\/span> <span>=&gt;<\/span> <span>prev<\/span><span>.<\/span><span>map<\/span><span>((<\/span><span>it<\/span><span>)<\/span> <span>=&gt;<\/span> <span>(<\/span><span>it<\/span> <span>===<\/span> <span>optimistic<\/span> <span>?<\/span> <span>created<\/span> <span>:<\/span> <span>it<\/span><span>)));<\/span> <span>}<\/span> <span>catch <\/span><span>(<\/span><span>err<\/span><span>:<\/span> <span>unknown<\/span><span>)<\/span> <span>{<\/span> <span>setTasks<\/span><span>(<\/span><span>snapshot<\/span><span>);<\/span> <span>setTasksError<\/span><span>(<\/span><span>err<\/span> <span>instanceof<\/span> <span>Error<\/span> <span>?<\/span> <span>err<\/span><span>.<\/span><span>message<\/span> <span>:<\/span> <span>\"<\/span><span>Could not save<\/span><span>\"<\/span><span>);<\/span> <span>}<\/span> <span>finally<\/span> <span>{<\/span> <span>setTasksSaving<\/span><span>(<\/span><span>false<\/span><span>);<\/span> <span>}<\/span> <span>},<\/span> <span>[<\/span><span>tasks<\/span><span>],<\/span> <span>);<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3as46rlsl4ix1u98me1t.webp\"><\/a><\/p>\n<p><em>Diagram from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<p>Here&#8217;s that state machine running. Rows appear instantly with a <code>tmp-<\/code> id. When the server answers, they either become real or vanish. Click to change the failure rate:<\/p>\n<blockquote>\n<p>A frame from the interactive sketch in <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a> \u2014 it does not render here.<\/p>\n<\/blockquote>\n<p>Leave it at 70% for a while and you&#8217;ll spot a subtle thing. When two adds overlap and the <em>first<\/em> fails, the rollback restores the first one&#8217;s snapshot, so the second row disappears too, even if it later succeeds. The generated React has the same property, because <code>snapshot<\/code> is captured per call. It&#8217;s on my list.<\/p>\n<p>Two smaller niceties: the <code>tasksSaving<\/code> flag only exists if some <code>busy=<\/code> reads it, and it clears in <code>finally<\/code>, <em>&#8220;or one failed mutation leaves the button saying &#8216;Adding\u2026&#8217; forever&#8221;<\/em>. And an error boundary is only emitted when the document uses an escape hatch, since generated code can&#8217;t throw in ways the compiler didn&#8217;t write.<\/p>\n<h2> <a name=\"seven-backends-one-vocabulary\" href=\"#seven-backends-one-vocabulary\"> <\/a> Seven backends, one vocabulary <\/h2>\n<p>The same 24 lines through every backend:<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>Backend<\/th>\n<th><code>--backend<\/code><\/th>\n<th>Lines for b.guml<\/th>\n<th>What it&#8217;s for<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>React + TS<\/td>\n<td><code>react<\/code><\/td>\n<td>253<\/td>\n<td>the default; ~85 lines are the inlined runtime<\/td>\n<\/tr>\n<tr>\n<td>React, shared runtime<\/td>\n<td><code>react --runtime shared<\/code><\/td>\n<td>171 + 95<\/td>\n<td>one runtime module across pages, so invalidation holds across them<\/td>\n<\/tr>\n<tr>\n<td>Svelte 5<\/td>\n<td><code>svelte<\/code><\/td>\n<td>202<\/td>\n<td>runes<\/td>\n<\/tr>\n<tr>\n<td>Web Components<\/td>\n<td><code>wc<\/code><\/td>\n<td>267<\/td>\n<td>no framework, no build step<\/td>\n<\/tr>\n<tr>\n<td>Static HTML<\/td>\n<td><code>html<\/code><\/td>\n<td>297<\/td>\n<td>mostly the inlined stylesheet<\/td>\n<\/tr>\n<tr>\n<td>JSON UI tree<\/td>\n<td><code>json<\/code><\/td>\n<td>274<\/td>\n<td>for your own renderer; the playground preview uses it<\/td>\n<\/tr>\n<tr>\n<td>A2UI<\/td>\n<td><code>a2ui<\/code><\/td>\n<td>240<\/td>\n<td>Google&#8217;s agent-UI wire format, &#8220;a2ui-shaped&#8221;<\/td>\n<\/tr>\n<tr>\n<td>MCP-UI<\/td>\n<td><code>mcp-ui<\/code><\/td>\n<td>13<\/td>\n<td>one JSON resource embedding the wc script<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>The design goal is that backends share decisions rather than copy them: <strong>one element table<\/strong> (<code>element_for(tag)<\/code>), <strong>one design-system table<\/strong> (<code>theme::active().classes(tag, mods)<\/code>), <strong>one expression lowering<\/strong> (<code>expr::lower_in<\/code>) and <strong>one liveness answer<\/strong> (<code>referenced_names<\/code>, which deliberately over-approximates so codegen never drops a declaration the output still mentions).<\/p>\n<p>The element table exists because of a bug. The HTML backend once had a fallback of <code>_ =&gt; \"div\"<\/code>, so the no-JavaScript build had <em>no landmarks<\/em>. Now a test checks that every tag lowers to the same element in every backend.<\/p>\n<p>The A2UI emitter is labelled honestly: it was <em>&#8220;written from a description of the protocol rather than against its published JSON schema&#8221;<\/em>. Actions become declared <strong>intents<\/strong>, so no JavaScript travels in the payload.<\/p>\n<h2> <a name=\"themes-and-capabilities\" href=\"#themes-and-capabilities\"> <\/a> Themes and capabilities <\/h2>\n<h3> <a name=\"a-theme-is-data-with-a-contract\" href=\"#a-theme-is-data-with-a-contract\"> <\/a> A theme is data, with a contract <\/h3>\n<p>There are no class strings in GUML source, so all presentation comes from a theme. A theme is JSON: rules mapping <code>(tag, modifiers)<\/code> to classes, plus a <strong>contract<\/strong> the compiler enforces. From <code>crates\/guml-codegen\/src\/theme.rs<\/code>: <\/p>\n<div>\n<pre><code><span>pub<\/span> <span>fn<\/span> <span>validate<\/span><span>(<\/span><span>&amp;<\/span><span>self<\/span><span>)<\/span> <span>-&gt;<\/span> <span>Result<\/span><span>&lt;<\/span><span>(),<\/span> <span>ThemeError<\/span><span>&gt;<\/span> <span>{<\/span> <span>if<\/span> <span>self<\/span><span>.contract.focus_visible<\/span><span>.trim<\/span><span>()<\/span><span>.is_empty<\/span><span>()<\/span> <span>{<\/span> <span>return<\/span> <span>Err<\/span><span>(<\/span><span>ThemeError<\/span><span>::<\/span><span>WeakContract<\/span><span>(<\/span><span>format!<\/span><span>(<\/span> <span>\"theme `{}` declares no `contract.focus_visible`; a control nobody can see focus on <\/span><span>\\<\/span><span> is unusable from a keyboard, and the compiler cannot supply one it does not know\"<\/span><span>,<\/span> <span>self<\/span><span>.name<\/span><span>)));<\/span> <span>}<\/span> <span>if<\/span> <span>self<\/span><span>.contract.min_contrast<\/span> <span>&lt;<\/span> <span>4.5<\/span> <span>{<\/span> <span>return<\/span> <span>Err<\/span><span>(<\/span><span>ThemeError<\/span><span>::<\/span><span>WeakContract<\/span><span>(<\/span><span>format!<\/span><span>(<\/span> <span>\"theme `{}` declares `min_contrast` {}, below the WCAG AA floor of 4.5 for body text\"<\/span><span>,<\/span> <span>...<\/span><span>)));<\/span> <span>}<\/span> <span>Ok<\/span><span>(())<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The compiler appends the contract&#8217;s focus and disabled classes to every focusable element itself, <em>&#8220;so a theme cannot forget it on one control&#8221;<\/em>. That&#8217;s what lets a themeable compiler still promise accessible output. Two themes are built in: stock <code>tailwind<\/code> (the default) and <code>shadcn<\/code>, which uses shadcn\/ui&#8217;s own <code>oklch<\/code> tokens, so a host already running shadcn drops it in unchanged.<\/p>\n<h3> <a name=\"what-will-this-document-do\" href=\"#what-will-this-document-do\"> <\/a> What will this document do? <\/h3>\n<p>If a model writes your UI, you want to know what it can do before you render it. <code>guml capabilities<\/code> walks the AST and tells you. Real output over the fixtures: <\/p>\n<div>\n<pre><code>fixtures\\b.guml \u2014 Tasks (app) network: self GET \/api\/tasks (tasks) POST \/api\/tasks (tasks.add) mutating PATCH \/api\/tasks\/{id} (tasks.save) mutating DELETE \/api\/tasks\/{id} (tasks.drop) mutating component `tabs` needs a runtime component `list` needs a runtime fixtures\\c.guml \u2014 Landing (core) inert: no script, no network, markup only fixtures\\d.guml \u2014 Spend (app) script: a `js` block runs code the compiler does not check raw: host markup the compiler does not escape escape hatches: 3 (10.7% of lines) <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>It also derives a Content-Security-Policy from that manifest, so the browser enforces what the compiler found: <\/p>\n<div>\n<pre><code>$ guml capabilities fixtures\/b.guml --csp react default-src 'none'; connect-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; form-action 'none'; frame-ancestors 'none'; base-uri 'none' <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhs73f4he8jly5o6oh4q.webp\"><\/a><\/p>\n<p><em>Diagram from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<h2> <a name=\"what-is-measured-and-what-is-not\" href=\"#what-is-measured-and-what-is-not\"> <\/a> What is measured, and what is not <\/h2>\n<p>This project is a study before it&#8217;s a language, and the repo is unusually strict about labelling claims. I&#8217;ll keep the same discipline here, because it&#8217;s easy to overclaim in this space.<\/p>\n<h3> <a name=\"measured-authored-fixtures-a-real-tokenizer\" href=\"#measured-authored-fixtures-a-real-tokenizer\"> <\/a> Measured: authored fixtures, a real tokenizer <\/h3>\n<p>These are hand-written GUML and hand-written React for the same three apps, counted with <code>cl100k_base<\/code>:<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>Fixture<\/th>\n<th>React + TS + Tailwind<\/th>\n<th>GUML<\/th>\n<th>Reduction<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Counter card<\/td>\n<td>368<\/td>\n<td>64<\/td>\n<td>82.6%<\/td>\n<td>5.8\u00d7<\/td>\n<\/tr>\n<tr>\n<td>Task CRUD<\/td>\n<td>1,434<\/td>\n<td>173<\/td>\n<td>87.9%<\/td>\n<td>8.3\u00d7<\/td>\n<\/tr>\n<tr>\n<td>Landing page<\/td>\n<td>1,648<\/td>\n<td>376<\/td>\n<td>77.2%<\/td>\n<td>4.4\u00d7<\/td>\n<\/tr>\n<tr>\n<td><strong>Total<\/strong><\/td>\n<td><strong>3,450<\/strong><\/td>\n<td><strong>613<\/strong><\/td>\n<td><strong>82.2%<\/strong><\/td>\n<td><strong>5.6\u00d7<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p><code>o200k_base<\/code> agrees within a percentage point. The Task CRUD row has a story. It was first published as 175 tokens. A recount said 182, and after <code>guml fmt<\/code> removed 12 bytes of padding it&#8217;s 173. The research report says so in a correction box: <em>&#8220;the original figure was wrong by 7 tokens and no one caught it, which is exactly the failure this project accuses other people&#8217;s headline claims of.&#8221;<\/em> The culprit was Windows Python reading stdin in the console codepage, which mangled the <code>\u2026<\/code> character.<\/p>\n<p>The obvious objection is &#8220;why not just emit JSON?&#8221;, so that was measured too. Minified JSON for the same task app is <strong>315 tokens against GUML&#8217;s 173, so GUML is 45% smaller.<\/strong><\/p>\n<h3> <a name=\"the-content-floor\" href=\"#the-content-floor\"> <\/a> The content floor <\/h3>\n<p>Compression isn&#8217;t uniform, and the reason is prose. In the landing page, <strong>232 of GUML&#8217;s 376 tokens are the human copy itself<\/strong>, which costs the same in any language. Structure is only 144 tokens in GUML, against about 1,416 in React. So the ratio follows a simple curve:<\/p>\n<p>$$<br \/> r(P) = \\frac{S_{\\text{React}} + P}{S_{\\text{GUML}} + P}, \\qquad r(0) = \\frac{1416}{144} \\approx 9.8\\times, \\qquad \\lim_{P \\to \\infty} r(P) = 1<br \/> $$<\/p>\n<p>Drag across it:<\/p>\n<blockquote>\n<p>A frame from the interactive sketch in <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a> \u2014 it does not render here.<\/p>\n<\/blockquote>\n<p>Structure-heavy screens approach 8\u201310\u00d7, and content-heavy ones settle toward 2\u20133\u00d7. In the report&#8217;s words: <em>&#8220;Any benchmark that reports a single average number is misleading.&#8221;<\/em> The benchmark harness takes that literally and has no code path that prints an overall average.<\/p>\n<h3> <a name=\"measured-guml-against-its-own-compiler-output\" href=\"#measured-guml-against-its-own-compiler-output\"> <\/a> Measured: GUML against its own compiler output <\/h3>\n<p>The README&#8217;s headline <strong>7.8\u00d7<\/strong> is a different measurement, so it gets its own label. <code>scripts\/measure.mjs<\/code> compiles every fixture and divides the size of the <em>emitted<\/em> React by the size of the GUML. That needs no second author, because nobody wrote the right-hand side. It uses a 3.6 characters-per-token estimate, not a tokenizer. My run: <\/p>\n<div>\n<pre><code> fixture guml emitted ratio vs hand-written ---------------------------------------------------------- a.guml 67 476 7.1\u00d7 5.8\u00d7 b.guml 183 2907 15.9\u00d7 8.8\u00d7 c.guml 420 1823 4.3\u00d7 4.2\u00d7 d.guml 203 2597 12.8\u00d7 \u2014 e.guml 604 4434 7.3\u00d7 \u2014 invoices.guml 211 3087 14.6\u00d7 \u2014 portfolio.guml 1039 5866 5.6\u00d7 \u2014 ---------------------------------------------------------- total 2727 21190 7.8\u00d7 <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The script prints its own caveat: the ratio <em>rises when the compiler generates more code<\/em>. <code>b.guml<\/code> reads 15.9\u00d7 here because the compiler now emits a cache layer a human wouldn&#8217;t write, so 15.9\u00d7 and the 8.3\u00d7 above must never be quoted together.<\/p>\n<h3> <a name=\"measured-the-cost-of-a-change\" href=\"#measured-the-cost-of-a-change\"> <\/a> Measured: the cost of a <em>change<\/em> <\/h3>\n<p>First drafts aren&#8217;t where the time goes. <code>bench\/guml-bench\/edit-locality.mjs<\/code> scripts eight real modifications and compares the <strong>smallest unified diff<\/strong> in each language. That&#8217;s the fair baseline, diff-based editing, not regeneration. Hover the bars:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famhts13d4ge0b6b7njzq.webp\"><\/a><\/p>\n<p><em>Chart from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<p>The median over the seven comparable edits is <strong>5.5\u00d7<\/strong>. The eighth edit, <em>add a loading state<\/em>, isn&#8217;t in the chart because it costs <strong>zero<\/strong> tokens in GUML (a resource&#8217;s loading state is generated from its declaration) against 67 in React. And again the caveat that ships with the script: it&#8217;s 8 scripted modifications, where the report specifies 50 tasks \u00d7 3. It&#8217;s a seed set, not a publishable sample.<\/p>\n<h3> <a name=\"not-measured\" href=\"#not-measured\"> <\/a> Not measured <\/h3>\n<p>Whether a model can <em>produce<\/em> correct GUML, and whether that&#8217;s better or worse than producing React. That&#8217;s the actual research question, and it&#8217;s still open. More on it <a href=\"#the-question-still-open\">below<\/a>.<\/p>\n<h2> <a name=\"a-real-model-tried-it\" href=\"#a-real-model-tried-it\"> <\/a> A real model tried it <\/h2>\n<p>There&#8217;s one exploratory run with a real model, and it&#8217;s reported because a negative result is worth reporting. Six small apps were generated by <strong>Llama 3.1 8B Instruct<\/strong> on build.nvidia.com, with the shipping prompt (about 3,073 estimated tokens), temperature 0.2, one generation each:<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>App<\/th>\n<th>Parses<\/th>\n<th>Requirements met<\/th>\n<th>First error<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>todo<\/td>\n<td>yes<\/td>\n<td>8 \/ 8<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td>expenses<\/td>\n<td>no<\/td>\n<td>6 \/ 6<\/td>\n<td>GUML0030 unknown tag <code>option<\/code> <\/td>\n<\/tr>\n<tr>\n<td>bmi<\/td>\n<td>no<\/td>\n<td>5 \/ 6<\/td>\n<td>GUML0020 trailing prose<\/td>\n<\/tr>\n<tr>\n<td>dashboard<\/td>\n<td>no<\/td>\n<td>5 \/ 6<\/td>\n<td>GUML0033 undeclared <code>order<\/code> <\/td>\n<\/tr>\n<tr>\n<td>signup<\/td>\n<td>no<\/td>\n<td>5 \/ 6<\/td>\n<td>GUML0030 <code>option<\/code> <\/td>\n<\/tr>\n<tr>\n<td>tip<\/td>\n<td>no<\/td>\n<td>3 \/ 5<\/td>\n<td>GUML0030 unknown tag <code>---<\/code> <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p><strong>1 of 6 parsed, but 32 of 37 functional requirements were met.<\/strong> The findings file says it best: <em>&#8220;The gap between those two numbers is the finding.&#8221;<\/em> The model understood the apps: the right resources, mutations, aggregates, enumerated filters and <code>disabled<\/code> bindings. It failed on surface rules.<\/p>\n<p>Then the interesting part. Two of those failures were the compiler <strong>refusing information it already had<\/strong>. A <code>select<\/code> with <code>option<\/code> children was rejected even though codegen already accepted that spelling, and a field&#8217;s accessible name was sitting in the binding next to it. After fixing both, the <em>same six generations<\/em> went from 1 of 6 compiling to <strong>3 of 6<\/strong>, with no regeneration: identical model output, and a compiler that stopped saying no.<\/p>\n<p>The two that still fail are <code>GUML0023<\/code>, the expression language: <code>**<\/code>, and <code>orders.where(status=new).count<\/code>. That&#8217;s what a model reaches for and the parser doesn&#8217;t cover yet. One more humbling number: of nine model-driven repair attempts, seven did no better than the free layers, and two made things worse. A remarkable bonus is that <code>todo.guml<\/code> independently reproduced the shape of the hand-written task fixture from a prompt that never mentioned it.<\/p>\n<h2> <a name=\"the-playground-and-the-chat\" href=\"#the-playground-and-the-chat\"> <\/a> The playground and the chat <\/h2>\n<p>The docs site is a Next.js app, and everything interactive on it runs the real compiler as WebAssembly in your tab. The playground shows real diagnostics, a format button, and an &#8220;emitted React&#8221; pane that is byte-for-byte what <code>guml build<\/code> writes:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyarxfh1in0be3o6e2f8q.png\"><\/a><\/p>\n<p>The preview is rendered from the compiler&#8217;s own JSON UI tree, never by <code>eval<\/code>, so it can&#8217;t drift from the code.<\/p>\n<p>The <code>\/chat<\/code> page closes the loop: describe an interface, a hosted model answers in GUML, and the compiler renders it in the tab. The free repair layers run on the answer, and the page tells you what they changed. The prompt-tax meter is honest too, at about 3,713 estimated tokens, and the system prompt is generated from the same file the Phase 0 experiment uses: <em>&#8220;A product that prompts differently from the experiment makes the experiment decorative.&#8221;<\/em><\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx7kahe66y22fbrqcawgr.png\"><\/a><\/p>\n<p>For agents there&#8217;s an MCP server, <code>guml mcp<\/code>, with five tools: <code>guml_registry<\/code>, <code>guml_spec<\/code>, <code>guml_check<\/code>, <code>guml_repair<\/code> and <code>guml_compile<\/code>. A prompt can carry a vocabulary, but it can&#8217;t tell a model whether what it just wrote is correct. That needs the compiler: <\/p>\n<div>\n<pre><code>\u2192 guml_check(source) DOES NOT COMPILE \u2014 1 error(s). Fix these and check again. error [GUML0030] line 4: unknown tag `crad` help: did you mean `card`? replace with: card <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h2> <a name=\"proof-what-i-ran-for-this-post\" href=\"#proof-what-i-ran-for-this-post\"> <\/a> Proof: what I ran for this post <\/h2>\n<p>I didn&#8217;t want to restate README numbers, so I re-ran what I could on a clean checkout of <code>main<\/code> (<code>4418bc5<\/code>):<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>Claim<\/th>\n<th>How I checked<\/th>\n<th>Result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>561 Rust tests<\/td>\n<td><code>cargo test --workspace<\/code><\/td>\n<td><strong>561 passed, 0 failed, 1 ignored<\/strong><\/td>\n<\/tr>\n<tr>\n<td>49 diagnostic codes<\/td>\n<td>counted from <code>guml explain<\/code> <\/td>\n<td><strong>49<\/strong><\/td>\n<\/tr>\n<tr>\n<td>61 conformance cases<\/td>\n<td>counted the cases in <code>spec\/tests\/*.txt<\/code> <\/td>\n<td><strong>15 + 30 + 7 + 9 = 61<\/strong><\/td>\n<\/tr>\n<tr>\n<td>7.8\u00d7 vs emitted React<\/td>\n<td><code>node scripts\/measure.mjs<\/code><\/td>\n<td> <strong>7.8\u00d7<\/strong>, output above<\/td>\n<\/tr>\n<tr>\n<td>Edit locality<\/td>\n<td><code>node bench\/guml-bench\/edit-locality.mjs<\/code><\/td>\n<td> <strong>median 5.50\u00d7<\/strong> over 7 edits<\/td>\n<\/tr>\n<tr>\n<td>One-pass diagnostics<\/td>\n<td><code>guml check broken.guml<\/code><\/td>\n<td>3 errors from 3 passes, output above<\/td>\n<\/tr>\n<tr>\n<td>Free repair<\/td>\n<td><code>guml repair --format json<\/code><\/td>\n<td> <strong>7 \u2192 2 errors<\/strong>, no model call<\/td>\n<\/tr>\n<tr>\n<td>Playground, chat, emitted React<\/td>\n<td>local docs site + headless Chrome<\/td>\n<td>the screenshots in this post<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>Not re-run by me: the 1,000,000-document fuzz run (the one ignored test), the mutation study (1,031 definitionally invalid mutants, <strong>95.5% detected<\/strong>), and anything needing an API key.<\/p>\n<h2> <a name=\"what-i-found-while-writing-this\" href=\"#what-i-found-while-writing-this\"> <\/a> What I found while writing this <\/h2>\n<p>Same rule as every project post: if I find something while writing, it goes in. GUML&#8217;s own invariant is <em>&#8220;a quietly wrong compiler destroys the reliability claim the whole project rests on&#8221;<\/em>, so these matter more here than they would elsewhere.<\/p>\n<h3> <a name=\"1-apply-3-fixes-deletes-a-line\" href=\"#1-apply-3-fixes-deletes-a-line\"> <\/a> 1. &#8220;Apply 3 fixes&#8221; deletes a line <\/h3>\n<p>Click <strong>apply 3 fixes<\/strong> on the playground&#8217;s broken sample and you get this:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonyvbreblhgn21jmpwnk.png\"><\/a><\/p>\n<p><code>metric {kount}<\/code> became the bare word <code>count<\/code>, which is now an unknown tag. Remember those carets? <code>GUML0033<\/code>&#8216;s span covers the <strong>whole element<\/strong>, but its suggestion is just the <strong>identifier<\/strong>. The Rust applier already knows about this failure mode and refuses it. From <code>crates\/guml-compiler\/src\/fix.rs<\/code>: <\/p>\n<div>\n<pre><code><span>\/\/\/ A suggestion replaces its span, so a bare word attached to a whole-line span replaces the<\/span> <span>\/\/\/ line with that word. [...] the failure mode is silent data loss, so the applier checks<\/span> <span>\/\/\/ rather than trusts: if the span covers whitespace, the replacement has to as well.<\/span> <span>fn<\/span> <span>spans_one_token<\/span><span>(<\/span><span>span_text<\/span><span>:<\/span> <span>&amp;<\/span><span>str<\/span><span>,<\/span> <span>replacement<\/span><span>:<\/span> <span>&amp;<\/span><span>str<\/span><span>)<\/span> <span>-&gt;<\/span> <span>bool<\/span> <span>{<\/span> <span>!<\/span><span>span_text<\/span><span>.contains<\/span><span>(<\/span><span>char<\/span><span>::<\/span><span>is_whitespace<\/span><span>)<\/span> <span>||<\/span> <span>replacement<\/span><span>.contains<\/span><span>(<\/span><span>char<\/span><span>::<\/span><span>is_whitespace<\/span><span>)<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The TypeScript twin in <code>@guml\/core<\/code>, which the playground and the chat page both call, doesn&#8217;t have that guard: <\/p>\n<div>\n<pre><code><span>export<\/span> <span>function<\/span> <span>applySuggestion<\/span><span>(<\/span><span>source<\/span><span>:<\/span> <span>string<\/span><span>,<\/span> <span>d<\/span><span>:<\/span> <span>Diagnostic<\/span><span>):<\/span> <span>string<\/span> <span>{<\/span> <span>if <\/span><span>(<\/span><span>!<\/span><span>d<\/span><span>.<\/span><span>suggestion<\/span> <span>||<\/span> <span>!<\/span><span>isApplicable<\/span><span>(<\/span><span>d<\/span><span>))<\/span> <span>return<\/span> <span>source<\/span><span>;<\/span> <span>return<\/span> <span>source<\/span><span>.<\/span><span>slice<\/span><span>(<\/span><span>0<\/span><span>,<\/span> <span>d<\/span><span>.<\/span><span>span<\/span><span>.<\/span><span>start<\/span><span>)<\/span> <span>+<\/span> <span>d<\/span><span>.<\/span><span>suggestion<\/span> <span>+<\/span> <span>source<\/span><span>.<\/span><span>slice<\/span><span>(<\/span><span>d<\/span><span>.<\/span><span>span<\/span><span>.<\/span><span>end<\/span><span>);<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Also, the button counts every diagnostic with a <code>suggestion<\/code>, including the <code>aria=\"\u2026\"<\/code> template it then correctly skips, so it says &#8220;3 fixes&#8221; and applies 2. The fix is small: port <code>spans_one_token<\/code> to TypeScript, count only applicable suggestions, and ideally narrow <code>GUML0033<\/code>&#8216;s span to the identifier. The lesson is bigger: <strong>two implementations of one policy drift<\/strong>, and the test that would catch it is a parity test over the same inputs.<\/p>\n<h3> <a name=\"2-web-components-delete-changes-the-filter\" href=\"#2-web-components-delete-changes-the-filter\"> <\/a> 2. Web Components: Delete changes the filter <\/h3>\n<p>In the <code>wc<\/code> output for the task app, top-level actions and row actions are numbered independently, both starting at 0. So the row&#8217;s Delete button carries <code>data-g-act=\"1\"<\/code>, which is exactly the <code>case \"click:1\"<\/code> that sets the filter to <code>all<\/code>. The row checkbox carries <code>data-g-act=\"0\"<\/code>, which only has a <code>submit:0<\/code> case. Clicking Delete changes the filter, and ticking a task does nothing. The existing test compares <em>sets<\/em> of action indices, and the collision makes those sets equal, so it passes. MCP-UI embeds the same script, so it inherits the bug.<\/p>\n<h3> <a name=\"3-svelte-new-tasks-are-posted-empty\" href=\"#3-svelte-new-tasks-are-posted-empty\"> <\/a> 3. Svelte: new tasks are posted empty <\/h3>\n<p>The Svelte backend emits <code>async function tasksAdd(item, body = {})<\/code> for every mutation, but the form calls <code>tasksAdd({ title: draft })<\/code>. The object lands in <code>item<\/code>, so the POST body is <code>{}<\/code>.<\/p>\n<h3> <a name=\"4-raw-raw-html-endraw-passes-the-inert-gate\" href=\"#4-raw-raw-html-endraw-passes-the-inert-gate\"> <\/a> 4. <code>raw html<\/code> passes the &#8220;inert&#8221; gate <\/h3>\n<p><code>core<\/code> is described as <em>&#8220;safe to render from an untrusted agent&#8221;<\/em>, and <code>--assert-inert<\/code> as the gate for that. But this document passes both <code>--core<\/code> and <code>--assert-inert<\/code>, and the HTML backend emits the handler verbatim: <\/p>\n<div>\n<pre><code>page P raw html &lt;img src=x onerror=alert(1)&gt; p Hi <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The parser rejects <code>js<\/code> at the core level but not <code>raw<\/code>, and <code>is_inert()<\/code> ignores the <code>raw_markup<\/code> flag the manifest already computes. The derived CSP would block the inline handler, but only if the host actually sends it. I&#8217;d fix this one first.<\/p>\n<h3> <a name=\"5-docs-drift\" href=\"#5-docs-drift\"> <\/a> 5. Docs drift <\/h3>\n<ul>\n<li>The README says shadcn\/ui is the default theme; the code defaults to stock Tailwind.<\/li>\n<li>The committed wasm is 861 KB, not the 787 KB the README and playground quote.<\/li>\n<li>The docs home page says &#8220;396 tests&#8221; and the architecture page &#8220;552 tests&#8221;; the suite has 561.<\/li>\n<\/ul>\n<p>The repo already has <code>check-readme-claims.mjs<\/code> to stop exactly this, and these are the claims it doesn&#8217;t cover yet.<\/p>\n<p>None of this changes the argument, and all of it was found by <em>running<\/em> the thing rather than reading it, which is the project&#8217;s own philosophy. React is the backend that&#8217;s exercised by typecheck and render tests, and it&#8217;s the one that held up. The others need the same harness pointed at them.<\/p>\n<h2> <a name=\"the-question-still-open\" href=\"#the-question-still-open\"> <\/a> The question still open <\/h2>\n<p>Everything above is the <em>instrument<\/em>. The research report is blunt about what the language itself contributes: <em>&#8220;the language is not novel. The measurement, the crossover characterization, and the convention-as-correctness-guarantee framing are.&#8221;<\/em><\/p>\n<p>The tension is real. A DSL cuts output tokens and narrows what a model can get wrong, but it also moves the model <strong>off-distribution<\/strong>. GUML has zero training data by construction, and the low-resource-language literature consistently reports accuracy drops. Against that, Anka (arXiv:2512.23214) reports a constrained DSL beating Python by 40 points on multi-step tasks, in one narrow domain. Nobody has characterised where the crossover is.<\/p>\n<p>So the report pre-registers six hypotheses: validity, correctness (<em>&#8220;the crux hypothesis. If H2 fails, the project is a cost optimization, not a research contribution&#8221;<\/em>), hallucination, consistency, the accessibility floor, and a capability threshold. The last one predicts GUML helps small models most and may <em>invert<\/em> on frontier models.<\/p>\n<p>And it defines a two-week kill-or-continue experiment, <strong>Phase 0<\/strong>:<\/p>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto\/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1w6ntlkujgpp4tc2s4jg.webp\"><\/a><\/p>\n<p><em>Diagram from <a href=\"https:\/\/www.ravikishan.me\/blog\/guml-token-efficient-ui-language-compiler-for-llms\" target=\"_blank\" rel=\"noopener noreferrer\">the original<\/a>.<\/em><\/p>\n<p>Status, verbatim from the repo: <strong>&#8220;not yet run. The gate is undecided.&#8221;<\/strong> Everything the experiment needs is built and runs in CI: the ten tasks, ten React references that typecheck under <code>--strict<\/code>, prompt assembly, scoring, and a blind scoresheet. An attempt with a valid key returned <code>Your credit balance is too low<\/code>, so zero of the 90 generations have run. What&#8217;s missing is the answer, not the instrument.<\/p>\n<h2> <a name=\"what-i-learned\" href=\"#what-i-learned\"> <\/a> What I learned <\/h2>\n<ul>\n<li> <strong>Diagnostics are the product.<\/strong> In a generation loop, an error message is an API: a code, a span and a replacement. &#8220;Every error in one pass&#8221; saves real money, and &#8220;append-only codes&#8221; is a compatibility promise like any other.<\/li>\n<li> <strong>Moving work into the compiler moves bugs into the compiler.<\/strong> That&#8217;s a good trade, because a compiler bug is fixed once for every generation. But it raises the bar: a silently wrong backend is worse than a loud model error.<\/li>\n<li> <strong>Two implementations of one rule will drift.<\/strong> Rust and TypeScript fix appliers, seven backends, a README and a docs site. Every drift I found was between two copies of the same decision.<\/li>\n<li> <strong>Compression has a floor, and it&#8217;s prose.<\/strong> You can&#8217;t compress the words a human has to read. Any single average ratio hides that.<\/li>\n<li> <strong>The honest number is the interesting one.<\/strong> 1 of 6 parsing, but 32 of 37 requirements met, taught me more than any compression figure, because it says exactly where the compiler, not the model, was the problem.<\/li>\n<li> <strong>Measure the thing that&#8217;s fair, not the thing that flatters.<\/strong> Diff-based edits instead of regeneration, the compiler&#8217;s own output instead of a second author, and a correction box when a number was wrong.<\/li>\n<\/ul>\n<h2> <a name=\"whats-next\" href=\"#whats-next\"> <\/a> What&#8217;s next <\/h2>\n<ul>\n<li>Fix the five findings above, starting with the <code>raw<\/code> inert gate, then port <code>spans_one_token<\/code> to <code>@guml\/core<\/code> with a parity test.<\/li>\n<li>Point the typecheck and render harness at Svelte and Web Components, not just React. The <code>wc<\/code> collision would have been one click away from being caught.<\/li>\n<li>Grow the expression language where models actually reach: <code>**<\/code>, ternaries, and <code>.where(...)<\/code> aggregates (<code>GUML0023<\/code>).<\/li>\n<li>Playwright, axe and Lighthouse over the emitted output. Today it&#8217;s proven to typecheck and server-render, not proven to work when clicked. That&#8217;s the largest gap in what&#8217;s verified.<\/li>\n<li> <strong>Run Phase 0.<\/strong> 90 generations, three models, a blind grader. Then publish whichever answer comes back.<\/li>\n<\/ul>\n<h2> <a name=\"try-it\" href=\"#try-it\"> <\/a> Try it <\/h2>\n<div>\n<pre><code>git clone https:\/\/github.com\/ravikisha\/guml <span>&amp;&amp;<\/span> <span>cd <\/span>guml cargo <span>test<\/span> <span>--workspace<\/span> cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> build fixtures\/b.guml <span># React + TS<\/span> cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> build fixtures\/b.guml <span>--backend<\/span> svelte cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> check fixtures\/invoices.guml <span>--format<\/span> json cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> capabilities fixtures cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> explain GUML0050 <span># what a model actually returns, repaired with no model call:<\/span> <span>printf<\/span> <span>'Here you go:\\n\\n``` guml\\npage P\\ndiv\\n span Hi\\n ```\\n'<\/span> | cargo run <span>-q<\/span> <span>-p<\/span> guml-cli <span>--<\/span> repair node scripts\/measure.mjs <span># the 7.8\u00d7 table<\/span> node bench\/guml-bench\/edit-locality.mjs <span># the cost of a change<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Or give your agent the compiler instead of a prompt: <\/p>\n<div>\n<pre><code><span>{<\/span><span> <\/span><span>\"mcpServers\"<\/span><span>:<\/span><span> <\/span><span>{<\/span><span> <\/span><span>\"guml\"<\/span><span>:<\/span><span> <\/span><span>{<\/span><span> <\/span><span>\"command\"<\/span><span>:<\/span><span> <\/span><span>\"guml\"<\/span><span>,<\/span><span> <\/span><span>\"args\"<\/span><span>:<\/span><span> <\/span><span>[<\/span><span>\"mcp\"<\/span><span>]<\/span><span> <\/span><span>}<\/span><span> <\/span><span>}<\/span><span> <\/span><span>}<\/span><span> <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>If you&#8217;ve read my posts on <a href=\"https:\/\/dev.to\/blog\/introducing-relaxlang-a-beginner-friendly-programming-langua\">RelaxLang<\/a> (a language) and <a href=\"https:\/\/dev.to\/blog\/relaxnative-native-c-cpp-rust-zig-go-for-nodejs\">Relaxnative<\/a> (Rust from Node), GUML is where those two threads meet: a language whose user is a model, and a Rust compiler that ships to the browser as WebAssembly.<\/p>\n<p>\u2b50 Star it on <strong><a href=\"https:\/\/github.com\/ravikisha\/guml\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub<\/a><\/strong>. If you have API credit and want to help answer the question this project exists for, the Phase 0 harness is waiting. And if you find a document where the compiler is quietly wrong, <a href=\"https:\/\/github.com\/ravikisha\/guml\/issues\" target=\"_blank\" rel=\"noopener noreferrer\">open an issue<\/a>. Those are the bugs that matter most here.<\/p>\n<p>Happy compiling! \ud83e\udd80<\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/ravikishan\/guml-let-the-model-write-24-lines-and-the-compiler-write-the-other-229-5fko\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Read it once and you know the whole app: a typed resource with three mutations (two of them optimistic), a text draft, a three-way filter, a live count in the heading, a form, tabs, and a list with an empty state. Here it is compiled in the browser. This is the docs playground running the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":6109,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"webixso_pending_account_ids":""},"categories":[41],"tags":[],"class_list":["post-6110","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/6110","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=6110"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/6110\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/6109"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=6110"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=6110"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=6110"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}