Interface layer

One HTML interface, five shells

HTML is already a declarative GUI language, and U already has the type that makes it safe to build. So the interface layer is not a new authoring system — it is the existing template machinery pointed at a webview, plus compile-time checks that a web framework normally performs at runtime or not at all.

Every section below is marked implemented partial design so nothing here reads as a promise the compiler already keeps.

1. The template model partial

A backtick literal is raw material. A tag is a function that gives its slots meaning. Calling the result produces the value.

U
`foo {{bar}}`              // raw template, slots untyped
HTML`foo {{bar}}`          // the tag types the slots     -> HTML +F(...)
HTML`foo {{bar}}`({bar})   // call with a slot map        -> HTML
HTML(`foo {{bar}}`)        // SUGAR for the line above

Parentheses around the literal are the invoke marker. HTML(…) yields HTML; HTML`…` yields a template. Lifting a template out for reuse is deleting one pair of characters — and a stored template is exactly a prepared statement.

The slot map

A template function takes a map from slot name to value, where the value type is the union that tag permits in that position:

U
d HtmlSlot : HTML | S | I | N | [HTML]
d SqlSlot  : S | I | N | L | [B]

HTML`<p>{{name}}</p>`  : HTML +F({S : HtmlSlot} +R)
SQL`SELECT ... {{id}}`  : [Row] +F({S : SqlSlot} +R) +A

Because slots are arguments and never text, safety stops being a per-tag rule. No tag can forget to escape or parameterise, because no tag ever concatenates — a new adapter inherits the property instead of re-implementing it.

Inside a backtick literal there is exactly one special form: {{name}}, the slot. A single { is an ordinary character — which is why CSS braces need no escaping inside CSS`…`.

The braces at the call site are unrelated: they are U's ordinary type-directed map literal, where {bar, baz} is property shorthand for {bar: bar, baz: baz}. That map lands in the function's trailing {} bag — the same anonymous destructured map every U function and constructor already takes. A tag is a constructor, so it follows the universal rule rather than inventing one, and the compiler need not materialise a map when the call site is literal.

Status. This model is the SPEC's, and it is largely not yet built. Nothing here has regressed — these are gaps found by testing, not drift.

Working today: both brace forms parse and are distinguished; each tag produces its own output type; unions and +F express the signature above; the on* prohibition and the SQL structural-position check are enforced.

Not yet built, stated precisely:

2. Handlebars is a sibling, not a subtype partial

Handlebars is not a subtype of HTML — substituting an unrendered template where markup is expected would print {{fieldA}} onto the page. Both are Template[Html]; they differ only in who calls them and when.

U
HTML`<div>{{fieldA}}</div>`         // called by U, server side
Handlebars`<div>{{fieldA}}</div>`   // called by the browser, client side

Same shape, same output type, different call site — which is precisely Q_Response::addTemplate(): one artifact usable from either side. And since {{ }} is both U's slot syntax and Handlebars' placeholder syntax, an existing Qbix template is already a valid U template literal.

2b. JS — values into JavaScript partial

The bridge needs to hand U values to the page, so JS is a tag like any other. The constructor JSON-encodes each slot, making it a valid right-hand side:

U
d JsSlot : S | I | N | L | Tree | JSON

f bootstrap(cfg: Tree +R) -> JS
	r => JS(`window.APP = {{cfg}}; window.READY = true;`)

JSON encoding alone is not enough, and the gaps matter most in exactly the place a GUI puts JS — inside a <script> block:

HazardWhy JSON misses itWhat the tag does
</script> in a stringan HTML-parser hazard, not a JS one — < is ordinary in JSescaped by the HTML tag at composition, so plain JS is not over-escaped
U+2028 / U+2029legal in JSON, historically illegal in a JS stringescaped explicitly
slot inside a string or `…`no encoding can help — ${} re-opens codecompile error

So HTML(`<script>{{code}}</script>`) composes safely with a JS value, while the on* prohibition still blocks the inline-handler route entirely. Under Qbix this is how Q_Response::setScriptData("MyPlugin.thing", …) would be emitted — the same job, checked at compile time.

Status: tag, slot rules and both encoders implemented and tested. Not yet: __validate__ is not called, so the JavaScript itself is not parsed, and the HTML tag does not yet apply the script-position escaper automatically.

3. Qbix compatibility partial

Qbix already answers the "do we need JSX" question, and the answer is no. Tools are declared as attributes on ordinary elements and activated afterwards. U emits exactly that markup.

U — equivalent to Q::tool()
f thingTool(publisherId: S, streamName: S) -> HTML
	r => HTML(`<div class="MyPlugin_thing_tool"
	                data-publisherid="{{publisherId}}"
	                data-streamname="{{streamName}}"></div>`)
Qbix conventionHow U meets it
Q::tool() / Q.Tool.setUpElementemit the element with data-* options; Q.activate unchanged
Q.Template.set / renderHandlebars`…` — same {{ }}, exported via addTemplate
delegated namespaced eventsenforced: on* interpolation is a compile error
tool.rendering() fine-grained updatesmatch it; no virtual DOM
Q.Tool.clear() before innerHTMLrequired before replacing markup containing tools

What U adds

A misspelled tool name fails at runtime today — the element simply never activates. With the tool registry loaded at build time through z f __schema__, that becomes a compile error, exactly as column names do for SQL:

compile error
HTML(`<div class="MyPlugin_thnig_tool">`)
                    ^ no tool registered under that name

4. Events bind in code implemented

HTML refuses an interpolation inside any on* attribute, because that value is parsed as JavaScript and no HTML escaping makes it safe. This forces the delegated, namespaced pattern Qbix already mandates — so the compiler enforces the framework's own convention rather than fighting it.

U
HTML(`<button onclick="{{js}}">x</button>`)              // COMPILE ERROR
HTML(`<button class="MyPlugin_btn">{{label}}</button>`)  // fine

DOM events then arrive as a stream. U already has pull streams with backpressure, so debounce, throttle, batch and the Rx operators apply unchanged — mirroring Q.debounce and Q.throttle.

5. The bridge is another +A source design

Calling into a webview is asynchronous: the answer arrives later, from outside the process. That is exactly what a GPU readback is, and what an HTTP response is. U already drives both through the fiber scheduler and the epoll reactor, so the bridge introduces no new concurrency concept — it is the third distinct subsystem +A has absorbed unmodified.

U
f eval(view: WebView +R, script: S) -> S +A
f load(view: WebView +R, page: HTML) -> N +A
f on(view: WebView +R, channel: S) -> [Message] +R

6. Five shells, two engines design

The view is identical everywhere because it is HTML. Only the shell differs — the same one-interface / several-backends split already used for GPU (Dawn vs browser WebGPU) and sockets (epoll vs WASI poll).

The compatibility story is better than it first looks: there are only two engines. WebKit powers WKWebView and WebKitGTK; Chromium powers WebView2 and Android's WebView. Both are modern, both run the Qbix JS runtime unchanged.

TargetWebviewEngineBindingDifficulty
Webthe page itselfnone (U already targets WASM)trivial
LinuxWebKitGTKWebKitplain Ceasy
macOSWKWebViewWebKitObj-C (a C superset)easy
iOSWKWebViewWebKitsame shim as macOSeasy
WindowsWebView2ChromiumCOMmoderate
Androidandroid.webkitChromiumJNI to Javahardest

The message channel differs — the shim hides it

Each host exposes a different JS→native channel. U emits a small shim that normalises all of them to one call, so U-side code sees a single interface:

HostJS sideNative side
WebKitGTKwindow.webkit.messageHandlers.u.postMessage()register_script_message_handler
WKWebView (macOS/iOS)window.webkit.messageHandlers.u.postMessage()WKScriptMessageHandler
WebView2window.chrome.webview.postMessage()add_WebMessageReceived
Androidwindow.u.post()addJavascriptInterface

The WebKit family shares an identical JS call, so there are really three variants, not five. Native→JS is uniform in shape everywhere — "evaluate this script, deliver the result later" — which is why it maps onto +A without special cases.

Every host requires UI-thread marshalling

WKWebView must be called on the main thread; Android's WebView and evaluateJavascript must run on the UI thread; WebView2 needs the message pump; WebKitGTK needs the GTK main loop. So the scheduler needs a post-to-UI-thread queue alongside the reactor. This is a real addition, not a detail — it is the one place the concurrency model grows to meet the GUI.

Serving the assets

Markup, CSS and JS have to reach the webview. file:// is simplest but carries origin restrictions that break fetch. The better answer is a custom scheme handler, which all four hosts support (WKURLSchemeHandler, AddWebResourceRequestedFilter, register_uri_scheme, shouldInterceptRequest). Assets are then served from memory with a real origin, and Qbix's Q.req works unchanged.

Per-target notes

7. Where JS belongs design

Not as application code. WASM still cannot touch the DOM directly, so a thin JS shim reads events and applies patches — but U generates it; nobody writes it. JS is the FFI to the browser, occupying the same role webgpu.h does for the GPU. Under Qbix most of that runtime already exists: Q.activate, Q.Tool.define, Q.Template.render and the event helpers are what the generated markup targets.

8. What this is not good for

HTML-as-interface is the right default, not a universal answer.

9. Implementation order

  1. Braces{{slot}} = parameter, {expr} = captured expression (currently reversed)
  2. Tags as functions over a template, with Tag(…) as the invoke sugar — this is what makes user-defined tags possible at all
  3. __check__ per position returning the slot union (unions and reflection already work; the wiring does not)
  4. SQL lowering to prepare/execute — makes the existing injection message true rather than aspirational
  5. Handlebars — nearly free once 1–4 land
  6. Tool-registry adapter — the compile-time win over the status quo
  7. Webview shells — Linux and macOS first, iOS next, Android last

Steps 1–5 are compiler work with no new runtime. Step 7 is where the platform surface begins, and where the UI-thread queue is needed.