Skip to main content

pathland_core/
lib.rs

1//! # pathland-core
2//!
3//! The 16-byte fixed-size opcode engine for Pathland: fixed-size opcodes, a
4//! single-producer ring buffer, a bump arena for variable-length data, and the
5//! linear-memory layout shared with host renderers.
6//!
7//! This crate is `no_std` + `alloc`. It has no runtime dependencies.
8//!
9//! ## Producer (guest engine)
10//!
11//! ```no_run
12//! use pathland_core::{init_memory, Guest, MemoryLayout};
13//!
14//! let layout = MemoryLayout::default();
15//! let mut mem = vec![0u8; layout.total_bytes()];
16//! init_memory(&mut mem, &layout);
17//!
18//! let mut guest = Guest::new(&mut mem, &layout);
19//! guest.begin_frame();
20//! guest.create_node(1, pathland_core::component_type::VSTACK).unwrap();
21//! guest.insert_child(0, 1, pathland_core::APPEND).unwrap();
22//! guest.set_property(1, pathland_core::property_id::SPACING, pathland_core::value_type::F32, 4.0f32.to_bits()).unwrap();
23//! guest.end_frame(); // publishes frameCount
24//! ```
25//!
26//! ## Consumer (host renderer)
27//!
28//! ```no_run
29//! use pathland_core::{init_memory, Guest, Host, MemoryLayout};
30//! # let layout = MemoryLayout::default();
31//! # let mut mem = vec![0u8; layout.total_bytes()];
32//! # init_memory(&mut mem, &layout);
33//! # let mut guest = Guest::new(&mut mem, &layout);
34//! # guest.begin_frame();
35//! # guest.create_node(1, 1).unwrap();
36//! # guest.end_frame();
37//! let mut host = Host::new(&mut mem, &layout);
38//! for frame in host.frames() {
39//!     for op in frame.opcodes() {
40//!         println!("{:?}", op);
41//!     }
42//! }
43//! ```
44
45#![no_std]
46#![forbid(unsafe_op_in_unsafe_fn)]
47
48#[cfg(test)]
49extern crate std;
50
51extern crate alloc;
52
53mod arena;
54mod conformance;
55mod constants;
56mod events;
57pub mod fit;
58mod memory;
59mod opcode;
60mod ring;
61pub mod tokens;
62
63pub use arena::ArenaError;
64pub use constants::*;
65pub use events::{Event, EventError};
66pub use memory::MemoryLayout;
67pub use opcode::{Opcode, OPCODE_SIZE};
68pub use ring::RingError;
69
70use alloc::vec;
71use alloc::vec::Vec;
72use arena as arena_fn;
73use memory::header;
74use memory::{OFF_FRAME_COUNT, OFF_READ_CURSOR, OFF_SLOT_COUNT, OFF_WRITE_CURSOR};
75use ring as ring_fn;
76
77/// Initialize the linear memory header for a layout. Call once at startup
78/// before creating a `Guest` or `Host`.
79pub fn init_memory(mem: &mut [u8], layout: &MemoryLayout) {
80    header::init(mem, layout);
81}
82
83/// Accessor for a shared-memory ring header, for consumers that read the ring
84/// in place (zero-copy) without a `Host` handle — e.g. a foreign driver that
85/// holds a raw pointer to the shared linear memory.
86///
87/// Field offsets match [`memory::OFF_*`] constants and are part of the stable
88/// wire layout (see `spec/OPCODE.md`).
89pub struct SharedHeader<'a> {
90    mem: &'a [u8],
91}
92
93impl<'a> SharedHeader<'a> {
94    /// Wrap an immutable view of the shared linear memory.
95    pub fn new(mem: &'a [u8]) -> Self {
96        Self { mem }
97    }
98
99    fn u32(&self, off: usize) -> u32 {
100        u32::from_le_bytes(self.mem[off..off + 4].try_into().unwrap())
101    }
102
103    /// `frameCount` — the completed-frame watermark.
104    pub fn frame_count(&self) -> u32 {
105        self.u32(memory::OFF_FRAME_COUNT)
106    }
107
108    /// `readCursor` — the slot the consumer has consumed up to (masked).
109    pub fn read_cursor(&self) -> u32 {
110        self.u32(memory::OFF_READ_CURSOR)
111    }
112
113    /// `writeCursor` — the slot the producer has written up to (masked).
114    pub fn write_cursor(&self) -> u32 {
115        self.u32(memory::OFF_WRITE_CURSOR)
116    }
117
118    /// `slotCount` — number of ring slots (power of two).
119    pub fn slot_count(&self) -> u32 {
120        self.u32(memory::OFF_SLOT_COUNT)
121    }
122
123    /// Byte offset of the ring region within the linear memory.
124    pub fn ring_offset(&self) -> usize {
125        self.u32(memory::OFF_RING_OFFSET) as usize
126    }
127
128    /// Byte length of the ring region.
129    pub fn ring_bytes(&self) -> usize {
130        self.u32(memory::OFF_RING_BYTES) as usize
131    }
132
133    /// Byte offset of the arena region within the linear memory.
134    pub fn arena_offset(&self) -> usize {
135        self.u32(memory::OFF_ARENA_OFFSET) as usize
136    }
137
138    /// Byte offset of the arena cursor.
139    pub fn arena_cursor(&self) -> u32 {
140        self.u32(memory::OFF_ARENA_CURSOR)
141    }
142}
143
144/// Number of ring slots per the header.
145fn slot_count(header: &[u8]) -> usize {
146    header::get_u32(header, OFF_SLOT_COUNT) as usize
147}
148
149/// Ring mask per the header (slot count is a power of two).
150fn slot_mask(header: &[u8]) -> usize {
151    let slots = slot_count(header);
152    debug_assert!(slots.is_power_of_two());
153    slots - 1
154}
155
156// ---------------------------------------------------------------------------
157// Guest (producer)
158// ---------------------------------------------------------------------------
159
160/// Producer side: the guest engine writing opcodes and arena data into shared
161/// linear memory. Owns the header, ring, and arena regions as disjoint slices.
162pub struct Guest<'a> {
163    header: &'a mut [u8],
164    slots: &'a mut [u8],
165    /// Host → guest event ring (guest reads raw inputs here).
166    event_slots: &'a mut [u8],
167    arena: &'a mut [u8],
168    /// Host → guest event arena (host-owned bump; the guest resolves `TEXT_CHANGED`
169    /// text here over the shared ring).
170    event_arena: &'a mut [u8],
171    /// Write cursor captured at `begin_frame`.
172    frame_start: u32,
173}
174
175impl<'a> Guest<'a> {
176    /// Create a guest view over the linear memory. `init_memory` MUST have been
177    /// called on the same memory beforehand.
178    pub fn new(mem: &'a mut [u8], layout: &MemoryLayout) -> Self {
179        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
180        let (slots, rest) = rest.split_at_mut(layout.ring_bytes());
181        let (event_slots, rest) = rest.split_at_mut(layout.event_ring_bytes());
182        let (arena, event_arena) = rest.split_at_mut(layout.arena_bytes);
183        let frame_start = header::get_u32(header, OFF_WRITE_CURSOR);
184        Self {
185            header,
186            slots,
187            event_slots,
188            arena,
189            event_arena,
190            frame_start,
191        }
192    }
193
194    fn mask(&self) -> usize {
195        slot_mask(self.header)
196    }
197
198    /// Record the start of a new frame.
199    pub fn begin_frame(&mut self) {
200        self.frame_start = header::get_u32(self.header, OFF_WRITE_CURSOR);
201    }
202
203    /// Number of opcodes written since the last `begin_frame`.
204    pub fn pending(&self) -> usize {
205        let write = header::get_u32(self.header, OFF_WRITE_CURSOR);
206        let mask = self.mask() as u32;
207        (write.wrapping_sub(self.frame_start) & mask) as usize
208    }
209
210    /// End the frame: increments `frameCount`.
211    pub fn end_frame(&mut self) {
212        let count = header::get_u32(self.header, OFF_FRAME_COUNT);
213        header::set_u32(self.header, OFF_FRAME_COUNT, count.wrapping_add(1));
214    }
215
216    /// Total frames produced so far.
217    pub fn frame_count(&self) -> u32 {
218        header::get_u32(self.header, OFF_FRAME_COUNT)
219    }
220
221    // --- TREE ---
222
223    pub fn create_node(&mut self, node_id: u32, component_type: u16) -> Result<(), RingError> {
224        ring_fn::push_create_node(self.slots, self.header, self.mask(), node_id, component_type)
225    }
226
227    pub fn delete_node(&mut self, node_id: u32) -> Result<(), RingError> {
228        ring_fn::push_delete_node(self.slots, self.header, self.mask(), node_id)
229    }
230
231    pub fn insert_child(&mut self, parent: u32, child: u32, index: u32) -> Result<(), RingError> {
232        ring_fn::push_insert_child(self.slots, self.header, self.mask(), parent, child, index)
233    }
234
235    pub fn remove_child(&mut self, parent: u32, child: u32) -> Result<(), RingError> {
236        ring_fn::push_remove_child(self.slots, self.header, self.mask(), parent, child)
237    }
238
239    pub fn move_child(
240        &mut self,
241        parent: u32,
242        child: u32,
243        new_index: u32,
244    ) -> Result<(), RingError> {
245        ring_fn::push_move_child(self.slots, self.header, self.mask(), parent, child, new_index)
246    }
247
248    // --- Raw opcode push (guest → host) ---
249
250    /// Push a caller-built raw opcode into the ring. This is the escape hatch
251    /// for foreign hosts (C ABI / FFM) that construct the 16-byte opcode
252    /// themselves and write it into the shared ring in place.
253    pub fn push_opcode(&mut self, op: Opcode) -> Result<(), RingError> {
254        ring_fn::push(self.slots, self.header, self.mask(), &op)
255    }
256
257    // --- STYLE ---
258
259    pub fn set_property(
260        &mut self,
261        node_id: u32,
262        property_id: u16,
263        value_type: u8,
264        value: u32,
265    ) -> Result<(), RingError> {
266        ring_fn::push_set_property(
267            self.slots,
268            self.header,
269            self.mask(),
270            node_id,
271            property_id,
272            value_type,
273            value,
274        )
275    }
276
277    /// Set a node's text content via the arena.
278    pub fn set_text(&mut self, node_id: u32, text: &str) -> Result<u32, RingError> {
279        let arena_ref = arena_fn::alloc_str(self.arena, self.header, text).map_err(|_| RingError::Full)?;
280        ring_fn::push_set_text(self.slots, self.header, self.mask(), node_id, arena_ref)?;
281        Ok(arena_ref)
282    }
283
284    /// Set a node's date value via `PARAMETER::SET_DATE` (draft): `days` since the
285    /// epoch (I32, pre-1970 negative) and `millis` of day (U32, 0..86,400,000).
286    pub fn set_date(&mut self, node_id: u32, days: i32, millis: u32) -> Result<(), RingError> {
287        let op = Opcode::new(
288            category::PARAMETER,
289            crate::parameter::SET_DATE,
290            0,
291            node_id,
292            days as u32,
293            millis,
294        );
295        ring_fn::push(self.slots, self.header, self.mask(), &op)
296    }
297
298    /// Override a design token globally via `PARAMETER::SET_DESIGN_TOKEN`:
299    /// `path` (arena string) → `value` of `value_type`. Applies everywhere the
300    /// renderer resolves tokens (spec/TOKENS.md). A `dark.`-prefixed path
301    /// overrides the dark variant. Returns the arena offset of the path.
302    pub fn set_design_token(
303        &mut self,
304        path: &str,
305        value_type: u8,
306        value: u32,
307    ) -> Result<u32, RingError> {
308        let arena_ref = arena_fn::alloc_str(self.arena, self.header, path)
309            .map_err(|_| RingError::Full)?;
310        let op = Opcode::new(
311            category::PARAMETER,
312            crate::parameter::SET_DESIGN_TOKEN,
313            0,
314            arena_ref,
315            u32::from(value_type),
316            value,
317        );
318        ring_fn::push(self.slots, self.header, self.mask(), &op)?;
319        Ok(arena_ref)
320    }
321
322    /// Override a design token with a **STRING value** via `PARAMETER::SET_DESIGN_TOKEN`
323    /// (spec/TOKENS.md): both the token path and the value are arena strings —
324    /// `A` = path ref, `B` = `STRING` value type, `C` = value ref. Returns the
325    /// arena offset of the path.
326    pub fn set_design_token_string(
327        &mut self,
328        path: &str,
329        value: &str,
330    ) -> Result<u32, RingError> {
331        let arena_ref = arena_fn::alloc_str(self.arena, self.header, path)
332            .map_err(|_| RingError::Full)?;
333        let value_ref = arena_fn::alloc_str(self.arena, self.header, value)
334            .map_err(|_| RingError::Full)?;
335        let op = Opcode::new(
336            category::PARAMETER,
337            crate::parameter::SET_DESIGN_TOKEN,
338            0,
339            arena_ref,
340            u32::from(crate::value_type::STRING),
341            value_ref,
342        );
343        ring_fn::push(self.slots, self.header, self.mask(), &op)?;
344        Ok(arena_ref)
345    }
346
347    /// Allocate arbitrary bytes into the arena, returning the offset.
348    pub fn alloc(&mut self, bytes: &[u8]) -> Result<u32, ArenaError> {
349        arena_fn::alloc(self.arena, self.header, bytes)
350    }
351
352    /// Allocate a string into the arena, returning the offset.
353    pub fn alloc_str(&mut self, s: &str) -> Result<u32, ArenaError> {
354        arena_fn::alloc_str(self.arena, self.header, s)
355    }
356
357    // --- META ---
358
359    pub fn reset(&mut self) -> Result<(), RingError> {
360        ring_fn::push_reset(self.slots, self.header, self.mask())
361    }
362
363    // --- EVENT ingress (host → guest) ---
364
365    /// Drain all pending raw-input events written by the host into the event
366    /// ring, decoding each 16-byte `EVENT` opcode into a typed [`Event`].
367    ///
368    /// The guest (app/engine) polls this to receive renderer-reported inputs.
369    pub fn drain_events(&mut self) -> Vec<Event> {
370        let mask = self.mask();
371        let ops = ring_fn::read_events(&self.event_slots, self.header, mask, usize::MAX);
372        ops.into_iter()
373            .filter_map(|op| crate::events::decode_event(&op, self.event_arena))
374            .collect()
375    }
376
377    /// Drain raw `EVENT`-category opcodes from the host → guest event ring as
378    /// their 16-byte wire form (no decoding). Used by foreign hosts that want
379    /// the raw bytes.
380    pub fn drain_event_opcodes(&mut self) -> Vec<Opcode> {
381        let mask = self.mask();
382        ring_fn::read_events(&self.event_slots, self.header, mask, usize::MAX)
383    }
384
385    /// Drain at most `max` raw `EVENT`-category opcodes, leaving the rest
386    /// pending for a later call (so a caller with a fixed-size buffer never
387    /// drops events it couldn't copy).
388    pub fn drain_event_opcodes_max(&mut self, max: usize) -> Vec<Opcode> {
389        let mask = self.mask();
390        ring_fn::read_events(&self.event_slots, self.header, mask, max)
391    }
392
393    /// The host → guest event arena bump cursor (diagnostics/tests).
394    pub fn event_arena_cursor(&self) -> u32 {
395        header::get_u32(self.header, memory::OFF_EVENT_ARENA_CURSOR)
396    }
397
398    /// Push a raw `EVENT`-category opcode into the host → guest event ring.
399    ///
400    /// This is the host-side write (the renderer reporting an input). Exposed
401    /// here (guest can hold the ring too) so a C-ABI shim can inject raw event
402    /// opcodes without round-tripping through the typed [`Event`] enum.
403    pub fn push_event_opcode(&mut self, op: Opcode) -> Result<(), RingError> {
404        ring_fn::push_event(self.event_slots, self.header, self.mask(), &op)
405    }
406}
407
408// ---------------------------------------------------------------------------
409// Host (consumer)
410// ---------------------------------------------------------------------------
411
412/// A completed frame produced by the guest: a sequence of opcodes plus arena
413/// access.
414///
415/// `Frame` is the **transport seam**: both the shared-memory ring (`Host`) and
416/// network batch decoders produce frame views, so consumers such as
417/// `RenderTree::apply_frame` work identically regardless of origin.
418pub struct Frame<'a> {
419    slots: &'a [u8],
420    arena: &'a [u8],
421    start: usize,
422    end: usize,
423}
424
425impl<'a> Frame<'a> {
426    /// Construct a frame view over contiguous 16-byte opcode slots plus an
427    /// arena region (`start`/`end` are byte offsets into `slots`).
428    ///
429    /// Used by transport backends to produce `Frame` views over their own
430    /// buffers; `Host` builds these internally from the ring.
431    pub fn from_parts(slots: &'a [u8], arena: &'a [u8], start: usize, end: usize) -> Self {
432        Self {
433            slots,
434            arena,
435            start,
436            end,
437        }
438    }
439
440    /// Iterate the opcodes in this frame, in order.
441    pub fn opcodes(&self) -> FrameIter<'a> {
442        FrameIter {
443            slots: self.slots,
444            cursor: self.start,
445            end: self.end,
446        }
447    }
448
449    /// Number of opcodes in the frame.
450    pub fn len(&self) -> usize {
451        (self.end - self.start) / Opcode::SIZE
452    }
453
454    pub fn is_empty(&self) -> bool {
455        self.len() == 0
456    }
457
458    /// Read an arena entry by offset (see `Guest::alloc` / `Guest::alloc_str`).
459    pub fn arena_get(&self, offset: u32) -> Result<&'a [u8], ArenaError> {
460        arena_fn::get(self.arena, offset)
461    }
462
463    /// Read an arena string by offset.
464    pub fn arena_str(&self, offset: u32) -> Result<&'a str, ArenaError> {
465        arena_fn::get_str(self.arena, offset)
466    }
467
468    /// The full arena region (for transport serialization of arena deltas).
469    pub fn arena(&self) -> &'a [u8] {
470        self.arena
471    }
472}
473
474/// Iterator over the opcodes in a `Frame`.
475pub struct FrameIter<'a> {
476    slots: &'a [u8],
477    cursor: usize,
478    end: usize,
479}
480
481impl<'a> Iterator for FrameIter<'a> {
482    type Item = Opcode;
483
484    fn next(&mut self) -> Option<Opcode> {
485        if self.cursor >= self.end {
486            return None;
487        }
488        let bytes: [u8; 16] = self.slots[self.cursor..self.cursor + 16].try_into().unwrap();
489        self.cursor += 16;
490        Some(Opcode::from_bytes(&bytes))
491    }
492}
493
494/// Consumer side: the host renderer reading completed frames.
495///
496/// SPSC contract: the host never reads past the producer's published
497/// `writeCursor`, and the producer never overwrites an unconsumed `readCursor`.
498/// If the host is slow, multiple produced frames are coalesced into one `Frame`
499/// (opcodes remain in order); the `frameCount` simply signals "new data".
500pub struct Host<'a> {
501    header: &'a mut [u8],
502    slots: &'a [u8],
503    /// Host → guest event ring (host writes raw inputs here).
504    event_slots: &'a mut [u8],
505    arena: &'a [u8],
506    /// Host → guest event arena (host-owned bump; the host writes event string
507    /// payloads here, e.g. `TEXT_CHANGED` text over the shared ring).
508    event_arena: &'a mut [u8],
509    /// Number of frames already consumed by this host view.
510    consumed: u32,
511}
512
513impl<'a> Host<'a> {
514    /// Create a host view over the linear memory.
515    pub fn new(mem: &'a mut [u8], layout: &MemoryLayout) -> Self {
516        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
517        let (slots, rest) = rest.split_at_mut(layout.ring_bytes());
518        let (event_slots, rest) = rest.split_at_mut(layout.event_ring_bytes());
519        let (arena, event_arena) = rest.split_at_mut(layout.arena_bytes);
520        let slots: &'a [u8] = slots;
521        let arena: &'a [u8] = arena;
522        Self {
523            header,
524            slots,
525            event_slots,
526            arena,
527            event_arena,
528            consumed: 0,
529        }
530    }
531
532    /// Total frames produced by the guest (for diagnostics).
533    pub fn frame_count(&self) -> u32 {
534        header::get_u32(self.header, OFF_FRAME_COUNT)
535    }
536
537    /// Consume all frames produced since the last call, advancing `readCursor`.
538    ///
539    /// Returns at most one `Frame` (all new opcodes since the last drain). If
540    /// the producer produced multiple frames before this call they coalesce.
541    pub fn frames(&mut self) -> Vec<Frame<'a>> {
542        let frame_count = header::get_u32(self.header, OFF_FRAME_COUNT);
543        if self.consumed >= frame_count {
544            return Vec::new();
545        }
546        let mask = slot_mask(self.header);
547        let start = ring_fn::cursor(self.header, OFF_READ_CURSOR, mask);
548        let end = ring_fn::cursor(self.header, OFF_WRITE_CURSOR, mask);
549
550        // The masked write cursor can wrap past the read cursor (the producer
551        // laps the reader numerically). The pending run is counted modulo the
552        // ring size; a plain `end < start` guard would permanently deadlock the
553        // ring at the wrap boundary (the reader never advances again). A
554        // wrapped run is drained in two calls — first the [start..slots) head,
555        // then the [0..end) tail — advancing `readCursor` between them, so the
556        // opcodes still reach the consumer in ring order.
557        let count = end.wrapping_sub(start) & mask;
558        if count == 0 {
559            return Vec::new();
560        }
561        let run_end = (start + count) & mask; // exclusive slot index of the full run
562        let (drain_to_slot, frame_byte_end) = if run_end > start {
563            (run_end, run_end * Opcode::SIZE)
564        } else {
565            (0, self.slots.len()) // wrapped: drain the head now, the tail next call
566        };
567
568        let frames = vec![Frame {
569            slots: self.slots,
570            arena: self.arena,
571            start: start * Opcode::SIZE,
572            end: frame_byte_end,
573        }];
574
575        header::set_u32(self.header, OFF_READ_CURSOR, drain_to_slot as u32);
576        self.consumed = frame_count;
577
578        // A `META::RESET` invalidates prior state: reset the host → guest event
579        // arena cursor so the host starts bump-allocating fresh (matching the
580        // guest arena's reset-on-RESET semantics).
581        let drained_end_byte = if drain_to_slot > start {
582            drain_to_slot * Opcode::SIZE
583        } else {
584            self.slots.len() // wrapped head: drained [start .. ring end)
585        };
586        let reset_seen = self.slots[start * Opcode::SIZE..drained_end_byte]
587            .chunks_exact(Opcode::SIZE)
588            .any(|slot| {
589                let bytes: [u8; 16] = slot.try_into().unwrap();
590                let op = Opcode::from_bytes(&bytes);
591                op.category() == category::META && op.command() == crate::meta::RESET
592            });
593        if reset_seen {
594            memory::header::set_u32(self.header, memory::OFF_EVENT_ARENA_CURSOR, 0);
595        }
596
597        frames
598    }
599
600    /// Write a raw-input event (host → guest) into the event ring, encoded as a
601    /// 16-byte `EVENT` opcode. The renderer reports inputs here; the guest
602    /// drains them via [`Guest::drain_events`].
603    ///
604    /// `TextChanged` text and `Navigate` URLs are bump-allocated into the
605    /// host → guest **event arena** and referenced by their absolute offset in
606    /// `B` (the `SET_TEXT` shared-memory convention); the guest resolves them
607    /// from the same region. If the event arena is full the event is rejected
608    /// with [`RingError::Full`] (backpressure).
609    pub fn send_event(&mut self, event: &Event) -> Result<(), RingError> {
610        let mask = slot_mask(self.header);
611        let op = match event {
612            Event::TextChanged { target, value } => {
613                let offset = crate::arena::alloc_event(&mut self.event_arena, self.header, value.as_bytes())
614                    .map_err(|_| RingError::Full)?;
615                Opcode::new(category::EVENT, crate::event::TEXT_CHANGED, 0, *target, offset, 0)
616            }
617            Event::Navigate { url: Some(url) } => {
618                let offset = crate::arena::alloc_event(&mut self.event_arena, self.header, url.as_bytes())
619                    .map_err(|_| RingError::Full)?;
620                Opcode::new(
621                    category::EVENT,
622                    crate::event::NAVIGATE,
623                    crate::flag::NAVIGATE_URL,
624                    0,
625                    offset,
626                    0,
627                )
628            }
629            _ => event.encode(),
630        };
631        ring_fn::push_event(self.event_slots, self.header, mask, &op)
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use crate::constants::{category, component_type, property_id, value_type};
639    use alloc::vec;
640
641    #[test]
642    fn guest_host_round_trip() {
643        let layout = MemoryLayout::default();
644        let mut mem = vec![0u8; layout.total_bytes()];
645        init_memory(&mut mem, &layout);
646
647        let mut guest = Guest::new(&mut mem, &layout);
648        guest.begin_frame();
649        guest.create_node(1, component_type::VSTACK).unwrap();
650        guest.insert_child(0, 1, APPEND).unwrap();
651        guest.set_property(1, property_id::SPACING, value_type::F32, 4.0f32.to_bits()).unwrap();
652        guest
653            .set_property(1, property_id::PADDING, value_type::F32, 8.0f32.to_bits())
654            .unwrap();
655        let arena_ref = guest.set_text(1, "Hello").unwrap();
656        assert_eq!(guest.pending(), 5);
657        guest.end_frame();
658        assert_eq!(guest.frame_count(), 1);
659
660        let mut host = Host::new(&mut mem, &layout);
661        assert_eq!(host.frame_count(), 1);
662        let frames = host.frames();
663        assert_eq!(frames.len(), 1);
664        let frame = &frames[0];
665        let ops: Vec<Opcode> = frame.opcodes().collect();
666        assert_eq!(ops.len(), 5);
667        assert_eq!(ops[0].category(), category::TREE);
668        assert_eq!(ops[0].a(), 1);
669        assert_eq!(ops[1].a(), 0); // parent root
670        assert_eq!(ops[2].b() as u16, property_id::SPACING);
671        assert_eq!(ops[2].c_f32(), 4.0);
672        assert_eq!(frame.arena_str(arena_ref).unwrap(), "Hello");
673        // Second drain is empty.
674        assert!(host.frames().is_empty());
675    }
676
677    #[test]
678    fn set_design_token_round_trips() {
679        let layout = MemoryLayout::default();
680        let mut mem = vec![0u8; layout.total_bytes()];
681        init_memory(&mut mem, &layout);
682
683        let mut guest = Guest::new(&mut mem, &layout);
684        guest.begin_frame();
685        let arena_ref = guest
686            .set_design_token("color.primary", value_type::COLOR, 0xFF_2563EB)
687            .unwrap();
688        let dark_ref = guest
689            .set_design_token("dark.color.primary", value_type::COLOR, 0xFF_60A5FA)
690            .unwrap();
691        guest.end_frame();
692
693        let mut host = Host::new(&mut mem, &layout);
694        let frame = &host.frames()[0];
695        let ops: Vec<Opcode> = frame.opcodes().collect();
696        assert_eq!(ops.len(), 2);
697        assert_eq!(ops[0].category(), category::PARAMETER);
698        assert_eq!(ops[0].command(), parameter::SET_DESIGN_TOKEN);
699        assert_eq!(ops[0].b(), u32::from(value_type::COLOR));
700        assert_eq!(ops[0].c(), 0xFF_2563EB);
701        assert_eq!(frame.arena_str(arena_ref).unwrap(), "color.primary");
702        assert_eq!(frame.arena_str(dark_ref).unwrap(), "dark.color.primary");
703    }
704
705    #[test]
706    fn set_design_token_string_round_trips() {
707        let layout = MemoryLayout::default();
708        let mut mem = vec![0u8; layout.total_bytes()];
709        init_memory(&mut mem, &layout);
710
711        let mut guest = Guest::new(&mut mem, &layout);
712        guest.begin_frame();
713        let path_ref = guest.set_design_token_string("font.body.family", "Inter").unwrap();
714        guest.end_frame();
715
716        let mut host = Host::new(&mut mem, &layout);
717        let frame = &host.frames()[0];
718        let ops: Vec<Opcode> = frame.opcodes().collect();
719        assert_eq!(ops.len(), 1);
720        assert_eq!(ops[0].category(), category::PARAMETER);
721        assert_eq!(ops[0].command(), parameter::SET_DESIGN_TOKEN);
722        assert_eq!(ops[0].b(), u32::from(value_type::STRING));
723        assert_eq!(frame.arena_str(ops[0].a()).unwrap(), "font.body.family");
724        assert_eq!(frame.arena_str(ops[0].c()).unwrap(), "Inter");
725        assert_eq!(path_ref, ops[0].a());
726    }
727
728    #[test]
729    fn ring_full_is_reported() {
730        let mut layout = MemoryLayout::default();
731        layout.slot_count = 4; // tiny ring
732        let mut mem = vec![0u8; layout.total_bytes()];
733        init_memory(&mut mem, &layout);
734
735        let mut guest = Guest::new(&mut mem, &layout);
736        // 4 slots -> capacity is slot_count-1 = 3 opcodes before full.
737        assert!(guest.create_node(1, component_type::VSTACK).is_ok());
738        assert!(guest.create_node(2, component_type::HSTACK).is_ok());
739        assert!(guest.create_node(3, component_type::TEXT).is_ok());
740        assert_eq!(
741            guest.create_node(4, component_type::TEXT).unwrap_err(),
742            RingError::Full
743        );
744    }
745
746    #[test]
747    fn host_event_round_trips_to_guest() {
748        let layout = MemoryLayout::default();
749        let mut mem = vec![0u8; layout.total_bytes()];
750        init_memory(&mut mem, &layout);
751
752        // Host (renderer) writes an event.
753        let mut host = Host::new(&mut mem, &layout);
754        host.send_event(&Event::PointerUp {
755            target: 4,
756            x: 10.0,
757            y: 20.0,
758            secondary: false,
759        })
760        .unwrap();
761        drop(host);
762
763        // Guest drains and decodes it.
764        let mut guest = Guest::new(&mut mem, &layout);
765        let events = guest.drain_events();
766        assert_eq!(
767            events,
768            vec![Event::PointerUp {
769                target: 4,
770                x: 10.0,
771                y: 20.0,
772                secondary: false,
773            }]
774        );
775        // Second drain is empty.
776        assert!(guest.drain_events().is_empty());
777    }
778
779    #[test]
780    fn drain_events_skips_unknown_event_commands() {
781        let layout = MemoryLayout::default();
782        let mut mem = vec![0u8; layout.total_bytes()];
783        init_memory(&mut mem, &layout);
784
785        // Write a valid event into the event ring, then release the host borrow.
786        {
787            let mut host = Host::new(&mut mem, &layout);
788            host.send_event(&Event::PointerUp {
789                target: 4,
790                x: 10.0,
791                y: 20.0,
792                secondary: false,
793            })
794            .unwrap();
795        }
796
797        // Push a second, malformed EVENT opcode directly into the ring.
798        let mask = slot_mask(&mem[..memory::HEADER_SIZE]);
799        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
800        let (_slots, rest) = rest.split_at_mut(layout.ring_bytes());
801        let (event_slots, _arena) = rest.split_at_mut(layout.event_ring_bytes());
802        let bogus = Opcode::new(crate::category::EVENT, 0xFF, 0, 1, 0, 0);
803        ring_fn::push_event(event_slots, header, mask, &bogus).unwrap();
804
805        // The guest sees only the decodable event; the unknown command is dropped.
806        let mut guest = Guest::new(&mut mem, &layout);
807        let events = guest.drain_events();
808        assert_eq!(events.len(), 1);
809        assert_eq!(
810            events[0],
811            Event::PointerUp {
812                target: 4,
813                x: 10.0,
814                y: 20.0,
815                secondary: false,
816            }
817        );
818    }
819
820    #[test]
821    fn host_frames_survives_a_wrapped_write_cursor() {
822        let layout = MemoryLayout {
823            slot_count: 4,
824            arena_bytes: MemoryLayout::default().arena_bytes,
825            event_arena_bytes: MemoryLayout::default().event_arena_bytes,
826        };
827        let mut mem = vec![0u8; layout.total_bytes()];
828        init_memory(&mut mem, &layout);
829
830        // Guest writes three opcodes (slots 0,1,2; capacity is slot_count-1).
831        {
832            let mut guest = Guest::new(&mut mem, &layout);
833            guest.begin_frame();
834            guest.create_node(1, component_type::VSTACK).unwrap();
835            guest.create_node(2, component_type::HSTACK).unwrap();
836            guest.create_node(3, component_type::TEXT).unwrap();
837            guest.end_frame();
838        }
839        // Host drains everything -> readCursor = 3.
840        {
841            let mut host = Host::new(&mut mem, &layout);
842            assert_eq!(host.frames().len(), 1);
843        }
844        // Guest writes one more opcode -> slot 3, writeCursor wraps 3 -> 0.
845        // Now masked write(0) < masked read(3) — the wrap that used to
846        // permanently deadlock the frame ring.
847        {
848            let mut guest = Guest::new(&mut mem, &layout);
849            guest.begin_frame();
850            guest.create_node(4, component_type::TEXT).unwrap();
851            guest.end_frame();
852        }
853
854        // The wrapped run is the head [3..4) + a (here empty) tail [0..0): the
855        // first read drains the head in ring order...
856        let mut host = Host::new(&mut mem, &layout);
857        let frames = host.frames();
858        assert_eq!(frames.len(), 1);
859        assert_eq!(frames[0].opcodes().count(), 1);
860        assert_eq!(frames[0].opcodes().next().unwrap().a(), 4);
861        // ...and readCursor advanced to 0, so nothing is left pending.
862        assert!(host.frames().is_empty());
863        // And the producer can wrap again without ever deadlocking.
864        {
865            let mut guest = Guest::new(&mut mem, &layout);
866            guest.begin_frame();
867            guest.create_node(5, component_type::TEXT).unwrap();
868            guest.end_frame();
869        }
870        let mut host = Host::new(&mut mem, &layout);
871        assert_eq!(host.frames()[0].opcodes().next().unwrap().a(), 5);
872    }
873
874    #[test]
875    fn icon_vocabulary_names_are_canonical_and_unique() {
876        use crate::constants::icon;
877        use alloc::collections::BTreeSet;
878
879        let mut seen = BTreeSet::new();
880        for name in icon::NAMES {
881            assert!(icon::is_canonical(name), "{} not canonical", name);
882            assert!(seen.insert(name), "duplicate canonical icon {}", name);
883        }
884        // Every documented constant is in the canonical list.
885        for name in [
886            icon::HOME,
887            icon::SETTINGS,
888            icon::PLAY,
889            icon::SKIP_FORWARD,
890            icon::VOLUME_MUTE,
891            icon::FILTER,
892        ] {
893            assert!(icon::is_canonical(name));
894        }
895        assert!(!icon::is_canonical("definitely-not-an-icon"));
896    }
897}