S01E08 2026-06-13

S01E08 — 13-06-2026.

zo 0.5.0 — Road to Mobile.

0.4.0 stabilized the compiler. 0.5.0 spends that stability: one zo source now runs on the desktop, across Apple’s platforms (iOS, tvOS, visionOS, watchOS), and on the web, with fine-grained reactivity, real components, styling, and forms. The toy grows up.

one source, every platform

Targets collapse onto a single --target axis. The same program, no rewrite, picks where it runs.

zo run hello.zo                    # desktop, native window
zo run hello.zo --target ios       # iOS Simulator
zo run hello.zo --target visionos  # visionOS Simulator
zo run hello.zo --target tvos      # tvOS Simulator
zo run hello.zo --target watchos   # Apple Watch Simulator
zo run hello.zo --target web       # the browser
zo build hello.zo --target webview # a native webview .app

The web target is a backend: the compiler transpiles zo to a public/ bundle. The Apple targets render through native UIKit in the Simulator. On device, glass panels paint real Liquid Glass.

fine-grained reactivity

A write refreshes only what it drives. No runtime signals. The compiler builds the binding graph ahead of time, so when a mut changes, only the slots that read it repaint. The cost of an update is the size of the change, not the size of the view.

fun main() {
  mut items: []str = [];

  imu view: </> ::= <>
    <button @click={fn() => items.push("hi")}>add</button>
    <ul>{items.map(fn(item) => <li>{item}</li>)}</ul>
  </>;

  #render view;
}

Lists ride the same path: items.map(...) binds the rows to the view, keyed diffs move only the rows that moved, an appended item no longer rebuilds the whole list, and an in-place write, items[index] = v, patches a single row.

components

A zsx component is a function that returns markup. It takes props as parameters, and <name ... /> calls it.

fun greeting(name: str) -> </> {
  return <h1>hello, {name}!</h1>;
}

fun main() {
  imu page ::= <div>
    <greeting name="world" />
    <greeting name="zo" />
  </div>;

  #render page;
}

Components nest, carry their own per-instance state, splice children through <slot />, take callbacks as Fn props so events flow back out, and cross pack boundaries, with a guard that rejects a component that includes itself.

styling

zo-styler reads a scoped $: { } stylesheet with Tailwind-like shorthand, and the same rules reach every target. Interaction states are real: :hover, :active, :focus, :disabled.

$: {
  .btn {
    bg: #2563eb;
    c: white;
  }
  .btn:hover {
    bg: red;
  }
}

fun main() {
  imu page ::= <button class="btn">press me</button>;

  #render page;
}

Borders, shadows, text wrap, and caps fill in alongside container surfaces, class selectors, background images, and the material: glass opt-in.

forms

HTML-faithful form controls land: text inputs, checkbox, radio, and select. They bind to state through @input and @submit, and the event carries the control’s value.

load core::zsx::*;

fun main() {
  mut text: str = "";

  imu view: </> ::= <input
    value={text}
    @input={fn(event) => text = event.value}
  />;

  #render view;
}

naming-convention warnings

zo-checker now warns when a name strays from zo’s conventions: a function or variable that isn’t snake_case, a type that isn’t PascalCase. It is a warning, not an error. It nudges, it doesn’t block.

diagnostics and developer experience

Diagnostics gain a third audience. Alongside human and JSON, --format=xml emits a form built for agents. Color is TTY-aware, and you can suppress it. -q / --quiet drops the build banner, --help leads with examples, exit codes map to failure modes, and build status moves to stderr, the way Cargo does it, so a program’s own output stays clean on stdout.

An agent reads the same diagnostic as XML, one well-formed document, isomorphic to the JSON:

<diagnostics schema="1">
  <diagnostic id="immutable-variable" code="E0309" severity="error" phase="analyzer">
    <message>Cannot mutate immutable variable</message>
    <fixes>
      <fix kind="insert" file="main.zo" byte_start="37" byte_end="37">
        <text>mut </text>
        <description>Declare the variable as mutable with `mut`</description>
      </fix>
    </fixes>
  </diagnostic>
</diagnostics>

The document also carries the source <snippet> and the primary <span> (byte, line, and column), so an agent locates and applies the fix without parsing prose.

language and internals

One arrow: =:> is gone. The arr pack becomes array.