Interface layer
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.
A backtick literal is raw material. A tag is a function that gives its slots meaning. Calling the result produces the value.
`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 aboveParentheses 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.
A template function takes a map from slot name to value, where the value type is the union that tag permits in that position:
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) +ABecause 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:
{{name}} performs immediate lexical capture — it requires
the name to be in scope. Under this model it should be a deferred parameter.{expr} is literal text, not an expression slot at all. So the two
forms are not simply swapped: {{ }} does the job { } should do, and
{ } does nothing. The fix is one change plus one new feature, not a swap.HTML(`…`) reports "HTML is not defined". Tag behaviour works; tags are
just not first-class, so user-defined tags are impossible.SQL value slots are concatenated:
u_str_concat("SELECT … n = ", nm). This one is a genuine discrepancy
with a safety consequence, because the compiler's own error message claims values
are parameterised. u_pg_prepare/u_pg_execute already exist and are
simply not connected.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.
HTML`<div>{{fieldA}}</div>` // called by U, server side
Handlebars`<div>{{fieldA}}</div>` // called by the browser, client sideSame 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.
JS — values into JavaScript partialThe 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:
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:
| Hazard | Why JSON misses it | What the tag does |
|---|---|---|
</script> in a string | an HTML-parser hazard, not a JS one — < is ordinary in JS | escaped by the HTML tag at composition, so plain JS is not over-escaped |
| U+2028 / U+2029 | legal in JSON, historically illegal in a JS string | escaped explicitly |
slot inside a string or `…` | no encoding can help — ${} re-opens code | compile 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.
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.
f thingTool(publisherId: S, streamName: S) -> HTML
r => HTML(`<div class="MyPlugin_thing_tool"
data-publisherid="{{publisherId}}"
data-streamname="{{streamName}}"></div>`)| Qbix convention | How U meets it |
|---|---|
Q::tool() / Q.Tool.setUpElement | emit the element with data-* options; Q.activate unchanged |
Q.Template.set / render | Handlebars`…` — same {{ }}, exported via addTemplate |
| delegated namespaced events | enforced: on* interpolation is a compile error |
tool.rendering() fine-grained updates | match it; no virtual DOM |
Q.Tool.clear() before innerHTML | required before replacing markup containing tools |
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:
HTML(`<div class="MyPlugin_thnig_tool">`)
^ no tool registered under that nameHTML 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.
HTML(`<button onclick="{{js}}">x</button>`) // COMPILE ERROR
HTML(`<button class="MyPlugin_btn">{{label}}</button>`) // fineDOM 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.
+A source designCalling 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.
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
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.
| Target | Webview | Engine | Binding | Difficulty |
|---|---|---|---|---|
| Web | the page itself | — | none (U already targets WASM) | trivial |
| Linux | WebKitGTK | WebKit | plain C | easy |
| macOS | WKWebView | WebKit | Obj-C (a C superset) | easy |
| iOS | WKWebView | WebKit | same shim as macOS | easy |
| Windows | WebView2 | Chromium | COM | moderate |
| Android | android.webkit | Chromium | JNI to Java | hardest |
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:
| Host | JS side | Native side |
|---|---|---|
| WebKitGTK | window.webkit.messageHandlers.u.postMessage() | register_script_message_handler |
| WKWebView (macOS/iOS) | window.webkit.messageHandlers.u.postMessage() | WKScriptMessageHandler |
| WebView2 | window.chrome.webview.postMessage() | add_WebMessageReceived |
| Android | window.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.
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.
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.
addJavascriptInterface methods need @JavascriptInterface,
and only primitives and strings cross reliably, so the channel is JSON.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.
HTML-as-interface is the right default, not a universal answer.
{{slot}} = parameter, {expr} = captured expression
(currently reversed)Tag(…) as the invoke sugar
— this is what makes user-defined tags possible at all__check__ per position returning the slot union
(unions and reflection already work; the wiring does not)SQL lowering to prepare/execute — makes the existing injection
message true rather than aspirationalHandlebars — nearly free once 1–4 landSteps 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.