From 30e6a35314161dc6dd7d638fe08ad829467b4960 Mon Sep 17 00:00:00 2001 From: invisageable Date: Fri, 12 Jun 2026 10:38:56 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat(zo):=20conditional=20rendering=20?= =?UTF-8?q?=E2=80=94=20structure=20swaps=20on=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 161 ++++++++++++-- README.md | 30 +-- .../src/content/initiation/en/001-prologue.md | 10 +- .../src/content/initiation/fr/000-preface.md | 20 -- .../src/content/initiation/fr/001-prologue.md | 16 +- .../compiler/zo-codegen-web/src/reactive.rs | 2 + crates/compiler/zo-dce/src/tests.rs | 2 + crates/compiler/zo-driver/src/cmd/run.rs | 125 ++++++++++- crates/compiler/zo-executor/src/executor.rs | 201 +++++++++++++++++- .../zo-executor/src/tests/templates.rs | 149 +++++++++++++ crates/compiler/zo-liveness/src/tests.rs | 2 + crates/compiler/zo-runtime-native/Cargo.toml | 3 + .../zo-runtime-native/tests/click_dispatch.rs | 117 ++++++++++ .../zo-runtime-render/src/reactive.rs | 194 ++++++++++++++++- crates/compiler/zo-sir/src/lib.rs | 6 +- crates/compiler/zo-sir/src/sir.rs | 27 +++ .../zo-tests/templating/zsx_conditional.zo | 18 ++ crates/compiler/zo-token/src/token.rs | 20 +- llms.txt | 32 +-- 19 files changed, 1006 insertions(+), 129 deletions(-) delete mode 100644 apps/site/src/content/initiation/fr/000-preface.md create mode 100644 crates/compiler/zo-runtime-native/tests/click_dispatch.rs create mode 100644 crates/compiler/zo-tests/templating/zsx_conditional.zo diff --git a/Cargo.lock b/Cargo.lock index 530bc9c2..d15931ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,6 +1016,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1509,6 +1519,19 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "dify" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc" +dependencies = [ + "anyhow", + "colored", + "getopts", + "image", + "rayon", +] + [[package]] name = "digest" version = "0.10.7" @@ -1708,12 +1731,12 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137c0ce4ce4152ff7e223a7ce22ee1057cdff61fce0a45c32459c3ccec64868d" +checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e" dependencies = [ "bytemuck", - "emath 0.34.1", + "emath 0.34.3", "serde", ] @@ -1758,14 +1781,14 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34aaf627da598dfadd64b0fee6101d22e9c451d1e5348157312720b7f459f0f" +checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" dependencies = [ "accesskit", "ahash", "bitflags 2.10.0", - "emath 0.34.1", + "emath 0.34.3", "epaint", "log", "nohash-hasher", @@ -1844,6 +1867,22 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_kittest" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb65436bc6f214ecc8ef4d9056a9344f92e70e22c162dbc3d515913df0fd3c5" +dependencies = [ + "dify", + "egui", + "image", + "kittest", + "open", + "serde", + "tempfile", + "toml 1.1.2+spec-1.1.0", +] + [[package]] name = "either" version = "1.15.0" @@ -1858,9 +1897,9 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" [[package]] name = "emath" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" +checksum = "b53f0d33a479321da6b0caa71366c9f67e8a2c149762d90bdc0d16e601ee8ecb" dependencies = [ "bytemuck", "serde", @@ -1956,14 +1995,14 @@ dependencies = [ [[package]] name = "epaint" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3017dd67f147a697ee0c8484fb568fd9553e2a0c114be5020dbbc11962841" +checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" dependencies = [ "ahash", "bytemuck", "ecolor", - "emath 0.34.1", + "emath 0.34.3", "epaint_default_fonts", "font-types", "log", @@ -1979,9 +2018,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3b85a2bb775a3ab02d077a65cc31575c11b2584581913253cc11ce49f48bba" +checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da" [[package]] name = "equivalent" @@ -3172,6 +3211,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9c13ae9d91148fcb4aab6654c4c2a7d02a15395ea9e23f65170f175f8b269ce" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3320,6 +3378,16 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kittest" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" +dependencies = [ + "accesskit", + "accesskit_consumer", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -4271,6 +4339,17 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -4369,6 +4448,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pdfium-render" version = "0.9.0" @@ -5334,6 +5419,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "servo_arc" version = "0.4.3" @@ -5749,7 +5843,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.2", "version-compare", ] @@ -6008,11 +6102,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.3", "toml_edit 0.20.2", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -6031,6 +6138,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -6050,7 +6166,7 @@ checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -6069,11 +6185,11 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.3", ] [[package]] @@ -7534,6 +7650,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -8475,6 +8597,7 @@ name = "zo-runtime-native" version = "0.4.0" dependencies = [ "eframe", + "egui_kittest", "image", "libloading 0.9.0", "rustc-hash 2.1.2", diff --git a/README.md b/README.md index c6f9e1ac..bd18e587 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,5 @@ # zo. - ``` - [zo] lines processed (including blank lines and comments) — 499998. - │ - ├── "Why accept slow compilers? Just make them faster." — Jonathan Blow - │ - ├── ✓ [zo@front-end] time — 301.591 ms (42.5%). - │ ├── ⏺ [zo@tokenizer] time — 57.064 ms (8.0%). - │ │ └── ⏺ processed — 2399990 tokens. - │ ├── ⏺ [zo@parser] time — 36.126 ms (5.1%). - │ │ └── ⏺ parsed — 2350646 nodes. - │ └── ⏺ [zo@analyzer] time — 208.401 ms (29.3%). - │ └── ⏺ annotated — 349996 nodes. - ├── ✓ [zo@back-end] time — 408.614 ms (57.5%). - │ ├── ⏺ [zo@codegen:arm64-apple-darwin] time — 391.537 ms (55.1%). - │ │ └── ⏺ generated — 1 artifacts. - │ └── ⏺ [zo@linker] time — 17.076 ms (2.4%). - │ └── ⏺ linked — 1 files. - └── ✓ [zo@total] time — 710.205 ms (100.0%). - - ⚡ speed: 704.02K LoC/s. - ``` - [![CI](https://github.com/invisageable/zo/workflows/CI/badge.svg)](https://github.com/invisageable/zo/actions) [![Discord](https://img.shields.io/badge/discord-compilords-7289DA?logo=discord)](https://discord.gg/JaNc4Nk5xw) --- @@ -30,7 +8,7 @@ THE AiM OF THE PROJECT iS TO ENHANCE THE DEVELOPER EXPERiENCE, MAKiNG iT SEAMLESS TO BUiLD SOFTWARE THAT REFLECTS YOUR CREATiViTY. WE FOCUS ON DETAiLS THAT MATTER, WHERE TRANSFORMiNG YOUR THOUGHTS iNTO PROGRAMS iS NOT JUST EASY, BUT ENJOYABLE. -zo (pronounced `/zuː/` just like "zoo") iS A SiMPLE, LiGHTWEiGHT, CROSS-PLATFORM, GENERAL-PURPOSE PROGRAMMiNG LANGUAGE. TO SHiP, RUN AND BUiLD TYPED-SAFE DESKTOP, MOBiLE AND WEB APPLiCATiONS WiTH ONE CODE SOURCE. THE CORE LiBRARY iNCLUDES SEVERAL PACKAGES. PROViDERS ARE AVAiLABLE TO EXPAND THE LANGUAGE's CAPABiLiTiES. +zo (pronounced `/zuː/` just like "zoo") iS A SiMPLE, LiGHTWEiGHT, CROSS-PLATFORM, GENERAL-PURPOSE PROGRAMMiNG LANGUAGE. TO SHiP, RUN AND BUiLD TYPE-SAFE DESKTOP, MOBiLE AND WEB APPLiCATiONS WiTH ONE CODE SOURCE. THE CORE LiBRARY iNCLUDES SEVERAL PACKAGES. PROViDERS ARE AVAiLABLE TO EXPAND THE LANGUAGE's CAPABiLiTiES. **JOiN THE DEVOLUTiON.** @@ -38,7 +16,7 @@ zo (pronounced `/zuː/` just like "zoo") iS A SiMPLE, LiGHTWEiGHT, CROSS-PLATFOR ## usage. -THiS PROGRAM DECLARES A COMPONENT (`counter`) COMPOSED BY TWO BUTTONS (` + {when open ? :

closed

} + ; + + #render page; +}"#, + |sir| { + let (commands, bindings) = sir + .iter() + .filter_map(|i| match i { + Insn::Template { + commands, bindings, .. + } => Some((commands, bindings)), + _ => None, + }) + .next_back() + .expect("page template"); + + assert_eq!(bindings.conditional.len(), 1, "one conditional region"); + + let cond = &bindings.conditional[0]; + + let blob_text = |cmds: &[UiCommand]| -> String { + cmds + .iter() + .filter_map(|c| match c { + UiCommand::Text(t) => Some(t.as_str()), + _ => None, + }) + .collect() + }; + + assert!( + blob_text(&cond.on_true).contains("menu items"), + "true branch compiled: {:?}", + cond.on_true + ); + assert!( + blob_text(&cond.on_false).contains("closed"), + "false branch compiled: {:?}", + cond.on_false + ); + + // `open` starts false → the false branch is inline. + assert_eq!(cond.len, cond.on_false.len()); + + let region_text: String = commands[cond.cmd_idx..cond.cmd_idx + cond.len] + .iter() + .filter_map(|c| match c { + UiCommand::Text(t) => Some(t.as_str()), + _ => None, + }) + .collect(); + + assert!( + region_text.contains("closed"), + "inline region is the initial branch: {region_text}" + ); + + // The branch blobs never leak into the SIR stream. + assert!( + !blob_text(commands).contains("menu items"), + "inactive branch must not be inline" + ); + }, + ); +} + +#[test] +fn tier2_conditional_missing_else_is_an_error() { + // `when` always takes both branches — `?` without `:` is an + // error, never an implicit empty fragment. + assert_execution_error( + r#" +fun main() { + mut open := true; + + imu page ::=
+ + {when open ? } +
; + + #render page; +}"#, + ErrorKind::ExpectedExpression, + ); +} + +#[test] +fn compile_time_conditional_keeps_the_tier1_fold() { + // `imu` condition: no runtime swap — the general path folds it + // and no conditional binding is recorded. + assert_sir_structure( + r#" +fun main() { + imu fancy := true; + + imu page ::=
+ {when fancy ? bold :

plain

} +
; + + #render page; +}"#, + |sir| { + let bindings = sir + .iter() + .filter_map(|i| match i { + Insn::Template { bindings, .. } => Some(bindings), + _ => None, + }) + .next_back() + .expect("page template"); + + assert!( + bindings.conditional.is_empty(), + "compile-time conditions stay tier 1" + ); + }, + ); +} + +#[test] +fn tier2_conditional_rejects_non_bool_condition() { + // Conditions are `bool` — an int flag is a type error, never a + // truthy coercion. + assert_execution_error( + r#" +fun main() { + mut open := 0; + + imu page ::=
+ + {when open ? :

closed

} +
; + + #render page; +}"#, + ErrorKind::TypeMismatch, + ); +} diff --git a/crates/compiler/zo-liveness/src/tests.rs b/crates/compiler/zo-liveness/src/tests.rs index 591057e1..b926c26d 100644 --- a/crates/compiler/zo-liveness/src/tests.rs +++ b/crates/compiler/zo-liveness/src/tests.rs @@ -17,6 +17,7 @@ fn array_store_uses_three_values() { index: ValueId(1), value: ValueId(2), ty_id: TyId(8), + owner: None, }; let mut uses = Vec::new(); @@ -71,6 +72,7 @@ fn array_store_produces_no_value() { index: ValueId(1), value: ValueId(2), ty_id: TyId(8), + owner: None, }]; let ids = compute_value_ids(&insns); diff --git a/crates/compiler/zo-runtime-native/Cargo.toml b/crates/compiler/zo-runtime-native/Cargo.toml index a2d2aa60..ee0ef3e5 100644 --- a/crates/compiler/zo-runtime-native/Cargo.toml +++ b/crates/compiler/zo-runtime-native/Cargo.toml @@ -28,3 +28,6 @@ libloading = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } thin-vec = { workspace = true } + +[dev-dependencies] +egui_kittest = { workspace = true } diff --git a/crates/compiler/zo-runtime-native/tests/click_dispatch.rs b/crates/compiler/zo-runtime-native/tests/click_dispatch.rs new file mode 100644 index 00000000..bd3dada0 --- /dev/null +++ b/crates/compiler/zo-runtime-native/tests/click_dispatch.rs @@ -0,0 +1,117 @@ +//! ```sh +//! cargo test -p zo-runtime-native --test click_dispatch +//! ``` +//! +//! Headless click-dispatch contract for the egui renderer: no +//! event fires without input, and a simulated click on a button +//! fires exactly its handler. Guards the tier-2 conditional +//! programs, whose buttons looked dead in manual testing while +//! probes showed spontaneous startup dispatches. + +use zo_runtime_native::renderer::Renderer; +use zo_runtime_render::render::Render; +use zo_ui_protocol::{Attr, ElementTag, EventKind, PropValue, UiCommand}; + +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; + +fn button(id: &str, label: &str, handler: &str) -> Vec { + vec![ + UiCommand::Element { + tag: ElementTag::Button, + attrs: vec![Attr::Prop { + name: "data-id".into(), + value: PropValue::Str(id.into()), + }], + self_closing: false, + }, + UiCommand::Text(label.into()), + UiCommand::EndElement, + UiCommand::Event { + widget_id: id.into(), + event_kind: EventKind::Click, + handler: handler.into(), + }, + ] +} + +fn conditional_stream() -> Vec { + let mut cmds = vec![UiCommand::Element { + tag: ElementTag::Div, + attrs: vec![], + self_closing: false, + }]; + + cmds.extend(button("0", "menu", "__closure_0")); + cmds.extend(button("1", "finish", "__closure_1")); + cmds.push(UiCommand::Element { + tag: ElementTag::P, + attrs: vec![], + self_closing: false, + }); + cmds.push(UiCommand::Text("closed".into())); + cmds.push(UiCommand::EndElement); + cmds.push(UiCommand::EndElement); + + cmds +} + +#[test] +fn no_events_fire_without_input() { + let mut renderer = Renderer::new(); + let commands = conditional_stream(); + + let mut harness = Harness::new_ui(move |ui| { + renderer.render(&commands); + renderer.render_with_ui(ui); + + let pending = renderer.take_pending_events(); + + assert!( + pending.is_empty(), + "no input was given, yet events fired: {pending:?}" + ); + }); + + // Several frames: startup, layout settle, idle. + for _ in 0..5 { + harness.run(); + } +} + +#[test] +fn clicking_a_button_fires_exactly_its_event() { + use std::sync::{Arc, Mutex}; + + let fired: Arc>> = + Arc::new(Mutex::new(Vec::new())); + let fired_inner = Arc::clone(&fired); + let mut renderer = Renderer::new(); + let commands = conditional_stream(); + + let mut harness = Harness::new_ui(move |ui| { + renderer.render(&commands); + renderer.render_with_ui(ui); + + let pending = renderer.take_pending_events(); + + fired_inner + .lock() + .unwrap() + .extend(pending.into_iter().map(|(id, kind, _)| (id, kind))); + }); + + harness.run(); + + // Click the "menu" button by its accessibility label. + harness.get_by_label("menu").click(); + harness.run(); + + let events = fired.lock().unwrap().clone(); + + assert_eq!( + events, + vec![(0, EventKind::Click)], + "one click on `menu` fires exactly widget 0's Click" + ); +} diff --git a/crates/compiler/zo-runtime-render/src/reactive.rs b/crates/compiler/zo-runtime-render/src/reactive.rs index 2e3bce38..e609145c 100644 --- a/crates/compiler/zo-runtime-render/src/reactive.rs +++ b/crates/compiler/zo-runtime-render/src/reactive.rs @@ -17,7 +17,7 @@ //! appliers, shared by the `aot` and `zo run` paths. use crate::evaluator::HandlerEvaluator; -use crate::render::StateCell; +use crate::render::{StateCell, StateValue}; use zo_interner::Symbol; use zo_sir::{Insn, ListItemCmd}; @@ -518,6 +518,93 @@ pub fn apply_list_bindings( } } +/// One tier-2 conditional, its variable resolved to a state-cell +/// slot — the driver builds these from +/// `TemplateBindings::conditional` + the slot table. +#[derive(Clone)] +pub struct ResolvedConditional { + /// Region start in the BASE template commands (the inline, + /// initially-active branch). + pub cmd_idx: usize, + /// The inline branch's length in the base stream. + pub len: usize, + /// The condition's state-cell slot; truthiness is non-zero / + /// non-empty. + pub slot: usize, + /// Branch blobs with their reactive text interps as + /// `(branch_relative_idx, slot)` pairs substituted at splice. + pub on_true: Vec, + pub true_text: Vec<(usize, usize)>, + pub on_false: Vec, + pub false_text: Vec<(usize, usize)>, +} + +/// Splice each conditional's ACTIVE branch over its base region, +/// substituting the branch's reactive text from the cells. +/// Returns `(base_cmd_idx, delta)` per binding so the caller can +/// shift later bindings that target commands past a region whose +/// length changed. Entries must be in ascending `cmd_idx` order +/// (the executor records them in walk order). +pub fn apply_conditional_bindings( + new_cmds: &mut Vec, + conds: &[ResolvedConditional], + cells: &[StateCell], +) -> Vec<(usize, isize)> { + let mut deltas = Vec::with_capacity(conds.len()); + let mut offset: isize = 0; + + for cond in conds { + let target = (cond.cmd_idx as isize + offset) as usize; + + // The executor type-checked the condition as `bool`; any + // other variant here is a wiring fault and reads false rather + // than inventing truthiness. + let truthy = cells + .get(cond.slot) + .is_some_and(|cell| matches!(cell.get(), StateValue::Bool(true))); + + let (blob, texts) = if truthy { + (&cond.on_true, &cond.true_text) + } else { + (&cond.on_false, &cond.false_text) + }; + + let mut rendered = blob.clone(); + + for (branch_idx, slot) in texts { + if let (Some(UiCommand::Text(out)), Some(cell)) = + (rendered.get_mut(*branch_idx), cells.get(*slot)) + { + *out = cell.get().display(); + } + } + + let new_len = rendered.len(); + + new_cmds.splice(target..target + cond.len, rendered); + deltas.push((cond.cmd_idx, new_len as isize - cond.len as isize)); + offset += new_len as isize - cond.len as isize; + } + + deltas +} + +/// Shift a base-stream command index by every conditional delta +/// whose region precedes it — bindings recorded before the +/// conditional keep their index; bindings after move with the +/// swap. +pub fn shift_for_deltas(cmd_idx: usize, deltas: &[(usize, isize)]) -> usize { + let mut shifted = cmd_idx as isize; + + for (at, delta) in deltas { + if cmd_idx > *at { + shifted += delta; + } + } + + shifted.max(0) as usize +} + /// Re-run each computed binding's closure over the current state /// cells and stamp the returned value (rendered via `display()`) /// into its bound `UiCommand::Text` slot. Shared by the @@ -1076,3 +1163,108 @@ mod tests { assert_eq!(out.to_vec(), vec![0, 2]); } } + +#[cfg(test)] +mod conditional_tests { + use super::*; + + use crate::render::StateValue; + + use zo_ui_protocol::ElementTag; + + fn text(s: &str) -> UiCommand { + UiCommand::Text(s.into()) + } + + fn el(tag: ElementTag) -> UiCommand { + UiCommand::Element { + tag, + attrs: vec![], + self_closing: false, + } + } + + fn cond_fixture() -> ResolvedConditional { + ResolvedConditional { + cmd_idx: 1, + len: 3, + slot: 0, + on_true: vec![el(ElementTag::Ul), text("menu"), UiCommand::EndElement], + true_text: vec![], + on_false: vec![el(ElementTag::P), text("closed"), UiCommand::EndElement], + false_text: vec![], + } + } + + /// Base: `
[false branch inline]
`. + fn base_stream() -> Vec { + vec![ + el(ElementTag::Div), + el(ElementTag::P), + text("closed"), + UiCommand::EndElement, + UiCommand::EndElement, + ] + } + + #[test] + fn truthy_cell_splices_the_true_branch() { + let mut cmds = base_stream(); + let cells = vec![StateCell::new(StateValue::Bool(true))]; + + let deltas = + apply_conditional_bindings(&mut cmds, &[cond_fixture()], &cells); + + assert!(matches!(&cmds[1], UiCommand::Element { tag, .. } + if *tag == ElementTag::Ul)); + assert!(matches!(&cmds[2], UiCommand::Text(t) if t == "menu")); + assert_eq!(deltas, vec![(1, 0)], "equal-length swap: zero delta"); + } + + #[test] + fn falsy_cell_keeps_the_false_branch() { + let mut cmds = base_stream(); + let cells = vec![StateCell::new(StateValue::Bool(false))]; + + apply_conditional_bindings(&mut cmds, &[cond_fixture()], &cells); + + assert!(matches!(&cmds[2], UiCommand::Text(t) if t == "closed")); + } + + #[test] + fn else_less_empty_branch_shrinks_and_reports_delta() { + let mut cmds = base_stream(); + let cells = vec![StateCell::new(StateValue::Bool(false))]; + let cond = ResolvedConditional { + on_false: Vec::new(), + ..cond_fixture() + }; + + let deltas = apply_conditional_bindings(&mut cmds, &[cond], &cells); + + assert_eq!(cmds.len(), 2, "region removed entirely"); + assert_eq!(deltas, vec![(1, -3)]); + assert_eq!(shift_for_deltas(4, &deltas), 1, "downstream shifts back"); + assert_eq!(shift_for_deltas(1, &deltas), 1, "region start holds"); + } + + #[test] + fn branch_text_substitutes_from_cells() { + let mut cmds = base_stream(); + let cells = vec![ + StateCell::new(StateValue::Bool(true)), + StateCell::new(StateValue::Str("ada".into())), + ]; + let cond = ResolvedConditional { + true_text: vec![(1, 1)], + ..cond_fixture() + }; + + apply_conditional_bindings(&mut cmds, &[cond], &cells); + + assert!( + matches!(&cmds[2], UiCommand::Text(t) if t == "ada"), + "branch interp reads the live cell" + ); + } +} diff --git a/crates/compiler/zo-sir/src/lib.rs b/crates/compiler/zo-sir/src/lib.rs index f6c8fd6e..5690740a 100644 --- a/crates/compiler/zo-sir/src/lib.rs +++ b/crates/compiler/zo-sir/src/lib.rs @@ -2,8 +2,8 @@ mod sir; pub mod validator; pub use sir::{ - BinOp, ComputedBinding, ImportKind, Insn, LinkEntry, LinkPath, - LinkResolution, LinkSpec, ListBinding, ListItemCmd, LoadSource, NurseryKind, - Sir, SpawnKind, TemplateBindings, UnOp, + BinOp, ComputedBinding, ConditionalBinding, ImportKind, Insn, LinkEntry, + LinkPath, LinkResolution, LinkSpec, ListBinding, ListItemCmd, LoadSource, + NurseryKind, Sir, SpawnKind, TemplateBindings, UnOp, }; pub use validator::{ValidationReport, Violation, ViolationKind, validate}; diff --git a/crates/compiler/zo-sir/src/sir.rs b/crates/compiler/zo-sir/src/sir.rs index 58a885db..1f9d9c6f 100644 --- a/crates/compiler/zo-sir/src/sir.rs +++ b/crates/compiler/zo-sir/src/sir.rs @@ -40,6 +40,33 @@ pub struct TemplateBindings { /// with the rendered batch. Used for /// `{arr.map(fn(t) => )}`. pub list: Vec<(usize, ListBinding)>, + /// Tier-2 conditionals: each entry swaps a command region + /// between two compiled branches when its reactive variable + /// changes (`{when open ? : }`). Both + /// branches compile; the initially-active one is inline in the + /// template commands at `[cmd_idx, cmd_idx + len)`. + pub conditional: Vec, +} + +/// One tier-2 conditional region — see +/// [`TemplateBindings::conditional`]. +#[derive(Clone, Debug, PartialEq)] +pub struct ConditionalBinding { + /// Region start in the template's command stream. + pub cmd_idx: usize, + /// Length of the INITIALLY-ACTIVE branch (the region inline in + /// the template commands). + pub len: usize, + /// The reactive variable the condition reads; truthiness is + /// non-zero. + pub var: Symbol, + /// The true branch's commands, with its reactive text interps + /// as `(branch_relative_idx, var)` pairs substituted at splice. + pub on_true: Vec, + pub true_text: Vec<(usize, Symbol)>, + /// The false branch (an empty fragment for else-less forms). + pub on_false: Vec, + pub false_text: Vec<(usize, Symbol)>, } /// Side-channel for a compound `{expr}` template diff --git a/crates/compiler/zo-tests/templating/zsx_conditional.zo b/crates/compiler/zo-tests/templating/zsx_conditional.zo new file mode 100644 index 00000000..de1e5396 --- /dev/null +++ b/crates/compiler/zo-tests/templating/zsx_conditional.zo @@ -0,0 +1,18 @@ +-- test-run-pass: tier-2 conditional rendering. `open` and `done` +-- are reactive `bool` state — conditions are typed, never coerced +-- — so both branches compile and the runtime swaps the region +-- when a handler flips the flag. + +fun main() { + mut open := false; + mut done := false; + + imu page ::=
+ + + {when open ?
    menu items
:

closed

} + {when done ? finished : pending} +
; + + #render page; +} diff --git a/crates/compiler/zo-token/src/token.rs b/crates/compiler/zo-token/src/token.rs index 5e460626..852a8b57 100644 --- a/crates/compiler/zo-token/src/token.rs +++ b/crates/compiler/zo-token/src/token.rs @@ -256,16 +256,24 @@ impl Token { /// /// @note — deliberately narrower than `is_regex_prefix`: /// only *value* positions — `return`, a block tail (after - /// `{` / `;`), a `match` arm (after `=>`). Assignment - /// operators are excluded by language design: `::=` is the - /// one and only template binding form, so `:=

` and - /// `=

` stay `Lt` and fail downstream as the invalid - /// programs they are. + /// `{` / `;`), a `match` arm or closure body (after `=>`), + /// and the two ternary positions (`when c ? : `; no + /// valid expression starts with `<`, and `` in type + /// position bypasses the door — its next byte is `/`). + /// Assignment operators are excluded by language design: + /// `::=` is the one and only template binding form, so + /// `:=

` and `=

` stay `Lt` and fail downstream as + /// the invalid programs they are. #[inline(always)] pub fn is_template_opener(&self) -> bool { matches!( self, - Self::Return | Self::LBrace | Self::Semicolon | Self::FatArrow + Self::Return + | Self::LBrace + | Self::Semicolon + | Self::FatArrow + | Self::Question + | Self::Colon ) } diff --git a/llms.txt b/llms.txt index dbad1001..2da01ba4 100644 --- a/llms.txt +++ b/llms.txt @@ -1,47 +1,19 @@ # zo -> Turn your thoughts into type-safe software and Ui instantly. +> *Turn your thoughts into type-safe software and Ui instantly.* -A language to ship, run, and build native and web applications optimized for speed. zo is a feature-rich ecosystem focus on high-performance, simplicity and data-oriented. +zo (pronounced /zuː/ just like "zoo") iS A SiMPLE, LiGHTWEiGHT, CROSS-PLATFORM, GENERAL-PURPOSE PROGRAMMiNG LANGUAGE. TO SHiP, RUN AND BUiLD TYPED-SAFE DESKTOP, MOBiLE AND WEB APPLiCATiONS WiTH ONE CODE SOURCE. THE CORE LiBRARY iNCLUDES SEVERAL PACKAGES. PROViDERS ARE AVAiLABLE TO EXPAND THE LANGUAGE's CAPABiLiTiES. ## Architecture - [CLAUDE.md](CLAUDE.md): The Three Prime Directives (velocity, pragmatism, insight) + pipeline overview - [Grammar (EBNF)](crates/compiler/zo-notes/public/grammar/zo.ebnf): Formal language grammar -## Compiler crates -- [zo-tokenizer](crates/compiler/zo-tokenizer): Zero-allocation tokenizer, pre-balanced delimiters -- [zo-parser](crates/compiler/zo-parser): Linear postorder parse tree, no recursion -- [zo-tree](crates/compiler/zo-tree): Parse tree data structure, indexed access -- [zo-analyzer](crates/compiler/zo-analyzer): Tree → SIR execution, type checking -- [zo-sir](crates/compiler/zo-sir): Semantic IR — typed, output of executing Tree -- [zo-ty](crates/compiler/zo-ty): Type representation -- [zo-ty-checker](crates/compiler/zo-ty-checker): Hindley-Milner type inference -- [zo-codegen-backend](crates/compiler/zo-codegen-backend): Codegen trait/abstraction layer -- [zo-codegen](crates/compiler/zo-codegen): Codegen orchestrator -- [zo-codegen-arm](crates/compiler/zo-codegen-arm): AArch64 backend -- [zo-codegen-clif](crates/compiler/zo-codegen-clif): Cranelift backend -- [zo-emitter](crates/compiler/zo-emitter): Machine code emission -- [zo-emitter-arm](crates/compiler/zo-emitter-arm): AArch64 instruction emission -- [zo-writer](crates/compiler/zo-writer): Binary file writer -- [zo-writer-macho](crates/compiler/zo-writer-macho): Mach-O object/binary writer -- [zo-linker](crates/compiler/zo-linker): Linking -- [zo-runtime](crates/compiler/zo-runtime): Scheduler, structured concurrency, TLS -- [zo-runtime-native](crates/compiler/zo-runtime-native): Native runtime support -- [zo-runtime-render](crates/compiler/zo-runtime-render): Render runtime support -- [zo-runtime-web](crates/compiler/zo-runtime-web): Web runtime support -- [zo-ui-protocol](crates/compiler/zo-ui-protocol): UI protocol bridging executor → render runtime (zsx commands) -- [zo-reporter](crates/compiler/zo-reporter): Diagnostic collection -- [zo-driver](crates/compiler/zo-driver): Orchestrates phases, owns the compile-to-run path -- [zo-benches](crates/compiler/zo-benches): Throughput benchmarks against clang, rustc -- [zo-test-runner](crates/compiler/zo-test-runner): Integration tests on compiled programs - ## Tools - [fret](crates/packager/fret): Zero-config package manager - [zo-vscode](crates/compiler/zo-vscode): VS Code language extension ## Apps - [Site](apps/site): Astro-based marketing/docs site (zo.compilords.house) -- [codelord](https://github.com/invisageable/codelord): Editor designed to disappear ## Performance targets - Tokenize + Parse: 10,000,000 LoC/s (Carbon: 8M) From 4d4508c67ff960b29a83fdcac48af115e4bd35f2 Mon Sep 17 00:00:00 2001 From: invisageable Date: Fri, 12 Jun 2026 19:45:45 +0200 Subject: [PATCH 2/5] fix(zo): conditionals brnches --- README.md | 2 +- .../src/content/initiation/en/001-prologue.md | 2 +- .../content/initiation/en/003-introduction.md | 2 +- .../src/content/initiation/en/099-epilogue.md | 5 + crates/compiler/zo-codegen-arm/src/codegen.rs | 142 +++++++++++- .../zo-notes/public/grammar/README.md | 3 + .../guidelines/{00-prologue.md => README.md} | 2 +- .../error-message-argument-structure.md | 0 crates/compiler/zo-notes/public/zo.md | 201 +++++++++++++++++ crates/compiler/zo-profiler/src/quotes.rs | 2 +- crates/compiler/zo-runtime-ios/src/ffi.rs | 23 +- crates/compiler/zo-runtime-native/src/ffi.rs | 23 +- crates/compiler/zo-runtime-render/src/aot.rs | 206 ++++++++++++++++-- crates/compiler/zo-runtime-web/src/ffi.rs | 23 +- crates/compiler/zo-ui-protocol/src/codec.rs | 16 ++ crates/compiler/zo-ui-protocol/src/lib.rs | 4 +- .../zo-ui-protocol/src/ui_protocol.rs | 13 ++ 17 files changed, 608 insertions(+), 61 deletions(-) create mode 100644 crates/compiler/zo-notes/public/grammar/README.md rename crates/compiler/zo-notes/public/guidelines/{00-prologue.md => README.md} (99%) rename crates/compiler/zo-notes/public/{docs => spec}/error-message-argument-structure.md (100%) create mode 100644 crates/compiler/zo-notes/public/zo.md diff --git a/README.md b/README.md index bd18e587..2a11821e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ THE AiM OF THE PROJECT iS TO ENHANCE THE DEVELOPER EXPERiENCE, MAKiNG iT SEAMLESS TO BUiLD SOFTWARE THAT REFLECTS YOUR CREATiViTY. WE FOCUS ON DETAiLS THAT MATTER, WHERE TRANSFORMiNG YOUR THOUGHTS iNTO PROGRAMS iS NOT JUST EASY, BUT ENJOYABLE. -zo (pronounced `/zuː/` just like "zoo") iS A SiMPLE, LiGHTWEiGHT, CROSS-PLATFORM, GENERAL-PURPOSE PROGRAMMiNG LANGUAGE. TO SHiP, RUN AND BUiLD TYPE-SAFE DESKTOP, MOBiLE AND WEB APPLiCATiONS WiTH ONE CODE SOURCE. THE CORE LiBRARY iNCLUDES SEVERAL PACKAGES. PROViDERS ARE AVAiLABLE TO EXPAND THE LANGUAGE's CAPABiLiTiES. +zo (pronounced `/zuː/` just like "zoo") iS A PRACTiCAL, LiGHTWEiGHT, CROSS-PLATFORM, GENERAL-PURPOSE PROGRAMMiNG LANGUAGE. TO SHiP, RUN AND BUiLD TYPE-SAFE DESKTOP, MOBiLE AND WEB APPLiCATiONS WiTH ONE CODE SOURCE. THE CORE LiBRARY iNCLUDES SEVERAL PACKAGES. PROViDERS ARE AVAiLABLE TO EXPAND THE LANGUAGE's CAPABiLiTiES. **JOiN THE DEVOLUTiON.** diff --git a/apps/site/src/content/initiation/en/001-prologue.md b/apps/site/src/content/initiation/en/001-prologue.md index 75939bb7..1d61adea 100644 --- a/apps/site/src/content/initiation/en/001-prologue.md +++ b/apps/site/src/content/initiation/en/001-prologue.md @@ -5,7 +5,7 @@ title: Prologue # prologue -[tsh-tsh] +[tsh-tsh] This initiation is for the `zo` programming language. What are we waiting for to improve our developer experience? Why does having to wait several seconds, or even minutes, to get feedback on the correctness of our program bother absolutely no one? diff --git a/apps/site/src/content/initiation/en/003-introduction.md b/apps/site/src/content/initiation/en/003-introduction.md index 4677a530..2594e43f 100644 --- a/apps/site/src/content/initiation/en/003-introduction.md +++ b/apps/site/src/content/initiation/en/003-introduction.md @@ -29,7 +29,7 @@ Every lesson delivers a high-fidelity snapshot of a functional zo program. Pay c Every executable compilation unit inside zo must expose an explicit, non-colored entry block called `main`: ```zo - -- Wassup?! I'm `main` a function. + -- Wassup?! I'm `main`, a function. -- Use me as a entry point with `fun` keyword. fun main() { -- This program does nothing... yet. diff --git a/apps/site/src/content/initiation/en/099-epilogue.md b/apps/site/src/content/initiation/en/099-epilogue.md index c903399b..7a17968a 100644 --- a/apps/site/src/content/initiation/en/099-epilogue.md +++ b/apps/site/src/content/initiation/en/099-epilogue.md @@ -1,2 +1,7 @@ # epilogue +zo is inherit concepts from rust-prehistory (thanks to graydon hoare for the hard work), imba for its simplicity, e4x for its advanced xml extension, cyclone, erlang & go for their concurrent system. zo do not reinvent the wheel, it just applies what already exist and tries to do it in a correct manner. + +You've finished the initiation, you're now ready to build something that matter for you. Even if you don't use zo again, we hope that you've appreciated the journey and whish all the best in your developer experience. + +TRiLU! diff --git a/crates/compiler/zo-codegen-arm/src/codegen.rs b/crates/compiler/zo-codegen-arm/src/codegen.rs index 5d445f76..cf7d3b01 100644 --- a/crates/compiler/zo-codegen-arm/src/codegen.rs +++ b/crates/compiler/zo-codegen-arm/src/codegen.rs @@ -249,6 +249,23 @@ const RECIPE_BLOB_SYM_BASE: u32 = 0xD000_0000; /// `ZoRuntimeContext`'s `ListBindingAbi` array. `recipe_sym` keys /// the postcard `Vec` item recipe in `template_data` /// (resolved to an address by the `string_fixups` machinery). +/// One tier-2 conditional region, ready to emit into the +/// `ZoRuntimeContext`'s `ConditionalBindingAbi` array. +/// `payload_sym` keys the postcard `ConditionalPayload` blob in +/// `template_data`. +struct ConditionalBindingEntry { + /// Region start of the inline (initially-active) branch. + cmd_idx: u32, + /// Inline branch length. + len: u32, + /// Reactive `bool` slot the condition reads. + slot: u32, + /// Symbol of the embedded payload blob. + payload_sym: Symbol, + /// Byte length of the payload blob. + payload_len: u32, +} + struct ListBindingEntry { /// Index of the placeholder `Text` command the rendered list /// items replace. @@ -443,6 +460,9 @@ pub struct ARM64Gen<'a> { /// `ZoRuntimeContext`'s `ListBindingAbi` array at `#render`. /// Built by the same pre-pass that populates `reactive_slots`. template_list_bindings: HashMap>, + /// Per-template tier-2 conditional regions, emitted into the + /// `ZoRuntimeContext` at `#render`. Built by the same pre-pass. + template_conditional_bindings: HashMap>, /// Per-template attribute bindings — `(cmd_idx, attr_idx, /// slot, is_str)` — to emit into the `ZoRuntimeContext`'s /// `AttrBindingAbi` array. The runtime re-applies each on @@ -906,6 +926,7 @@ impl<'a> ARM64Gen<'a> { template_text_bindings: HashMap::default(), reactive_arr_slots: HashSet::default(), template_list_bindings: HashMap::default(), + template_conditional_bindings: HashMap::default(), template_attr_bindings: HashMap::default(), next_recipe_blob: 0, fns_needing_calls: HashSet::default(), @@ -2100,6 +2121,7 @@ impl<'a> ARM64Gen<'a> { self.template_text_bindings.clear(); self.reactive_arr_slots.clear(); self.template_list_bindings.clear(); + self.template_conditional_bindings.clear(); self.template_attr_bindings.clear(); let mut sym_first_store_ty: HashMap = HashMap::default(); @@ -2206,6 +2228,58 @@ impl<'a> ARM64Gen<'a> { if !list_entries.is_empty() { self.template_list_bindings.insert(*id, list_entries); } + + // Tier-2 conditionals: resolve the condition + branch-text + // symbols to slots, embed both branch blobs as one postcard + // `ConditionalPayload`, and record the region for the + // context emission. + let mut conditional_entries = + Vec::with_capacity(bindings.conditional.len()); + + for cond in &bindings.conditional { + let slot = self.reactive_slot_for(cond.var); + + let resolve_texts = |this: &mut Self, texts: &[(usize, Symbol)]| { + texts + .iter() + .map(|(idx, sym)| { + let text_slot = this.reactive_slot_for(*sym); + let is_str = sym_first_store_ty + .get(sym) + .is_some_and(|t| t.0 == STR_TYPE_ID); + + (*idx as u32, text_slot, is_str) + }) + .collect::>() + }; + + let payload = zo_ui_protocol::ConditionalPayload { + on_true: cond.on_true.clone(), + true_text: resolve_texts(self, &cond.true_text), + on_false: cond.on_false.clone(), + false_text: resolve_texts(self, &cond.false_text), + }; + let payload_bytes = codec::encode_payload(&payload).unwrap_or_default(); + let payload_len = payload_bytes.len() as u32; + let payload_sym = Symbol(RECIPE_BLOB_SYM_BASE + self.next_recipe_blob); + + self.next_recipe_blob += 1; + self.template_data.push((payload_sym, payload_bytes)); + + conditional_entries.push(ConditionalBindingEntry { + cmd_idx: cond.cmd_idx as u32, + len: cond.len as u32, + slot, + payload_sym, + payload_len, + }); + } + + if !conditional_entries.is_empty() { + self + .template_conditional_bindings + .insert(*id, conditional_entries); + } } // Pre-pass companion: find every function whose body @@ -8995,6 +9069,19 @@ impl<'a> ARM64Gen<'a> { .cloned() .unwrap_or_default(); let attr_count = attr_bindings.len(); + // (cmd_idx, len, slot, payload_sym, payload_len) per + // conditional region. + let conditional_bindings: Vec<(u32, u32, u32, Symbol, u32)> = self + .template_conditional_bindings + .get(&value) + .map(|entries| { + entries + .iter() + .map(|e| (e.cmd_idx, e.len, e.slot, e.payload_sym, e.payload_len)) + .collect() + }) + .unwrap_or_default(); + let conditional_count = conditional_bindings.len(); // Stack layout (mirrors `ZoRuntimeContext` in // `zo-runtime-render::aot`): @@ -9007,25 +9094,34 @@ impl<'a> ARM64Gen<'a> { // [sp + 48..56] list_bindings_count // [sp + 56..64] attr_bindings_ptr // [sp + 64..72] attr_bindings_count - // [sp + 72 .. +16*T] TextBinding[T] — 16B each: + // [sp + 72..80] conditional_bindings_ptr + // [sp + 80..88] conditional_bindings_count + // [sp + 88 .. +16*T] TextBinding[T] — 16B each: // cmd_idx u32 @0, slot_id u32 @4, is_str u32 @8, // _pad u32 @12. // [.. +24*L] ListBindingAbi[L] — 24B each: cmd_idx u32 @0, // items_slot u32 @4, recipe_ptr @8, recipe_len @16. // [.. +16*A] AttrBindingAbi[A] — 16B each: cmd_idx u32 @0, // attr_idx u32 @4, slot u32 @8, is_str u32 @12. - const CTX_BYTES: i16 = 72; + // [.. +32*C] ConditionalBindingAbi[C] — 32B each: + // cmd_idx u32 @0, len u32 @4, slot u32 @8, _pad @12, + // payload_ptr @16, payload_len @24. + const CTX_BYTES: i16 = 88; const TEXT_BASE: i16 = CTX_BYTES; const TEXT_STRIDE: i16 = 16; const LIST_STRIDE: i16 = 24; const ATTR_STRIDE: i16 = 16; + const CONDITIONAL_STRIDE: i16 = 32; let text_bytes = (bindings_count as i16) * TEXT_STRIDE; let list_base = TEXT_BASE + text_bytes; let list_bytes = (list_count as i16) * LIST_STRIDE; let attr_base = list_base + list_bytes; let attr_bytes = (attr_count as i16) * ATTR_STRIDE; - let total = CTX_BYTES + text_bytes + list_bytes + attr_bytes; + let conditional_base = attr_base + attr_bytes; + let conditional_bytes = (conditional_count as i16) * CONDITIONAL_STRIDE; + let total = + CTX_BYTES + text_bytes + list_bytes + attr_bytes + conditional_bytes; // Align up to 16; AArch64 needs sp 16-byte aligned. let stack_reserve = ((total + 15) & !15) as u16; @@ -9157,6 +9253,46 @@ impl<'a> ARM64Gen<'a> { self.emitter.emit_str(XZR, SP, 64); } + if conditional_count > 0 { + // 32-byte `#[repr(C)] ConditionalBindingAbi` per entry: + // lo = cmd_idx | len<<32 @0; slot (with _pad=0) @8; + // payload_ptr (ADR fixup) @16; payload_len @24. + for (i, &(cmd_idx, len, slot, payload_sym, payload_len)) in + conditional_bindings.iter().enumerate() + { + let entry_base = conditional_base + (i as i16) * CONDITIONAL_STRIDE; + let lo = (cmd_idx as u64) | ((len as u64) << 32); + + self.emit_mov_imm_64(X9, lo); + self.emitter.emit_str(X9, SP, entry_base); + + self.emit_mov_imm_64(X9, slot as u64); + self.emitter.emit_str(X9, SP, entry_base + 8); + + // payload_ptr — PC-relative ADR resolved against the + // embedded payload blob via `string_fixups`. + let adr_pos = self.emitter.current_offset(); + + self.string_fixups.push((adr_pos, payload_sym)); + self.emitter.emit_adr(X9, 0); + self.emitter.emit_str(X9, SP, entry_base + 16); + + self.emit_mov_imm_64(X9, payload_len as u64); + self.emitter.emit_str(X9, SP, entry_base + 24); + } + + // conditional_bindings_ptr = SP + conditional_base. + self.emitter.emit_add_imm(X9, SP, conditional_base as u16); + self.emitter.emit_str(X9, SP, 72); + + // conditional_bindings_count. + self.emit_mov_imm_64(X9, conditional_count as u64); + self.emitter.emit_str(X9, SP, 80); + } else { + self.emitter.emit_str(XZR, SP, 72); + self.emitter.emit_str(XZR, SP, 80); + } + // `MOV X0, SP` — AArch64 MOV (register) is `ORR Rd, // XZR, Rm`; SP and XZR share encoding 31 so ORR // would zero X0. Use `ADD X0, SP, #0` (the "MOV diff --git a/crates/compiler/zo-notes/public/grammar/README.md b/crates/compiler/zo-notes/public/grammar/README.md new file mode 100644 index 00000000..b7c3046c --- /dev/null +++ b/crates/compiler/zo-notes/public/grammar/README.md @@ -0,0 +1,3 @@ +# grammar. + +> *C'est une grammaire amère sa mère.* diff --git a/crates/compiler/zo-notes/public/guidelines/00-prologue.md b/crates/compiler/zo-notes/public/guidelines/README.md similarity index 99% rename from crates/compiler/zo-notes/public/guidelines/00-prologue.md rename to crates/compiler/zo-notes/public/guidelines/README.md index a1148497..f389b348 100644 --- a/crates/compiler/zo-notes/public/guidelines/00-prologue.md +++ b/crates/compiler/zo-notes/public/guidelines/README.md @@ -1,4 +1,4 @@ -# prologue. +# guidelines. > *The guidelines catalog.* diff --git a/crates/compiler/zo-notes/public/docs/error-message-argument-structure.md b/crates/compiler/zo-notes/public/spec/error-message-argument-structure.md similarity index 100% rename from crates/compiler/zo-notes/public/docs/error-message-argument-structure.md rename to crates/compiler/zo-notes/public/spec/error-message-argument-structure.md diff --git a/crates/compiler/zo-notes/public/zo.md b/crates/compiler/zo-notes/public/zo.md new file mode 100644 index 00000000..e4fb811b --- /dev/null +++ b/crates/compiler/zo-notes/public/zo.md @@ -0,0 +1,201 @@ +# zo. + +> *Turn your thoughts into type-safe software and Ui instantly.* + +This manual is for the zo programming language. + +@author — invisageable +@author — compilords + +> *DiSCLAiMER — zo is in early development. We are opening gates, testing some paths. Changes will be made, certain parts work, certain parts don't. Some of them will be removed.* +> +> *Some features described in this manual may not be available yet. It crafts the foundation as a draft. This document is not the final specification.* +> +> *For any suggestion, keep your focus on reductions to the language. What feature can be combined or omitted? At this point, every ``additive'' feature we're likely to support is already on the table. The task ahead involves combining, trimming, and implementing.* + +## introduction. + +zo is a Rust-prehistory's child. It inherits most of its concepts and philosophy. Without being an erzatz, zo is way far from Rust and find its niche with zsx (zo Syntax Extension) as builtin to build cross-platform user interfaces. It's a general-purpose programming language so you can create and maintain any kind of application. + +zo is a mix of imperative, concurrent actor, functional styles. It supports generics, foreign function interfaces, ... + +## goals. + +The language design pursues the following goals: + + - Compile-time error detection and prevention. + - Run-time fault tolerance and containment. + - Clarity and precision of expression. + - Implementation simplicity. + - Run-time efficiency. + - High concurrency. + +> *NOTE — zo is inspired by technologies that have been used earlier in other languages. These engineering goals are already been prove and are pretty solid. zo do not reivent the wheel, it assembles thing accordingly.* + +Like all new langages developped in a technological context. zo's goals focus on writing large programs that interact with internet (server & client), user interfaces and are thus much more concerned with safety and concurrency than older generations of program. + +## features. + +### no-pointer. + +... + +### lightweight tasks with no shared mutable state. + +... + +### ui template as builtin. + +... + +### cross-platform. + +... + +### direct interface to C code. + +... + +### generic code. + +... + +### local type inference. + +... + +### dynamic metaprogramming. + +... + +### static metaprogramming. + +... + +### idempotent failure. + +... + +### type inference. + +... + +### static control over mutability. + +... + +### helpful error messages. + +... + +## influences. + +> *"Why accept slow compilers? Just make them faster." — Jonathan Blow* + +> *"Semantic is king." — Robert Virding* + +> *""Simplicity is a prerequisite for reliability." — Edsger W. Dijkstra* + +"\"Re-think traditional compiler design.\" — Chandler Carruth", + +"\"The faster your software runs, the less power his consumed.\" — Chandler Carruth", + +"\"Challenge assumptions with aggressive goals.\" — Chandler Carruth", + +"\"Performance is king!\" — Mike Acton", + +"\"People don't get it! People don't know how fast computers are!\" — Jonathan Blow", + +--- + +zo is not a particularly original language. It may however appear unusual by contemporary standards, as its design elements are drawn from a number of `historical` languages that have, with a few exceptions, fallen out of +favour. Five prominent lineages contribute the most: + +**-rust-prehistory** + +... + +**-jai** + +... + +**erlang** + +... + +**-cyclone-and-ada** + +... + +**-imba** + +... + +**-es4-and-e4x** + +... + +Additional specific influences can be seen from the following languages: + + - The structural algebraic types and compilation manager of SML. + - The syntax-extension systems of Camlp4 and the Common Lisp readtable. + - The deterministic destructor system of C++. + +## tutorial. + +See the [initiation](https://zo.compilords.house/initiation) + +## reference. + + - Lexical structure. + - Grammar. + - zsx. + +### lexical structure. + +... + + - Ignored characters. + - Identifier tokens. + - Keyword tokens. + - Numeric tokens. + - String and character tokens. + - Syntactic extension tokens. + - Special symbol tokens. + +**-ignored-characters** + +... + +**-identifier-tokens** + +... + +**-keyword-tokens** + +... + +**-numeric-tokens** + +... + +**-string-and-character-tokens** + +... + +**-syntactic-extension-tokens** + +... + +**-special-symbol-tokens** + +... + +### grammar. + +See the EBNF [grammar](crates/compiler/zo-notes/public/grammar/zo.ebnf). + +### zsx. + +... + diff --git a/crates/compiler/zo-profiler/src/quotes.rs b/crates/compiler/zo-profiler/src/quotes.rs index 94ba2ef3..339ac25d 100644 --- a/crates/compiler/zo-profiler/src/quotes.rs +++ b/crates/compiler/zo-profiler/src/quotes.rs @@ -26,7 +26,7 @@ pub(crate) const QUOTES: &[&str] = &[ "\"Lies — code is more important than data.\" — Mike Acton", "\"Performance equals Efficiency.\" — Eskil Steenberg", "\"I don't know why linkers are so slow, but they are slow.\" — Jonathan Blow", - "\"People don't get it ! People don't know how fast computers are!\" — Jonathan Blow", + "\"People don't get it! People don't know how fast computers are!\" — Jonathan Blow", "\"Simplicity is the ultimate sophistication.\" — Leonardo da Vinci", "\"If you can't make a tool your primary workflow then you didn't build a tool.\" — Rik Arends", "\"Insanely faster, Usain Bolt would be jealous.\" — i2N", diff --git a/crates/compiler/zo-runtime-ios/src/ffi.rs b/crates/compiler/zo-runtime-ios/src/ffi.rs index 01788ce5..ab489c4e 100644 --- a/crates/compiler/zo-runtime-ios/src/ffi.rs +++ b/crates/compiler/zo-runtime-ios/src/ffi.rs @@ -1,9 +1,9 @@ //! The `_zo_run_native` C-ABI entry point for iOS. use zo_runtime_render::aot::{ - RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, build_registry, - decode_attr_bindings, decode_list_bindings, decode_template, - rebuild_with_lists, + RebuildInputs, RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, + build_registry, decode_attr_bindings, decode_conditional_bindings, + decode_list_bindings, decode_template, rebuild_with_regions, }; use zo_runtime_render::render::EventRegistry; @@ -43,6 +43,7 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { let lists = unsafe { decode_list_bindings(ctx_ref) }; let attrs = unsafe { decode_attr_bindings(ctx_ref) }; + let conditionals = unsafe { decode_conditional_bindings(ctx_ref) }; // Initial frame: bake every `mut`'s value into its `Text`, // apply attribute bindings, and splice each list's initial @@ -51,13 +52,14 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { // the first frame; the splice brings a non-empty initial list // onto the screen. let initial = unsafe { - rebuild_with_lists( - &base, - ctx_ref.text_bindings_ptr, - ctx_ref.text_bindings_count, - &attrs, - &lists, - ) + rebuild_with_regions(RebuildInputs { + base: &base, + text_bindings_ptr: ctx_ref.text_bindings_ptr, + text_bindings_count: ctx_ref.text_bindings_count, + attrs: &attrs, + lists: &lists, + conditionals: &conditionals, + }) }; let shared = Arc::new(Mutex::new(initial)); @@ -76,6 +78,7 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { bindings_ptr: SendPtr(ctx_ref.text_bindings_ptr), bindings_count: ctx_ref.text_bindings_count, report: Arc::clone(&report), + conditionals, }, ), None => EventRegistry::new(), diff --git a/crates/compiler/zo-runtime-native/src/ffi.rs b/crates/compiler/zo-runtime-native/src/ffi.rs index 0c654536..4c6b9647 100644 --- a/crates/compiler/zo-runtime-native/src/ffi.rs +++ b/crates/compiler/zo-runtime-native/src/ffi.rs @@ -11,9 +11,9 @@ use crate::runtime::Runtime; use zo_runtime_render::aot::{ - RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, build_registry, - decode_attr_bindings, decode_list_bindings, decode_template, - rebuild_with_lists, + RebuildInputs, RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, + build_registry, decode_attr_bindings, decode_conditional_bindings, + decode_list_bindings, decode_template, rebuild_with_regions, }; use zo_runtime_render::render::RuntimeConfig; @@ -55,6 +55,7 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { let lists = unsafe { decode_list_bindings(ctx_ref) }; let attrs = unsafe { decode_attr_bindings(ctx_ref) }; + let conditionals = unsafe { decode_conditional_bindings(ctx_ref) }; // Initial frame: bake every `mut`'s value into its `Text`, // apply attribute bindings, and splice each list's initial @@ -63,13 +64,14 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { // the first frame; the splice brings a non-empty initial list // onto the screen. let initial = unsafe { - rebuild_with_lists( - &base, - ctx_ref.text_bindings_ptr, - ctx_ref.text_bindings_count, - &attrs, - &lists, - ) + rebuild_with_regions(RebuildInputs { + base: &base, + text_bindings_ptr: ctx_ref.text_bindings_ptr, + text_bindings_count: ctx_ref.text_bindings_count, + attrs: &attrs, + lists: &lists, + conditionals: &conditionals, + }) }; let shared = Arc::new(Mutex::new(initial)); @@ -88,6 +90,7 @@ pub unsafe extern "C" fn zo_run_native(ctx: *const ZoRuntimeContext) { bindings_ptr: SendPtr(ctx_ref.text_bindings_ptr), bindings_count: ctx_ref.text_bindings_count, report: Arc::new(Mutex::new(UpdateReport::default())), + conditionals, }, )); } diff --git a/crates/compiler/zo-runtime-render/src/aot.rs b/crates/compiler/zo-runtime-render/src/aot.rs index 8c02daa4..709ee4d4 100644 --- a/crates/compiler/zo-runtime-render/src/aot.rs +++ b/crates/compiler/zo-runtime-render/src/aot.rs @@ -17,7 +17,7 @@ use crate::reactive::{ use crate::render::{EventHandler, EventRegistry}; use zo_ui_protocol::codec::{self, CodecError}; -use zo_ui_protocol::{LIST_ITEM_SENTINEL, UiCommand}; +use zo_ui_protocol::{ConditionalPayload, LIST_ITEM_SENTINEL, UiCommand}; use std::collections::HashSet; use std::slice; @@ -116,6 +116,23 @@ pub struct ListBindingAbi { pub recipe_len: usize, } +/// One tier-2 conditional region in the AOT context. The inline +/// region `[cmd_idx, cmd_idx + len)` of the base template holds +/// the initially-active branch; the blob at +/// `(payload_ptr, payload_len)` is a postcard +/// [`ConditionalPayload`]. Field order + sizes are ABI: +/// `cmd_idx`@0, `len`@4, `slot`@8, `_pad`@12, `payload_ptr`@16, +/// `payload_len`@24 (32 bytes, 8-aligned). +#[repr(C)] +pub struct ConditionalBindingAbi { + pub cmd_idx: u32, + pub len: u32, + pub slot: u32, + pub _pad: u32, + pub payload_ptr: *const u8, + pub payload_len: usize, +} + /// One reactive attribute binding (``). /// The runtime re-applies `commands[cmd_idx].attrs[attr_idx]`'s /// value from state slot `slot` (`is_str` selects `STR_STATE` @@ -184,6 +201,12 @@ pub struct ZoRuntimeContext { pub attr_bindings_ptr: *const AttrBindingAbi, /// Number of `AttrBindingAbi`s at `attr_bindings_ptr`. pub attr_bindings_count: usize, + /// Pointer to an array of `ConditionalBindingAbi` records — + /// tier-2 `{when flag ? … : …}` regions. Null / count 0 = none. + pub conditional_bindings_ptr: *const ConditionalBindingAbi, + /// Number of `ConditionalBindingAbi`s at + /// `conditional_bindings_ptr`. + pub conditional_bindings_count: usize, } /// Send wrapper for a raw pointer or fn pointer. The exe's @@ -450,6 +473,59 @@ pub struct ListBindingDecoded { pub recipe: Vec, } +/// One decoded tier-2 conditional, payload unpacked. +#[derive(Clone)] +pub struct ConditionalDecoded { + pub cmd_idx: usize, + pub len: usize, + pub slot: u32, + pub payload: ConditionalPayload, +} + +/// Decode the context's `ConditionalBindingAbi` array. A binding +/// whose payload fails to decode is skipped (defensive — a +/// corrupt blob drops that one region, not the whole UI). +/// +/// # Safety +/// +/// `ctx.conditional_bindings_ptr` must be null or point to +/// `ctx.conditional_bindings_count` valid entries whose payload +/// ranges are valid for the call. +pub unsafe fn decode_conditional_bindings( + ctx: &ZoRuntimeContext, +) -> Vec { + if ctx.conditional_bindings_ptr.is_null() { + return Vec::new(); + } + + let abis = unsafe { + slice::from_raw_parts( + ctx.conditional_bindings_ptr, + ctx.conditional_bindings_count, + ) + }; + let mut out = Vec::with_capacity(abis.len()); + + for abi in abis { + let bytes = + unsafe { slice::from_raw_parts(abi.payload_ptr, abi.payload_len) }; + + let decoded: Result = + codec::decode_payload(bytes); + + if let Ok(payload) = decoded { + out.push(ConditionalDecoded { + cmd_idx: abi.cmd_idx as usize, + len: abi.len as usize, + slot: abi.slot, + payload, + }); + } + } + + out +} + /// Decode the context's `ListBindingAbi` array into owned /// `ListBinding`s, postcard-decoding each recipe once. A binding /// whose recipe fails to decode is skipped (defensive — a @@ -578,13 +654,17 @@ fn apply_attr_bindings( /// /// `text_bindings_ptr` must be null or point to /// `text_bindings_count` valid `TextBinding` entries. -pub unsafe fn rebuild_with_lists( - base: &[UiCommand], - text_bindings_ptr: *const TextBinding, - text_bindings_count: usize, - attrs: &[AttrBindingDecoded], - lists: &[ListBindingDecoded], +pub unsafe fn rebuild_with_regions( + inputs: RebuildInputs<'_>, ) -> Vec { + let RebuildInputs { + base, + text_bindings_ptr, + text_bindings_count, + attrs, + lists, + conditionals, + } = inputs; let mut cmds = base.to_vec(); unsafe { @@ -595,15 +675,66 @@ pub unsafe fn rebuild_with_lists( ); } - // Attr bindings apply before the splice — they target the base + // Attr bindings apply before any splice — they target the base // commands (e.g. the ``), all of which sit before any - // list anchor in the current shapes. + // region in the current shapes. apply_attr_bindings(&mut cmds, attrs, |_| true); + // Conditional regions splice FIRST, against base coordinates; + // their deltas shift every later list anchor — the same order + // the `zo run` applier uses. + let mut cond_deltas: Vec<(usize, isize)> = + Vec::with_capacity(conditionals.len()); + let mut cond_offset: isize = 0; + + for cond in conditionals { + let target = (cond.cmd_idx as isize + cond_offset) as usize; + + // The state cell holds the executor-checked `bool`, + // represented as 0/1 in scalar state — this reads the bool's + // representation, not a truthiness coercion. + let truthy = zo_state_get(cond.slot) != 0; + let (blob, texts) = if truthy { + (&cond.payload.on_true, &cond.payload.true_text) + } else { + (&cond.payload.on_false, &cond.payload.false_text) + }; + + let mut rendered = blob.clone(); + + for &(branch_idx, slot, is_str) in texts { + if let Some(UiCommand::Text(out)) = rendered.get_mut(branch_idx as usize) + && let Some(text) = state_slot_text(slot, is_str) + { + *out = text; + } + } + + let new_len = rendered.len(); + + if target + cond.len <= cmds.len() { + cmds.splice(target..target + cond.len, rendered); + cond_deltas.push((cond.cmd_idx, new_len as isize - cond.len as isize)); + cond_offset += new_len as isize - cond.len as isize; + } + } + + let shift = |idx: usize| -> usize { + let mut shifted = idx as isize; + + for &(at, delta) in &cond_deltas { + if idx > at { + shifted += delta; + } + } + + shifted.max(0) as usize + }; + let mut offset: isize = 0; for list in lists { - let target = (list.cmd_idx as isize + offset) as usize; + let target = (shift(list.cmd_idx) as isize + offset) as usize; if target >= cmds.len() { continue; @@ -620,6 +751,17 @@ pub unsafe fn rebuild_with_lists( cmds } +/// Inputs for one full stream rebuild from the base template — +/// see [`rebuild_with_regions`]. +pub struct RebuildInputs<'a> { + pub base: &'a [UiCommand], + pub text_bindings_ptr: *const TextBinding, + pub text_bindings_count: usize, + pub attrs: &'a [AttrBindingDecoded], + pub lists: &'a [ListBindingDecoded], + pub conditionals: &'a [ConditionalDecoded], +} + /// Refresh every reactive `commands[cmd_idx]` from /// `state` / `str_state`. Pure inner that takes state /// explicitly — `refresh_bindings_from_global` wraps it @@ -832,6 +974,8 @@ pub struct RegistryInputs { pub bindings_count: usize, /// Per-event update report the view layer reads after dispatch. pub report: Arc>, + /// Decoded tier-2 conditionals (empty when none). + pub conditionals: Vec, } /// Build an `EventRegistry` whose callbacks dispatch through @@ -860,6 +1004,7 @@ pub fn build_registry( bindings_ptr, bindings_count, report, + conditionals, } = inputs; let mut registry = EventRegistry::new(); @@ -872,6 +1017,7 @@ pub fn build_registry( .map(|list| arr_slot_items(list.items_slot)) .collect(), )); + let conditionals = Arc::new(conditionals); let mut seen: HashSet = HashSet::new(); let mut handler_idx: u32 = 0; @@ -914,6 +1060,7 @@ pub fn build_registry( let attrs = Arc::clone(&attrs); let report = Arc::clone(&report); let list_keys = Arc::clone(&list_keys); + let conditionals = Arc::clone(&conditionals); let cb: EventHandler = Box::new(move |payload| { // RFC 2229 disjoint captures would otherwise pull // `dispatch_send.0` / `bindings_send.0` directly into the @@ -950,9 +1097,11 @@ pub fn build_registry( drain_dirty(&mut written); let hits_list = lists.iter().any(|l| written.contains(&l.items_slot)); + let hits_conditional = + conditionals.iter().any(|c| written.contains(&c.slot)); let mut cmds = cmds_arc.lock().unwrap(); - if hits_list { + if hits_list || hits_conditional { // Keyed diff per written list: same item count means the // stream keeps its shape (fixed stride per item), so the // write patches in place — only the changed commands mark @@ -986,13 +1135,14 @@ pub fn build_registry( } let rebuilt = unsafe { - rebuild_with_lists( - base.as_slice(), - bindings_send.0, - bindings_count, - attrs.as_slice(), - lists.as_slice(), - ) + rebuild_with_regions(RebuildInputs { + base: base.as_slice(), + text_bindings_ptr: bindings_send.0, + text_bindings_count: bindings_count, + attrs: attrs.as_slice(), + lists: lists.as_slice(), + conditionals: conditionals.as_slice(), + }) }; let mut report = report.lock().unwrap(); @@ -1106,6 +1256,8 @@ mod tests { list_bindings_count: 0, attr_bindings_ptr: std::ptr::null(), attr_bindings_count: 0, + conditional_bindings_ptr: std::ptr::null(), + conditional_bindings_count: 0, } } @@ -1504,6 +1656,7 @@ mod tests { bindings_ptr: SendPtr(std::ptr::null()), bindings_count: 0, report: Arc::clone(&report), + conditionals: Vec::new(), }, ); @@ -1598,6 +1751,8 @@ mod tests { list_bindings_count: abis.len(), attr_bindings_ptr: std::ptr::null(), attr_bindings_count: 0, + conditional_bindings_ptr: std::ptr::null(), + conditional_bindings_count: 0, }; let decoded = unsafe { decode_list_bindings(&ctx) }; @@ -1613,7 +1768,7 @@ mod tests { } #[test] - fn rebuild_with_lists_splices_array_items() { + fn rebuild_with_regions_splices_array_items() { let _serial = state_lock(); // Array slot 420 is unique to this test. zo_state_init(430); @@ -1637,8 +1792,16 @@ mod tests { recipe: li_recipe(), }]; - let rebuilt = - unsafe { rebuild_with_lists(&base, std::ptr::null(), 0, &[], &lists) }; + let rebuilt = unsafe { + rebuild_with_regions(RebuildInputs { + base: &base, + text_bindings_ptr: std::ptr::null(), + text_bindings_count: 0, + attrs: &[], + lists: &lists, + conditionals: &[], + }) + }; assert_eq!( rebuilt, @@ -1764,6 +1927,7 @@ mod tests { bindings_ptr: SendPtr(std::ptr::null()), bindings_count: 0, report: Arc::new(Mutex::new(UpdateReport::default())), + conditionals: Vec::new(), }, ); let mut out: Vec = cmds diff --git a/crates/compiler/zo-runtime-web/src/ffi.rs b/crates/compiler/zo-runtime-web/src/ffi.rs index 0c87c8ba..fe7b695a 100644 --- a/crates/compiler/zo-runtime-web/src/ffi.rs +++ b/crates/compiler/zo-runtime-web/src/ffi.rs @@ -11,9 +11,9 @@ use crate::runtime::Runtime; use zo_runtime_render::aot::{ - RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, build_registry, - decode_attr_bindings, decode_list_bindings, decode_template, - rebuild_with_lists, + RebuildInputs, RegistryInputs, SendPtr, UpdateReport, ZoRuntimeContext, + build_registry, decode_attr_bindings, decode_conditional_bindings, + decode_list_bindings, decode_template, rebuild_with_regions, }; use zo_runtime_render::render::RuntimeConfig; @@ -55,19 +55,21 @@ pub unsafe extern "C" fn zo_run_web(ctx: *const ZoRuntimeContext) { let lists = unsafe { decode_list_bindings(ctx_ref) }; let attrs = unsafe { decode_attr_bindings(ctx_ref) }; + let conditionals = unsafe { decode_conditional_bindings(ctx_ref) }; // Initial frame: bake every `mut`'s value into its `Text`, apply // attribute bindings, and splice each list's initial items over its // placeholder — identical to the native entry, since the command // stream is backend-agnostic. let initial = unsafe { - rebuild_with_lists( - &base, - ctx_ref.text_bindings_ptr, - ctx_ref.text_bindings_count, - &attrs, - &lists, - ) + rebuild_with_regions(RebuildInputs { + base: &base, + text_bindings_ptr: ctx_ref.text_bindings_ptr, + text_bindings_count: ctx_ref.text_bindings_count, + attrs: &attrs, + lists: &lists, + conditionals: &conditionals, + }) }; let shared = Arc::new(Mutex::new(initial)); @@ -86,6 +88,7 @@ pub unsafe extern "C" fn zo_run_web(ctx: *const ZoRuntimeContext) { bindings_ptr: SendPtr(ctx_ref.text_bindings_ptr), bindings_count: ctx_ref.text_bindings_count, report: Arc::new(Mutex::new(UpdateReport::default())), + conditionals, }, )); } diff --git a/crates/compiler/zo-ui-protocol/src/codec.rs b/crates/compiler/zo-ui-protocol/src/codec.rs index df5c46ed..94178d7e 100644 --- a/crates/compiler/zo-ui-protocol/src/codec.rs +++ b/crates/compiler/zo-ui-protocol/src/codec.rs @@ -38,6 +38,22 @@ pub fn decode(bytes: &[u8]) -> Result, CodecError> { postcard::from_bytes(bytes) } +/// Encode any postcard payload (the tier-2 conditional branch +/// payload rides this; `encode` stays the command-stream entry). +pub fn encode_payload( + value: &T, +) -> Result, CodecError> { + postcard::to_allocvec(value) +} + +/// Decode any postcard payload — the sibling of +/// [`encode_payload`]. +pub fn decode_payload( + bytes: &[u8], +) -> Result { + postcard::from_bytes(bytes) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/compiler/zo-ui-protocol/src/lib.rs b/crates/compiler/zo-ui-protocol/src/lib.rs index 049caba4..98b17922 100644 --- a/crates/compiler/zo-ui-protocol/src/lib.rs +++ b/crates/compiler/zo-ui-protocol/src/lib.rs @@ -6,8 +6,8 @@ mod ui_protocol; pub use ui::Ui; pub use ui_protocol::{ - Attr, ElementTag, EventKind, LIST_ITEM_SENTINEL, PropValue, StyleScope, - UiCommand, + Attr, ConditionalPayload, ElementTag, EventKind, LIST_ITEM_SENTINEL, + PropValue, StyleScope, UiCommand, }; /// Whether a directive name mounts a template onto the active diff --git a/crates/compiler/zo-ui-protocol/src/ui_protocol.rs b/crates/compiler/zo-ui-protocol/src/ui_protocol.rs index 85a5b816..6909e9db 100644 --- a/crates/compiler/zo-ui-protocol/src/ui_protocol.rs +++ b/crates/compiler/zo-ui-protocol/src/ui_protocol.rs @@ -303,6 +303,19 @@ impl ElementTag { } } +/// Tier-2 conditional branch payload, postcard-encoded into the +/// AOT context's `ConditionalBindingAbi`: both compiled branch +/// blobs plus their reactive text interps as +/// `(branch_relative_idx, state_slot, is_str)` triples the runtime +/// substitutes at splice time. +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct ConditionalPayload { + pub on_true: Vec, + pub true_text: Vec<(u32, u32, bool)>, + pub on_false: Vec, + pub false_text: Vec<(u32, u32, bool)>, +} + /// Event types that can occur in the UI. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] pub enum EventKind { From 5a91cf46f2b2ac65d041ea012f8af959894af197 Mon Sep 17 00:00:00 2001 From: invisageable Date: Fri, 12 Jun 2026 22:29:32 +0200 Subject: [PATCH 3/5] feat(zo): runtime ABI skew guard --- Cargo.lock | 6 ++ Cargo.toml | 1 + crates/compiler/zo-abi/Cargo.toml | 7 +++ crates/compiler/zo-abi/src/lib.rs | 61 ++++++++++++++++++++ crates/compiler/zo-compiler/Cargo.toml | 1 + crates/compiler/zo-compiler/src/compiler.rs | 50 ++++++++++++++++ crates/compiler/zo-notes/public/zo.md | 16 ++--- crates/compiler/zo-runtime-render/src/aot.rs | 5 ++ crates/compiler/zo-runtime/Cargo.toml | 1 + crates/compiler/zo-runtime/src/lib.rs | 8 +++ 10 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 crates/compiler/zo-abi/Cargo.toml create mode 100644 crates/compiler/zo-abi/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d15931ee..e26674d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8025,6 +8025,10 @@ dependencies = [ "zo-driver", ] +[[package]] +name = "zo-abi" +version = "0.4.0" + [[package]] name = "zo-analyzer" version = "0.4.0" @@ -8208,6 +8212,7 @@ dependencies = [ "hashbrown 0.16.1", "rustc-hash 2.1.2", "tempfile", + "zo-abi", "zo-analyzer", "zo-bundler", "zo-codegen", @@ -8569,6 +8574,7 @@ dependencies = [ "sha1", "sha2", "sysinfo", + "zo-abi", "zo-c-abi", "zo-error", "zo-reporter", diff --git a/Cargo.toml b/Cargo.toml index 62760120..bc02f85f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ zo-emitter-arm = { path = "crates/compiler/zo-emitter-arm", version = "0.4.0" } zo-emitter-x86 = { path = "crates/compiler/zo-emitter-x86", version = "0.3.11" } zo-error = { path = "crates/compiler/zo-error", version = "0.4.0" } zo-executor = { path = "crates/compiler/zo-executor", version = "0.4.0" } +zo-abi = { path = "crates/compiler/zo-abi", version = "0.4.0" } zo-host-paths = { path = "crates/compiler/zo-host-paths", version = "0.4.0" } zo-interner = { path = "crates/compiler/zo-interner", version = "0.4.0" } zo-linker = { path = "crates/compiler/zo-linker", version = "0.4.0" } diff --git a/crates/compiler/zo-abi/Cargo.toml b/crates/compiler/zo-abi/Cargo.toml new file mode 100644 index 00000000..30fdd805 --- /dev/null +++ b/crates/compiler/zo-abi/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "zo-abi" +version.workspace = true +edition.workspace = true + +[lib] +doctest = false diff --git a/crates/compiler/zo-abi/src/lib.rs b/crates/compiler/zo-abi/src/lib.rs new file mode 100644 index 00000000..422a31c8 --- /dev/null +++ b/crates/compiler/zo-abi/src/lib.rs @@ -0,0 +1,61 @@ +//! The toolchain ↔ runtime ABI contract. +//! +//! A dependency-free leaf both sides share: the runtime cdylib +//! embeds [`RUNTIME_ABI_TAG`] as bytes, and `zo build` scans the +//! staged dylib for it before shipping — a mismatch means the +//! staged runtime would decode an older context layout and +//! silently drop behavior, so the build refuses instead. Neither +//! the compiler nor the runtime needs the other's types for this; +//! the contract is the only shared surface. + +/// Runtime ABI tag, embedded verbatim in the runtime dylib and +/// scanned by `zo build` from the staged copy. +/// +/// @note — BUMP THE SUFFIX whenever `ZoRuntimeContext` or any +/// `#[repr(C)]` binding ABI changes shape. +pub const RUNTIME_ABI_TAG: &[u8; 14] = b"ZO_RT_ABI:0001"; + +/// The tag's scan prefix — version-independent, used to locate the +/// tag bytes inside a staged dylib. +pub const RUNTIME_ABI_TAG_PREFIX: &[u8; 10] = b"ZO_RT_ABI:"; + +/// Locates the `ZO_RT_ABI:` tag inside a dylib's bytes, returning +/// the full tag slice (prefix + version digits) when present. +pub fn find_abi_tag(bytes: &[u8]) -> Option<&[u8]> { + let prefix = RUNTIME_ABI_TAG_PREFIX.as_slice(); + let tag_len = RUNTIME_ABI_TAG.len(); + + bytes + .windows(prefix.len()) + .position(|window| window == prefix) + .and_then(|at| bytes.get(at..at + tag_len)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_the_tag_anywhere_in_the_bytes() { + let mut bytes = vec![0u8; 64]; + + bytes.extend_from_slice(RUNTIME_ABI_TAG); + bytes.extend_from_slice(&[0u8; 32]); + + assert_eq!(find_abi_tag(&bytes), Some(RUNTIME_ABI_TAG.as_slice())); + } + + #[test] + fn missing_tag_reads_none() { + assert_eq!(find_abi_tag(&[0u8; 128]), None); + } + + #[test] + fn a_stale_tag_is_returned_for_the_diagnostic() { + let mut bytes = b"ZO_RT_ABI:0000".to_vec(); + + bytes.extend_from_slice(&[0u8; 16]); + + assert_eq!(find_abi_tag(&bytes), Some(b"ZO_RT_ABI:0000".as_slice())); + } +} diff --git a/crates/compiler/zo-compiler/Cargo.toml b/crates/compiler/zo-compiler/Cargo.toml index 72e5c8d3..cd0af2d6 100644 --- a/crates/compiler/zo-compiler/Cargo.toml +++ b/crates/compiler/zo-compiler/Cargo.toml @@ -23,6 +23,7 @@ zo-linker = { workspace = true } zo-ownership = { workspace = true } zo-parser = { workspace = true } zo-sir = { workspace = true } +zo-abi = { workspace = true } zo-pp = { workspace = true } zo-profiler = { workspace = true } zo-reporter = { workspace = true } diff --git a/crates/compiler/zo-compiler/src/compiler.rs b/crates/compiler/zo-compiler/src/compiler.rs index 06a69048..b42a80a5 100644 --- a/crates/compiler/zo-compiler/src/compiler.rs +++ b/crates/compiler/zo-compiler/src/compiler.rs @@ -2623,6 +2623,14 @@ fn stage_dylib_as( // partial one. for src in &candidates { if src.exists() { + // The runtime dylib carries an ABI tag; a staged copy whose + // tag differs from the compiler's would decode an older + // `ZoRuntimeContext` and silently drop behavior (dead + // reactivity). Refuse the build instead. + if dest == RUNTIME_STAGED_DYLIB { + verify_runtime_abi_tag(src); + } + let destination = output_dir.join(dest); let tmp = output_dir.join(format!(".{}.{}.tmp", dest, std::process::id())); @@ -2637,3 +2645,45 @@ fn stage_dylib_as( } } } + +/// Scan a runtime dylib's bytes for the ABI tag and compare it to +/// the compiler's expected value. A missing or mismatched tag +/// aborts the build: a stale runtime next to a fresh binary fails +/// silently at runtime (the staged library reads an older context +/// layout), which costs far more than failing loudly here. +fn verify_runtime_abi_tag(dylib: &std::path::Path) { + let Ok(bytes) = std::fs::read(dylib) else { + // Unreadable staging sources surface as copy failures below; + // nothing useful to add here. + return; + }; + + let found = zo_abi::find_abi_tag(&bytes); + + if found == Some(zo_abi::RUNTIME_ABI_TAG.as_slice()) { + return; + } + + let expected = String::from_utf8_lossy(zo_abi::RUNTIME_ABI_TAG); + + match found { + Some(stale) => eprintln!( + "zo: runtime ABI mismatch — the staged runtime library \ + ({}) carries tag `{}` but this compiler expects \ + `{expected}`. Rebuild the runtime flavor libraries the \ + compiler ships with (libzo_runtime_ui / libzo_runtime_core) \ + so they match this compiler build.", + dylib.display(), + String::from_utf8_lossy(stale), + ), + None => eprintln!( + "zo: runtime ABI tag missing — the staged runtime library \ + ({}) predates ABI tagging and cannot be paired with this \ + compiler. Rebuild the runtime flavor libraries \ + (libzo_runtime_ui / libzo_runtime_core).", + dylib.display(), + ), + } + + std::process::exit(1); +} diff --git a/crates/compiler/zo-notes/public/zo.md b/crates/compiler/zo-notes/public/zo.md index e4fb811b..27904a4c 100644 --- a/crates/compiler/zo-notes/public/zo.md +++ b/crates/compiler/zo-notes/public/zo.md @@ -96,15 +96,15 @@ Like all new langages developped in a technological context. zo's goals focus on > *""Simplicity is a prerequisite for reliability." — Edsger W. Dijkstra* -"\"Re-think traditional compiler design.\" — Chandler Carruth", +> *""Re-think traditional compiler design." — Chandler Carruth"* -"\"The faster your software runs, the less power his consumed.\" — Chandler Carruth", +> *""The faster your software runs, the less power his consumed." — Chandler Carruth"* -"\"Challenge assumptions with aggressive goals.\" — Chandler Carruth", +> *""Challenge assumptions with aggressive goals." — Chandler Carruth"* -"\"Performance is king!\" — Mike Acton", +> *""Performance is king!" — Mike Acton"* -"\"People don't get it! People don't know how fast computers are!\" — Jonathan Blow", +> *""People don't get it! People don't know how fast computers are!" — Jonathan Blow"* --- @@ -125,11 +125,13 @@ favour. Five prominent lineages contribute the most: **-cyclone-and-ada** -... +Cyclone is a programming language to make a low-level C-like language safe made by Nikhil Swamy, Michael Hicks, Greg Morrisett, Dan Grossman and Trevor Jim. zo adopt + +Ada is an extremely strong typing programming language and is designed for developing very large software systems. **-imba** -... +Imba is a fascinating Web programming language, the right fit to build frontend and backend made by Sindre Aarsaether and (...). zo integrated a similar system to do styling with zsx. **-es4-and-e4x** diff --git a/crates/compiler/zo-runtime-render/src/aot.rs b/crates/compiler/zo-runtime-render/src/aot.rs index 709ee4d4..21afe3ba 100644 --- a/crates/compiler/zo-runtime-render/src/aot.rs +++ b/crates/compiler/zo-runtime-render/src/aot.rs @@ -150,6 +150,11 @@ pub struct AttrBindingAbi { /// AOT entry-point context. Built in the compiled exe's /// stack frame by codegen and passed by reference to +/// +/// @note — append-only: new fields go at the END, and every shape +/// change bumps `zo_abi::RUNTIME_ABI_TAG` so `zo build` +/// rejects stale staged runtimes instead of silently dropping the +/// new behavior. /// `_zo_run_native` at startup. /// /// Field order is part of the ABI. Optional callback / diff --git a/crates/compiler/zo-runtime/Cargo.toml b/crates/compiler/zo-runtime/Cargo.toml index 99d92e9f..e66394b4 100644 --- a/crates/compiler/zo-runtime/Cargo.toml +++ b/crates/compiler/zo-runtime/Cargo.toml @@ -34,6 +34,7 @@ ui = [ zo-c-abi = { workspace = true } zo-error = { workspace = true } zo-reporter = { workspace = true } +zo-abi = { workspace = true } zo-ui-protocol = { workspace = true } zo-runtime-render = { workspace = true, optional = true } diff --git a/crates/compiler/zo-runtime/src/lib.rs b/crates/compiler/zo-runtime/src/lib.rs index 6fb5bfca..bf2d4fce 100644 --- a/crates/compiler/zo-runtime/src/lib.rs +++ b/crates/compiler/zo-runtime/src/lib.rs @@ -53,3 +53,11 @@ pub use zo_runtime_web::{Browsering, Quiet, Server}; /// that reference. #[cfg(all(feature = "ui", any(target_os = "ios", target_os = "watchos")))] pub use zo_runtime_ios::zo_run_native; + +/// The ABI tag `zo build` scans for in the staged dylib. Lives in +/// the cdylib root so BOTH flavors carry it — the lean core build +/// excludes the render tree entirely. `#[used]` keeps the bytes in +/// the artifact even with no Rust reader. See +/// `zo_abi::RUNTIME_ABI_TAG` for the bump rule. +#[used] +pub static ZO_RUNTIME_ABI_TAG: [u8; 14] = *zo_abi::RUNTIME_ABI_TAG; From 51e1416c07b506bec03ac69f1ba1297171dbae2d Mon Sep 17 00:00:00 2001 From: invisageable Date: Sat, 13 Jun 2026 07:17:34 +0200 Subject: [PATCH 4/5] feat(zo): per-instance component state --- crates/compiler/zo-executor/src/executor.rs | 217 +++++++++++++++++- .../zo-executor/src/tests/templates.rs | 51 ++++ .../templating/zsx_counter_instances.zo | 22 ++ 3 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 crates/compiler/zo-tests/templating/zsx_counter_instances.zo diff --git a/crates/compiler/zo-executor/src/executor.rs b/crates/compiler/zo-executor/src/executor.rs index a7ed9b08..f31e41d9 100644 --- a/crates/compiler/zo-executor/src/executor.rs +++ b/crates/compiler/zo-executor/src/executor.rs @@ -78,7 +78,7 @@ pub struct ExecuteOutput { const MAX_COMPONENT_DEPTH: usize = 64; /// One registered component body fragment. -#[derive(Clone, Copy)] +#[derive(Clone)] struct ComponentFragment { /// Fragment node range in this executor's tree. range: (usize, usize), @@ -86,6 +86,23 @@ struct ComponentFragment { /// component must always instantiate — its registration-time /// bake has no slot content to show. slot: Slot, + /// Body-local `mut` declarations: every instantiation creates + /// its own cells for these under synthetic per-instance names, + /// so two ``s count independently. + state: Vec, +} + +/// One body-local `mut` a component re-materializes per use site. +#[derive(Clone, Copy)] +struct ComponentState { + /// The declared name — aliased to the synthetic per-instance + /// name during the instantiation's body re-execution. + name: Symbol, + /// Snapshot of the initial value at registration (the decl's + /// init already executed; the value store is append-only, so + /// the id stays valid). + value_id: ValueId, + ty_id: TyId, } /// One component tag opened with children @@ -347,6 +364,13 @@ pub struct Executor<'a> { /// site. Suppresses component registration (the enclosing /// function must not re-register with an instance's template). instantiating: u32, + /// Per-instantiation alias frames: the component body's state + /// names resolve to their synthetic per-instance names + /// (`count` → `count$3`) while the frame is live. A stack so + /// nested instantiations compose. + instance_aliases: Vec>, + /// Monotonic id minting the synthetic state names. + instance_counter: u32, /// Components currently being instantiated, outermost first. /// Local components can't recurse (registration order), but an /// *imported* fragment re-executes where every spliced component @@ -1008,6 +1032,8 @@ impl<'a> Executor<'a> { saw_slot_tag: false, slot_frames: Vec::new(), instantiating: 0, + instance_aliases: Vec::new(), + instance_counter: 0, instantiation_stack: Vec::new(), saved_outer_funs: Vec::new(), pending_function: None, @@ -2275,6 +2301,7 @@ impl<'a> Executor<'a> { ComponentFragment { range: (entry.range.0 as usize, entry.range.1 as usize), slot: entry.slot, + state: Vec::new(), }, ); } @@ -3735,6 +3762,7 @@ impl<'a> Executor<'a> { name_span: self.pending_fn_name_span, pending_return: false, scope_depth: self.scope_stack.len(), + locals_base: self.locals.len(), }); // Update body_start in the pending function @@ -4711,6 +4739,12 @@ impl<'a> Executor<'a> { } if let Some(NodeValue::Symbol(sym)) = self.node_value(idx) { + // Inside a component instantiation, a body `mut` resolves + // to its synthetic per-instance name (`count` → `count$N`) + // so each instance reads/writes its own cell. A no-op + // outside instantiation and for non-state symbols. + let sym = self.resolve_instance_alias(sym); + // Copy fields to avoid borrow issues. let local_info = self.lookup_local(sym).map(|l| { ( @@ -6163,6 +6197,7 @@ impl<'a> Executor<'a> { self.sir_values.pop(); let span = self.tree.spans[target_idx]; + let name = self.resolve_instance_alias(name); self.pending_assign = Some((name, span)); } else if self.tree.nodes[target_idx].token == Token::RBracket { @@ -8688,6 +8723,7 @@ impl<'a> Executor<'a> { name_span: Span::ZERO, pending_return: false, scope_depth: self.scope_stack.len(), + locals_base: self.locals.len(), }); // Executing the body here only lowers it to SIR — the closure runs @@ -19884,6 +19920,7 @@ impl<'a> Executor<'a> { self.sir_values.pop(); let span = self.tree.spans[target_idx]; + let name = self.resolve_instance_alias(name); self.pending_compound_receiver = None; self.pending_compound = Some((name, op, span)); @@ -25053,7 +25090,7 @@ impl<'a> Executor<'a> { && self.tree.nodes[idx].token == Token::Ident { self.node_value(idx).and_then(|v| match v { - NodeValue::Symbol(s) => Some(s), + NodeValue::Symbol(s) => Some(self.resolve_instance_alias(s)), _ => None, }) } else { @@ -25279,6 +25316,7 @@ impl<'a> Executor<'a> { name_span: Span::ZERO, pending_return: false, scope_depth: self.scope_stack.len(), + locals_base: self.locals.len(), }); // Param scope: push each capture as a local so @@ -25520,12 +25558,36 @@ impl<'a> Executor<'a> { Slot::No }; + // Body-local `mut`s become per-instance state: each use + // site re-materializes them under synthetic names, so two + // instances never share a cell. + let state: Vec = self + .current_function + .as_ref() + .map(|ctx| ctx.locals_base) + .map(|base| { + self.locals[base.min(self.locals.len())..] + .iter() + .filter(|local| { + local.mutability == Mutability::Yes + && local.local_kind == LocalKind::Variable + }) + .map(|local| ComponentState { + name: local.name, + value_id: local.value_id, + ty_id: local.ty_id, + }) + .collect() + }) + .unwrap_or_default(); + self.component_templates.insert(name, template_id); self.component_fragments.insert( name, ComponentFragment { range: (start_idx, end_idx), slot, + state, }, ); @@ -26045,7 +26107,7 @@ impl<'a> Executor<'a> { tag_span: Span, ) -> Option<(Vec, TemplateBindings)> { let sym = self.interner.symbol(tag)?; - let fragment = *self.component_fragments.get(&sym)?; + let fragment = self.component_fragments.get(&sym)?.clone(); let (frag_start, frag_end) = fragment.range; // Instantiating a component already on the stack (or past the @@ -26078,14 +26140,17 @@ impl<'a> Executor<'a> { if params.is_empty() && fragment.slot == Slot::No + && fragment.state.is_empty() && self.pending_slot.is_empty() && self.component_templates.contains_key(&sym) { - // A local zero-prop, slot-free component has a baked - // registration-time template identical for every use — the - // clone path is cheaper. A slotted component (or one used - // with children) must instantiate, and an imported component - // has no local bake; both fall through. + // A local zero-prop, slot-free, STATELESS component has a + // baked registration-time template identical for every use — + // the clone path is cheaper. A component with body `mut` + // state must instantiate so each use site gets its own cells; + // a slotted component (or one used with children) must + // instantiate too, and an imported component has no local + // bake; all fall through. return None; } @@ -26152,6 +26217,16 @@ impl<'a> Executor<'a> { // the parent's collected-so-far bindings must survive the // nested completion's `mem::take`. let saved_bindings = std::mem::take(&mut self.template_bindings); + + // Per-instance state cells: re-materialize each body `mut` + // under a synthetic name BEFORE the SIR snapshot — the init + // instructions belong to the enclosing stream (codegen seeds + // the slot from them), only the fragment's emissions roll + // back. + let aliases = self.materialize_instance_state(&fragment.state); + + self.instance_aliases.push(aliases); + let sir_len = self.sir.instructions.len(); self.instantiating += 1; @@ -26159,6 +26234,7 @@ impl<'a> Executor<'a> { self.execute_template_fragment(frag_start, frag_end); self.instantiation_stack.pop(); self.instantiating -= 1; + self.instance_aliases.pop(); self.pending_styles = saved_styles; self.pending_var_name = saved_pending_var; @@ -26185,6 +26261,127 @@ impl<'a> Executor<'a> { instance } + /// Re-materialize a component's body `mut`s for one use site: + /// fresh value cells under synthetic names (`count$3`), init SIR + /// emitted into the enclosing stream so codegen and the run path + /// seed each instance's slot, and the alias map the body's + /// references resolve through. Only scalar state (int / bool / + /// str) is supported — exactly what the reactive cells model. + fn materialize_instance_state( + &mut self, + state: &[ComponentState], + ) -> HashMap { + let mut aliases = HashMap::default(); + + for entry in state { + self.instance_counter += 1; + + let synthetic = self.interner.intern(&format!( + "{}${}", + self.interner.get(entry.name), + self.instance_counter + )); + + let value_idx = entry.value_id.0 as usize; + let Some(kind) = self.values.kinds.get(value_idx).copied() else { + continue; + }; + let payload = self.values.indices[value_idx] as usize; + + let (fresh_value, init_sir) = match kind { + Value::Int => { + let value = self.values.ints[payload]; + let dst = ValueId(self.sir.next_value_id); + + self.sir.next_value_id += 1; + self.sir.emit(Insn::ConstInt { + dst, + value, + ty_id: entry.ty_id, + }); + + (self.values.store_int(value), dst) + } + Value::Bool => { + let value = self.values.bools[payload]; + let dst = ValueId(self.sir.next_value_id); + + self.sir.next_value_id += 1; + self.sir.emit(Insn::ConstBool { + dst, + value, + ty_id: entry.ty_id, + }); + + (self.values.store_bool(value), dst) + } + Value::String => { + let symbol = self.values.strings[payload]; + let dst = ValueId(self.sir.next_value_id); + + self.sir.next_value_id += 1; + self.sir.emit(Insn::ConstString { + dst, + symbol, + ty_id: entry.ty_id, + }); + + (self.values.store_string(symbol), dst) + } + // Non-scalar body state falls outside the reactive cell + // model; the body still sees the registration-time value. + _ => continue, + }; + + self.sir.emit(Insn::VarDef { + name: synthetic, + ty_id: entry.ty_id, + init: Some(init_sir), + mutability: Mutability::Yes, + pubness: Pubness::No, + }); + self.sir.emit(Insn::Store { + name: synthetic, + value: init_sir, + ty_id: entry.ty_id, + }); + + self.push_local(Local { + name: synthetic, + ty_id: entry.ty_id, + value_id: fresh_value, + pubness: Pubness::No, + mutability: Mutability::Yes, + sir_value: Some(init_sir), + local_kind: LocalKind::Variable, + auto_drop: AutoDrop::No, + owning_pack: None, + span: Span::ZERO, + }); + + if let Some(frame) = self.scope_stack.last_mut() { + frame.count += 1; + } + + aliases.insert(entry.name, synthetic); + } + + aliases + } + + /// Resolve a body symbol through the live instantiation alias + /// frames (innermost first) — `count` inside an instance reads + /// and writes its own `count$N` cell. + fn resolve_instance_alias(&self, sym: Symbol) -> Symbol { + for frame in self.instance_aliases.iter().rev() { + if let Some(&aliased) = frame.get(&sym) { + return aliased; + } + } + + sym + } + fn try_resolve_template_component( &self, tag: &str, @@ -26851,6 +27048,10 @@ struct FunCtx { /// Scope depth when the function body was entered. /// Only close the function at this depth's RBrace. pub(crate) scope_depth: usize, + /// `locals.len()` at body entry — everything past it belongs to + /// this function. Component registration reads the slice to + /// capture body-local `mut` state for per-instance cells. + pub(crate) locals_base: usize, } /// Snapshot of the outer fun's state when a nested `fun` diff --git a/crates/compiler/zo-executor/src/tests/templates.rs b/crates/compiler/zo-executor/src/tests/templates.rs index 402e1cca..9a90322f 100644 --- a/crates/compiler/zo-executor/src/tests/templates.rs +++ b/crates/compiler/zo-executor/src/tests/templates.rs @@ -1900,3 +1900,54 @@ fun main() { ErrorKind::TypeMismatch, ); } + +#[test] +fn per_instance_state_gives_each_instance_its_own_cell() { + // Two `` instances must NOT share a `count` cell. The + // executor re-materializes the body `mut` under a synthetic + // per-instance name (`count$N`); the page must carry two + // distinct reactive text bindings, each with its own init. + assert_sir_structure( + r#" +fun counter() -> { + mut count := 0; + + return

; +} + +fun main() { + imu page ::=
+ + +
; + + #render page; +}"#, + |sir| { + // The page template carries two text bindings (one per + // instance's `{count}`), and they target distinct symbols. + let bindings = sir + .iter() + .filter_map(|i| match i { + Insn::Template { bindings, .. } => Some(bindings), + _ => None, + }) + .next_back() + .expect("page template"); + + assert_eq!( + bindings.text.len(), + 2, + "two instances → two independent text bindings: {:?}", + bindings.text + ); + + let (a, b) = (bindings.text[0].1, bindings.text[1].1); + + assert_ne!(a, b, "the two instances must bind DISTINCT state symbols"); + }, + ); +} diff --git a/crates/compiler/zo-tests/templating/zsx_counter_instances.zo b/crates/compiler/zo-tests/templating/zsx_counter_instances.zo new file mode 100644 index 00000000..172186a3 --- /dev/null +++ b/crates/compiler/zo-tests/templating/zsx_counter_instances.zo @@ -0,0 +1,22 @@ +-- test-run-pass: per-instance component state. Each `` +-- owns its own `count` cell — clicking one instance's button must +-- not change the other's display. + +fun counter(label: str) -> { + mut count := 0; + + return
+ {label} + + {count} +
; +} + +fun main() { + imu page ::=
+ + +
; + + #render page; +} From 6acdeae5dd97e33da90cd1f6bb39b9e7cca0dbf6 Mon Sep 17 00:00:00 2001 From: invisageable Date: Sat, 13 Jun 2026 11:45:34 +0200 Subject: [PATCH 5/5] feat(zo): support form controls, checkbox, radio, select --- Cargo.toml | 6 +- README.md | 10 +- crates/compiler/zo-abi/src/lib.rs | 8 +- .../compiler/zo-bundler/src/ios/simulator.rs | 21 +- crates/compiler/zo-codegen-web/src/lib.rs | 49 ++- .../compiler/zo-codegen-web/src/reactive.rs | 10 +- crates/compiler/zo-executor/src/executor.rs | 29 +- .../zo-executor/src/tests/templates.rs | 78 ++++ crates/compiler/zo-notes/public/zo.md | 6 +- crates/compiler/zo-runtime-ios/src/app.rs | 392 +++++++++++++++++- .../zo-runtime-native/src/renderer.rs | 207 ++++++++- crates/compiler/zo-runtime-render/src/aot.rs | 8 +- .../compiler/zo-runtime-render/src/layout.rs | 44 ++ crates/compiler/zo-runtime/src/lib.rs | 2 +- .../compiler/zo-tests/templating/zsx_forms.zo | 24 ++ .../zo-ui-protocol/src/ui_protocol.rs | 13 +- 16 files changed, 832 insertions(+), 75 deletions(-) create mode 100644 crates/compiler/zo-tests/templating/zsx_forms.zo diff --git a/Cargo.toml b/Cargo.toml index bc02f85f..3c7045e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,16 +231,14 @@ objc2-ui-kit = { version = "0.3", features = [ "UIVisualEffectView", "UIGlassEffect", "UIButtonConfiguration", + "UISwitch", + "UISegmentedControl", "UIImage", "UIImageView", "objc2-core-foundation", "objc2-quartz-core", "objc2-core-graphics", ] } -# The glass panel's specular rim is drawn on the effect view's -# `CALayer` (`setBorderColor` takes a `CGColor`), so quartz-core needs -# its `objc2-core-graphics` feature — `objc2-ui-kit` pulls quartz-core -# but only with default features, so enable it here for the whole graph. objc2-quartz-core = { version = "0.3", features = [ "CALayer", "objc2-core-graphics", diff --git a/README.md b/README.md index 2a11821e..43664ca8 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,12 @@ Available Memory — 9.4 GB ``` > *zo is in early development and is not yet ready for production. Currently it supports desktop (ARM64), MacOS (iOS, tvOS, visionOS, watchOS), web (bundled or webview). We plan to support more — desktop (Linux, Windows) and mobile (Android). Styling is not yet fully unified across all platforms.* -> -> *WARNiNG — regarding Ai usage, we are using Ai to build based on our architecture and specification (made by humans). The compiler currently covers over 1500 unit and integration tests.* -> -> *DiSCLAiMER — this project is about having fun again. It is maintained by a small group of volunteers during their spare time. zo is not the new X or better than Y, zo is just different. Period. Be gentle.* + +> [!IMPORTANT] +> *This project is about having fun again. It is maintained by a small group of volunteers during their spare time. We're exploring. zo is not the new X or better than Y, zo is just different. Period. Be gentle.* + +> [!CAUTION] +> *For transparency: regarding Ai usage, we're using Ai to build zo based on our architecture and specification (made by humans). The compiler currently covers over 1500 unit and integration tests.* ## sponsors & supports. diff --git a/crates/compiler/zo-abi/src/lib.rs b/crates/compiler/zo-abi/src/lib.rs index 422a31c8..e57deac9 100644 --- a/crates/compiler/zo-abi/src/lib.rs +++ b/crates/compiler/zo-abi/src/lib.rs @@ -11,8 +11,12 @@ /// Runtime ABI tag, embedded verbatim in the runtime dylib and /// scanned by `zo build` from the staged copy. /// -/// @note — BUMP THE SUFFIX whenever `ZoRuntimeContext` or any -/// `#[repr(C)]` binding ABI changes shape. +/// @note — the digits are a fixed sentinel, not a version: the +/// guard's job today is catching a runtime dylib that predates +/// tagging entirely (a flavor never rebuilt). Versioning the tag +/// for cross-binary compatibility waits until zo ships binaries to +/// users — until then, `just build_runtime` keeps the flavors in +/// lockstep with the compiler. pub const RUNTIME_ABI_TAG: &[u8; 14] = b"ZO_RT_ABI:0001"; /// The tag's scan prefix — version-independent, used to locate the diff --git a/crates/compiler/zo-bundler/src/ios/simulator.rs b/crates/compiler/zo-bundler/src/ios/simulator.rs index a5105f0a..0185a1fa 100644 --- a/crates/compiler/zo-bundler/src/ios/simulator.rs +++ b/crates/compiler/zo-bundler/src/ios/simulator.rs @@ -8,7 +8,7 @@ use std::io; use std::path::Path; -use std::process::Command; +use std::process::{Command, Stdio}; /// One Simulator device, addressed by a `simctl` specifier — a UDID /// (preferred, unambiguous) or a device name. @@ -77,9 +77,24 @@ impl Simulator { run("xcrun", &["simctl", "install", &self.device, app]) } - /// Launch the installed app by its bundle identifier. + /// Launch the installed app by its bundle identifier, streaming + /// its stdout/stderr to this terminal (`--console-pty`) so app + /// logs surface during `zo run --target ios`, like `cargo run`. + /// Blocks until the app exits (Ctrl-C to stop). fn start(&self, bundle_id: &str) -> io::Result<()> { - run("xcrun", &["simctl", "launch", &self.device, bundle_id]) + let status = Command::new("xcrun") + .args(["simctl", "launch", "--console-pty", &self.device, bundle_id]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status()?; + + if status.success() { + return Ok(()); + } + + Err(io::Error::other(format!( + "xcrun simctl launch {bundle_id}: exited with {status}" + ))) } } diff --git a/crates/compiler/zo-codegen-web/src/lib.rs b/crates/compiler/zo-codegen-web/src/lib.rs index d59204af..9aba0616 100644 --- a/crates/compiler/zo-codegen-web/src/lib.rs +++ b/crates/compiler/zo-codegen-web/src/lib.rs @@ -310,20 +310,10 @@ impl WebGen { fn emit_attr(&mut self, tag: &ElementTag, attr: &Attr) { match attr { Attr::Prop { name, value } => { - let s = value.to_display(); - let rendered = self.rewrite_attr_value(tag, name, &s); - - self - .html_buffer - .push_str(&format!(" {name}=\"{}\"", escape_html(&rendered),)); + self.emit_value_attr(tag, name, &value.to_display()); } Attr::Dynamic { name, initial, .. } => { - let s = initial.to_display(); - let rendered = self.rewrite_attr_value(tag, name, &s); - - self - .html_buffer - .push_str(&format!(" {name}=\"{}\"", escape_html(&rendered),)); + self.emit_value_attr(tag, name, &initial.to_display()); } Attr::Style { name, value } => { // Inline style shorthand — emit as a style="" segment. MVP: one @@ -342,6 +332,31 @@ impl WebGen { } } + /// Emit one HTML attribute. Boolean attributes (`checked`, + /// `disabled`, …) are present-means-true: a truthy `value` + /// writes the bare name, a falsy one writes nothing — never + /// `checked="false"`, which a browser reads as checked. Every + /// other attribute writes `name="value"`. + fn emit_value_attr(&mut self, tag: &ElementTag, name: &str, value: &str) { + if is_boolean_attr(name) { + // The push and the early return guard DIFFERENT conditions — + // the return skips the `name="value"` path for every boolean + // attr, the push fires only for truthy ones — so they can't + // collapse into one `&&`. + if value != "false" { + self.html_buffer.push_str(&format!(" {name}")); + } + + return; + } + + let rendered = self.rewrite_attr_value(tag, name, value); + + self + .html_buffer + .push_str(&format!(" {name}=\"{}\"", escape_html(&rendered))); + } + /// Per-tag attribute value rewrites. Currently only Img `src` needs /// the `zo://localhost` protocol prefix; everything else passes /// through unchanged. @@ -433,6 +448,16 @@ fn escape_html(s: &str) -> String { .replace('\'', "'") } +/// HTML boolean attributes — present means true, absent means +/// false. A reactive `checked={flag}` renders as bare `checked` +/// only when the flag is truthy. +fn is_boolean_attr(name: &str) -> bool { + matches!( + name, + "checked" | "disabled" | "selected" | "readonly" | "multiple" + ) +} + #[cfg(test)] mod tests { use super::{WebGen, escape_html, is_absolute_fs_path, normalize_uri_path}; diff --git a/crates/compiler/zo-codegen-web/src/reactive.rs b/crates/compiler/zo-codegen-web/src/reactive.rs index f490ccef..ba180acb 100644 --- a/crates/compiler/zo-codegen-web/src/reactive.rs +++ b/crates/compiler/zo-codegen-web/src/reactive.rs @@ -156,7 +156,10 @@ impl<'a> ReactiveJs<'a> { function fire(slot){{var ops=binds[slot];if(!ops)return;\ for(var i=0;i bool { /// `ElementTag`. Unknown tags fall through to /// `ElementTag::Custom` so the renderer can still stamp them /// verbatim (and component resolution is attempted one layer up). +/// Resolve a markup tag to its `ElementTag`. Delegates to +/// `ElementTag::from_name` — the single source of truth for the +/// tag table — so new tags never need a second edit here. An empty +/// tag (never produced by the parser) falls back to `Div`. fn tag_to_element(tag: &str) -> ElementTag { - match tag { - "div" => ElementTag::Div, - "section" => ElementTag::Section, - "main" => ElementTag::Main, - "article" => ElementTag::Article, - "aside" => ElementTag::Aside, - "header" => ElementTag::Header, - "footer" => ElementTag::Footer, - "nav" => ElementTag::Nav, - "form" => ElementTag::Form, - "ul" => ElementTag::Ul, - "ol" => ElementTag::Ol, - "li" => ElementTag::Li, - "span" => ElementTag::Span, - "h1" => ElementTag::H1, - "h2" => ElementTag::H2, - "h3" => ElementTag::H3, - "p" => ElementTag::P, - "img" => ElementTag::Img, - "button" => ElementTag::Button, - "input" => ElementTag::Input, - "textarea" => ElementTag::Textarea, - other => ElementTag::Custom(other.to_string()), - } + ElementTag::from_name(tag).unwrap_or(ElementTag::Div) } diff --git a/crates/compiler/zo-executor/src/tests/templates.rs b/crates/compiler/zo-executor/src/tests/templates.rs index 9a90322f..2eba07c6 100644 --- a/crates/compiler/zo-executor/src/tests/templates.rs +++ b/crates/compiler/zo-executor/src/tests/templates.rs @@ -1951,3 +1951,81 @@ fun main() { }, ); } + +#[test] +fn form_controls_emit_their_tags_and_attrs() { + // HTML-faithful surface: checkbox/radio are ``, + // dropdowns are ` + + + ; + + #render page; +}"#, + |sir| { + let page = sir + .iter() + .filter_map(|i| match i { + Insn::Template { commands, .. } => Some(commands), + _ => None, + }) + .next_back() + .expect("page template"); + + let tag_of = |tag: &ElementTag, name: &str| { + page + .iter() + .filter(|c| { + matches!(c, UiCommand::Element { tag: t, attrs, .. } + if t == tag + && attrs.iter().any(|a| a.name() == "type" + && a.as_str() == Some(name))) + }) + .count() + }; + + assert_eq!(tag_of(&ElementTag::Input, "checkbox"), 1, "checkbox"); + assert_eq!(tag_of(&ElementTag::Input, "radio"), 1, "radio"); + + let selects = page + .iter() + .filter(|c| { + matches!(c, UiCommand::Element { tag, .. } + if *tag == ElementTag::Select) + }) + .count(); + let options = page + .iter() + .filter(|c| { + matches!(c, UiCommand::Element { tag, .. } + if *tag == ElementTag::Option) + }) + .count(); + + assert_eq!(selects, 1, "one select"); + assert_eq!(options, 2, "two options"); + + // The radio's group name + value survive as attributes. + let radio_named = page.iter().any(|c| { + matches!(c, + UiCommand::Element { attrs, .. } + if attrs.iter().any(|a| a.name() == "name" + && a.as_str() == Some("size"))) + }); + + assert!(radio_named, "radio carries its group name"); + }, + ); +} diff --git a/crates/compiler/zo-notes/public/zo.md b/crates/compiler/zo-notes/public/zo.md index 27904a4c..cbf4b310 100644 --- a/crates/compiler/zo-notes/public/zo.md +++ b/crates/compiler/zo-notes/public/zo.md @@ -2,7 +2,7 @@ > *Turn your thoughts into type-safe software and Ui instantly.* -This manual is for the zo programming language. +This reference manual is for the zo programming language. @author — invisageable @author — compilords @@ -125,13 +125,13 @@ favour. Five prominent lineages contribute the most: **-cyclone-and-ada** -Cyclone is a programming language to make a low-level C-like language safe made by Nikhil Swamy, Michael Hicks, Greg Morrisett, Dan Grossman and Trevor Jim. zo adopt +Cyclone is a programming language to make a low-level C-like language safe made by Nikhil Swamy, Michael Hicks, Greg Morrisett, Dan Grossman and Trevor Jim. zo adopt Cyclone's static lifetime analysis, unique/affine pointer semantics, and lexical region drop boundaries. Ada is an extremely strong typing programming language and is designed for developing very large software systems. **-imba** -Imba is a fascinating Web programming language, the right fit to build frontend and backend made by Sindre Aarsaether and (...). zo integrated a similar system to do styling with zsx. +Imba is a fascinating Web programming language, the right fit to build frontend and backend made by Sindre Aarsaether and (...). zo integrated a similar styling system coupled to zsx, it includes shorthands `d == display`, `bg == background`, etc. **-es4-and-e4x** diff --git a/crates/compiler/zo-runtime-ios/src/app.rs b/crates/compiler/zo-runtime-ios/src/app.rs index 6c4d4e7c..f02fa2e9 100644 --- a/crates/compiler/zo-runtime-ios/src/app.rs +++ b/crates/compiler/zo-runtime-ios/src/app.rs @@ -24,8 +24,8 @@ use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, sel}; use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize}; use objc2_foundation::{ - NSBundle, NSData, NSDictionary, NSObjectProtocol, NSOperatingSystemVersion, - NSProcessInfo, NSString, + NSArray, NSBundle, NSData, NSDictionary, NSObjectProtocol, + NSOperatingSystemVersion, NSProcessInfo, NSString, }; #[cfg(target_os = "watchos")] use objc2_ui_kit::UIScreen; @@ -33,8 +33,9 @@ use objc2_ui_kit::{ UIApplication, UIApplicationLaunchOptionsKey, UIButton, UIButtonConfiguration, UIButtonType, UIColor, UIControlEvents, UIControlState, UIFont, UIGlassEffect, UIGlassEffectStyle, UIImage, - UIImageView, UILabel, UITextBorderStyle, UITextField, UIView, - UIViewContentMode, UIViewController, UIVisualEffectView, UIWindow, + UIImageView, UILabel, UISegmentedControl, UISwitch, UITextBorderStyle, + UITextField, UIView, UIViewContentMode, UIViewController, UIVisualEffectView, + UIWindow, }; #[cfg(target_os = "ios")] use objc2_ui_kit::{ @@ -121,6 +122,13 @@ enum PlacedView { /// An editable text input (`` / `