Dissecting ChatGPT's Generative UI: Why DIL Compiles to an AST Instead of Raw Code

Back to blog
Mehran Mozaffari·

The Anatomy of DIL: Token Streaming, AST Splitting, and Component Registries

When ChatGPT rolled out Generative UI, the immediate assumption across my feeds was that the model had simply been hooked up to an in-browser code runner emitting React strings. It wasn't. Under the hood, OpenAI sidestepped raw frontend code generation entirely, implementing an intermediate representation called DIL (Declarative Interface Language).

DIL solves the central friction of Generative UI: models cannot be trusted with raw DOM access, yet rigid JSON tool-calling destroys conversational flow. The model streams a composite grammar combining standard Markdown prose for explanatory text, JSX-like declarative tags mapped to design system primitives, and lightweight inline JavaScript expressions for state bindings and calculation hooks.

The execution model hinges on a three-stage pipeline that decouples presentation from evaluation:

sequenceDiagram
    autonumber
    participant Model as LLM Inference
    participant Compiler as Server Orchestrator / Compiler
    participant ClientRuntime as Client Sandboxed Runtime
    participant Catalog as Design System Registry

    Model->>Compiler: Stream DIL tokens (Markdown + JSX tags + JS logic)
    activate Compiler
    Compiler->>Compiler: Parse grammar & validate schema
    Compiler->>Compiler: Segregate into Layout AST & JS State Bundle
    Compiler-->>ClientRuntime: Stream structured AST chunks & JS state logic
    deactivate Compiler
    activate ClientRuntime
    ClientRuntime->>Catalog: Resolve AST nodes to whitelisted primitives
    Catalog-->>ClientRuntime: Mount Native Primitives (Slider, Card, Chart)
    ClientRuntime->>ClientRuntime: Execute JS bindings in isolated sandbox
    Note over ClientRuntime: Local reactive loop updates at 60fps without network hops
    deactivate ClientRuntime

The orchestration layer parses the raw token stream in real time and splits it into two distinct payloads before emitting them to the web or mobile client:

  1. A structural layout AST: A sanitized JSON document tree containing component topologies, nesting hierarchies, and declarative props. Unrecognized tags are stripped before reaching the client.
  2. A constrained JS state bundle: A restricted payload containing reactive formulas, state stores, and change hooks.

On the client, the UI mounts via a sandboxed interpreter against an immutable, pre-bundled component whitelist—OpenAI's design system primitives like <Slider />, <Card />, <DataTable />, and <Metric />.

Because dynamic expressions bind directly to client-side reactive state within an isolated scope, moving a slider or toggling a scenario recalculates downstream charts and summary metrics locally at 60 frames per second. There are no inference round-trips, no server-side re-renders, and no network hops. Critically, because the client runtime maps structural AST nodes exclusively to static design primitives, the generated UI runs completely walled off from window, document.cookie, and arbitrary HTTP endpoints. As I argued when analyzing why the deterministic harness is the real architecture of production AI agents, stability comes from constraining model autonomy behind strict mechanical contracts.

The Four GenUI Paradigms: Where Intermediate DSLs Fit Against Iframes and Tool Calls

Generative UI architectures cluster into distinct archetypes based on where they draw the line between developer control and model freedom:

quadrantChart
    title GenUI Architectural Placement
    x-axis Low Flexibility / Constrained --> High Flexibility / Open-Ended
    y-axis Low Security & Cohesion --> High Security & Cohesion
    quadrant-1 Walled Expressive DSLs
    quadrant-2 Rigid Structured Systems
    quadrant-3 Loose Fragile Scripting
    quadrant-4 Sandboxed Mini-Apps
    Tool Calling: [0.22, 0.88]
    Adaptive Cards: [0.32, 0.76]
    DIL: [0.65, 0.84]
    Claude Artifacts: [0.88, 0.38]

At one extreme sits Structured Tool Calling (popularized by Vercel AI SDK and CopilotKit). The model outputs typed JSON arguments against a hard-coded schema, and the client binds them to pre-built React components. It is indestructible, perfectly styled, and completely uninspired: the model cannot invent new layouts or weave novel interactive states. It simply fills forms.

At the opposite extreme sit Sandboxed Iframes (such as Anthropic Claude Artifacts and v0). The model outputs raw React, HTML, or Tailwind code, and an in-browser bundler compiles it inside an isolated iframe. While this grants the model unmatched compositional freedom—it can spin up arbitrary charts or multi-step canvas apps—it fragments the interface. The output is an isolated window, styling diverges from the host application, startup latency is noticeable, and security depends entirely on browser cross-origin boundaries.

Server-Driven UI schemas like Microsoft Adaptive Cards offer strict cross-platform determinism across enterprise surfaces, but they deliberately sacrifice local reactive scripting.

Dimension Structured Tool Calls (Vercel AI SDK) Intermediate DSL (OpenAI DIL) Sandboxed Code (Claude Artifacts)
Token Economy Moderate; JSON repetition and key overhead Highly efficient; compact JSX-like syntax Low; requires extensive code boilerplate and imports
Rendering Latency Instantaneous; renders as soon as arguments validate Near-instant streaming; compiled AST increments High; requires bundling, WebAssembly workers, or iframe boot
Local Reactivity Low; interactions require developer pre-wiring or round-trips High; sandboxed JS bundle recalculates state client-side Complete; full JS runtime runs within the iframe
Visual Cohesion Native; uses host application design system Native; mapped strictly to design catalog primitives Low; styles and themes frequently clash with host chat
Security Posture Absolute; restricted to pre-defined component props High; structural AST filtering and isolated execution Moderate; relies on iframe isolation and CSP policies

OpenAI built DIL to occupy the hybrid middle ground. By combining a compact intermediate language with a client-side design system mapping, DIL achieves the styling fidelity and low token footprint of native UI while granting the model the agency to invent local reactive relationships on the fly.

Streaming ASTs and Token Repair: Where the Compiler Breaks Mid-Generation

The hardest engineering problem in a DIL-style architecture is not client sandboxing—it is parsing an incomplete Abstract Syntax Tree over a streaming token transport.

In conventional chat streams, token-by-token emission is forgiving; text renders sequentially as it arrives. But hierarchical declarative interfaces cannot render half a component. If an inference stream yields <Card><Metric label="Gross Margin" value={calcMargin(, the client cannot mount that subtree. The parser faces a dilemma: wait for tag closure and freeze the chat display, or construct speculative AST projections.

When orchestrators use speculative auto-closing heuristics to stream UI progressively, layout tearing becomes immediate. If the parser speculatively closes a <Container> before the model finishes emitting internal child elements, the reconciler prematurely commits DOM mutations, causing visual flicker, layout jumping, and collapsed cards that snap open moments later.

Model Stream:   <Dashboard> <Metric value=42 /> <Chart data={dataset}
Speculative:    <Dashboard> <Metric value=42 /> <Chart data={dataset} /></Dashboard>  [Mounted]
Next Tokens:    ...labels={['Q1','Q2']} /> <SummaryText> ...
Result:         Reconciler teardown, DOM thrashing, and mid-stream layout tearing

Even if the structural syntax remains intact, semantic hallucinations break the execution layer in two distinct ways:

  • Component Prop Drift: Models routinely hallucinate plausible-sounding attributes that do not exist in the client catalog—such as emitting <Slider stepSize={5} snapToGrid={true} /> instead of step={5}. Without an aggressive server-side normalization layer that discards unknown keys, the client reconciler will drop the node or throw invalid attribute warnings.
  • State Expression Type Collisions: Because layout tags and execution bundles are decoupled, the model can emit state calculations that assume specific runtime types that never materialize. If an expression invokes .filter() or .reduce() on an initialized variable that resolves to null or an empty object during initial mount, the client-side sandboxed evaluator crashes on an unhandled TypeError.

Unless your pipeline wraps every generated node in an isolated error boundary configured to fall back to tabular Markdown prose, a single unclosed brace or bad prop mapping will tear down the entire conversational viewport. Real-world resilience requires treating model-generated DSLs as inherently corrupted inputs until validated against a strict schema.

The Client Sandbox Boundary: WebAssembly Isolation and Infinite Loop Traps

Executing untrusted, model-generated JavaScript on the client cannot rely on naked eval() or loose new Function() wrappers. Even with scope proxies stripping window and document, JavaScript's prototype chain remains notoriously porous. If an agent emits a dynamic formula like netMargin = (revenue - cogs) / revenue, running that calculation natively in the main UI thread exposes the client to prototype pollution and uncontained CPU deadlocks.

To secure this boundary, the client runtime needs a fully virtualized execution layer. In my view, the only robust architecture for this is an embedded WebAssembly interpreter—such as QuickJS compiled to WASM—isolated inside a dedicated Web Worker.

flowchart TD
    ASTNode["Incoming AST Node & JS State Logic"] --> Validate["AST Schema Validator"]
    Validate --> Route{"Evaluation Route"}
    
    Route -->|"Static Props"| Mount["Mount Native React Primitive"]
    Route -->|"Reactive Expression"| Worker["Web Worker Bridge"]
    
    subgraph WASM_Sandbox ["Isolated WebAssembly Scope"]
        Worker --> QJS["QuickJS-WASM Engine"]
        QJS --> StepQuota["Execution Step & CPU Budget Guard"]
        StepQuota --> Calc["Evaluate State Transformation"]
    end

    Calc -->|"Success (Delta Return)"| Store["Update Local Reactive State"]
    Store --> Mount
    
    StepQuota -->|"Timeout / Cycle Exceeded"| ErrorBoundary["Catch in React Error Boundary"]
    Calc -->|"Runtime TypeError"| ErrorBoundary
    
    ErrorBoundary --> Fallback["Degrade to Static Markdown / Table"]

This setup solves two structural vulnerabilities:

  1. Circular Reactive Execution Traps: Because the model synthesizes reactive bindings on the fly, it frequently introduces circular dependencies—State A updates State B, which triggers a hook re-evaluating State A. In standard browser environments, this locks the UI thread at 100% CPU. Inside QuickJS-WASM, we can enforce strict execution-step quotas (JS_SetInterruptHandler) and microsecond timeout budgets. If a computation loop exceeds 50,000 steps or 15 milliseconds, the engine aborts evaluation immediately.
  2. Visual Phishing Containment: While the sandbox prevents credential exfiltration via network calls or storage access, visual injection remains high-risk. A model processing adversarial input could synthesize an authentic-looking authentication card using the host application's official inputs and buttons. Mitigating this requires hard schema exclusions at the component catalog layer: the runtime must reject type="password", external action targets, or any credential-mimicking form fields before layout mounting begins.

When a sandboxed calculation faults or trips an execution quota, the failure must not unseat the parent transcript. The encapsulating React Error Boundary intercepts the exception, bypasses the reactive binding, and degrades the component to a static, deterministic Markdown summary.

State Synchronization and Context Drift: Managing the Client Interaction Blindspot

Local client-side reactivity introduces an architectural paradox: the user experiences frictionless, zero-latency interactivity, but the model becomes completely detached from conversational reality.

When a user adjusts a mortgage calculator slider from $350k to $700k, that state transition lives entirely inside the local Web Worker store. No HTTP packet hits the inference orchestrator. If the user then submits a follow-up prompt—"Now factor in an 8% interest rate for that purchase price"—the model's context window still reflects the initial $350k default. The model answers the wrong question because it cannot observe client mutations.

To bridge this interaction blindspot, the client runtime must maintain an active state delta ledger. Whenever an input changes, the runtime records a serialized JSON patch capturing the dirty fields:

User Action:     Slider moved to 700000
Local Delta:     { "purchasePrice": 700000, "downPayment": 140000 }
Next Turn Prep:  System prepends invisible state context to user prompt:
                 [Client State: purchasePrice=700000, downPayment=140000]
                 "Now factor in an 8% interest rate for that purchase price"

When the user dispatches their next message, the host application intercepts the payload and injects this compacted state delta into the turn's prompt context. This restores model grounding without forcing redundant round-trip inference cycles during manual slider manipulation.

The second operational hurdle is transcript persistence. Chat histories are durable, but design systems change. If an older chat contains a DIL widget that references <MetricCard variant="compact" />, and six months later the design system renames that primitive or refactors its props, replaying historical tokens from the database will cause the component reconciler to fail.

Handling this requires treating stored DIL transcripts as immutable versioned snapshots. When a user navigates away, the client should persist both the raw DIL stream and a frozen AST snapshot stamped with the active catalog schema version. If a historical conversation loads against an incompatible runtime version, the client should not attempt speculative repairs—it should render the compiled static AST directly, preserving the user's past interaction without risking schema collisions.

What to Build: Three Production GenUI Architectures for Applied Engineering Teams

If your team wants to adopt the DIL pattern without waiting for proprietary platform APIs, you do not need to reinvent OpenAI's entire compiler stack. You need clear mechanical boundaries that isolate layout, state logic, and host application execution. Three concrete reference architectures show how this works in practice.

For calculation-dense conversational domains, consider a Headless Financial Scenario Simulator with WASM Sandboxing. The objective is an interactive interface where users tweak mortgage, equity, and interest sliders with instant recalculation of amortization schedules and debt charts directly in chat, eliminating model round-trips. To build this, wire a constrained context-free grammar (CFG) at inference time to force valid declarative tags. The stream flows through a server-side AST parser backed by an Acorn-derived tokenizer, which separates the presentation tree from inline calculation expressions. The client mounts the tree against a pre-compiled shadcn/ui component catalog, routing formula evaluation to a QuickJS instance running in WebAssembly inside a dedicated Web Worker. The primary failure mode to guard against is recursive state oscillation: an amortization slider callback updates an interest hook, which mutates a dependent term and triggers an endless recalculation cascade. Set a hard limit on instruction steps inside the QuickJS worker—say, 30,000 instructions per event—and immediately abort the cycle if a state transaction exceeds that budget.

When workflows require structured input collection, implement a State-Synchronized Dynamic Form Generator with Context Re-Injection. This targets multi-step configuration flows and onboarding sequences generated inline from chat, designed to serialize field updates back into the conversation model without human copy-pasting. The architecture pairs an AST-driven dynamic form engine with a client-side state delta manager. As the user toggles checkboxes or enters numbers, a local ledger records dirty state deltas. When the user eventually types a follow-up prompt, the chat client intercepts the dispatch event and injects a compact JSON state diff into the prompt context payload. The acute vulnerability here is visual spoofing and prompt-injection-driven credential harvesting. If an agent ingests untrusted third-party tool output, it might construct an authentic-looking authentication card. Enforce a strict whitelist on the component catalog that outright strips password fields, file uploaders, and external submission targets before the layout reconciler touches the DOM.

Finally, for high-throughput streaming environments, build a Self-Healing Streamed AST Parser with Markdown Fallback Boundaries. This acts as a streaming frontend proxy that digests partial declarative chunk tokens, speculatively closes open container nodes, and gracefully degrades on schema errors. You pair a token-streaming finite state machine with a speculative AST auto-closer and encapsulating error boundaries configured to fall back to static GitHub-flavored Markdown tables on evaluation exceptions. The mechanical risk is layout tearing: if your speculative closer seals a layout container early and incoming tokens reveal child metrics, the DOM flickers violently. Keep unverified nodes in a buffered reconciliation state until complete container boundaries resolve.

The Production Blueprint: Adopting Constrained UI Generation Without Code Execution Risk

Building production-grade Generative UI requires abandoning the false dichotomy between rigid JSON tool calls and arbitrary code execution. Shipping raw HTML, arbitrary Tailwind strings, or untrusted React bundles straight to client runtimes creates persistent security liabilities, layout mismatches, and severe token inflation. An intermediate, AST-backed representation like DIL provides an intentional middle ground: it delivers the structural flexibility an LLM needs to build bespoke interfaces while ensuring the host application maintains absolute control over styling, execution rights, and layout boundaries.

Operationalizing this model hinges on three architectural decisions.

First, replace post-hoc prompt coaxing with constrained decoding at inference time. If your backend relies on prompt reminders to keep the model from hallucinating non-existent properties, your pipeline will fail in production. Enforcing a context-free grammar or JSON Schema mask during token sampling guarantees that the model emits only registered catalog primitives and valid prop signatures. As I discussed in my analysis of the deterministic harness, shifting validation guarantees from prompt engineering into the compiler layer is the only reliable way to stabilize probabilistic model outputs.

Second, treat your component catalog as an immutable, versioned runtime contract. Design systems evolve rapidly, but chat transcripts are permanent. If you refactor a primitive or deprecate a layout variant six months from now, historical sessions re-evaluating raw markup will crash. Persist historical turns as serialized, static AST snapshots pinned to a explicit catalog version schema rather than re-compiling live tokens on every page visit.

Finally, segregate layout presentation from reactive evaluation. Let the main browser thread reconcile static DOM primitives while offloading reactive calculations, formulas, and state transformations to an isolated WebAssembly sandbox bound by strict step quotas. By isolating compute from presentation and pairing the parser with automatic Markdown fallbacks, your team can deliver dynamic, real-time interfaces without inheriting the systemic vulnerabilities of unconstrained code execution.

Resources

(no official sources were available to link)

Updated 2026-10-07 by Mehran Mozaffari.

Related posts