//! Source paint dispatch produces checked immutable commands. One read-only VM //! resumable call sequences all overrides, sharing one instruction/frame budget. use crate::{ document::{Frame, RenderCommand}, layout::{self, Size}, mount::Mount, object_list, ui_bridge::{MAX_DIMENSION, UiNode, attach_desktop_type}, }; use anyhow::{Result, bail}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError, WeakObjectRef, }; use std::{ collections::{BTreeMap, HashMap}, sync::{Arc, Mutex}, }; fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } fn object(value: Value) -> Result { if let Value::Object(object) = value { Ok(object) } else { Err(invalid("paint must return managed objects")) } } fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result { if let Value::Text(value) = store.get_attr(object, name)? { Ok(value) } else { Err(invalid("paint field must be text")) } } fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result { let Value::Number(value) = store.get_attr(object, name)? else { return Err(invalid("paint field must be a Number")); }; let value = value .to_string() .parse::() .map_err(|_| invalid("invalid paint number"))?; if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 || (!matches!(name, "x" | "y") && value < 0.0) { return Err(invalid("paint number exceeds bounds")); } Ok(value) } fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result { let registry = object(store.get_attr(desktop, "types")?)?; object( store .dict_get(registry, &Value::Text(name.into()))? .ok_or(invalid("missing paint type"))?, ) } fn decode( store: &ObjectStore, command: ObjectRef, expected: ObjectRef, ) -> Result { if !store.is_instance(command, expected)? { return Err(invalid("paint commands must be PaintCommand instances")); } let x = number(store, command, "x")?; let y = number(store, command, "y")?; let color = text(store, command, "color")?; let kind = text(store, command, "kind")?; let result = match kind.as_str() { "roundedRect" | "strokeRoundedRect" => { let width = number(store, command, "width")?; let height = number(store, command, "height")?; let radius = number(store, command, "radius")?; if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 { return Err(invalid("paint rectangle exceeds bounds")); } if kind == "roundedRect" { RenderCommand::RoundedRect { x, y, width, height, radius, color, } } else { RenderCommand::StrokeRoundedRect { x, y, width, height, radius, color, stroke_width: number(store, command, "strokeWidth")?, } } } "text" => { let size = number(store, command, "size")?; if size.fract() != 0.0 || !(1.0..=8.0).contains(&size) { return Err(invalid("paint text size must be an integer from 1 to 8")); } let wrap_bounds = match ( store.get_attr(command, "wrapWidth"), store.get_attr(command, "wrapHeight"), ) { (Err(VmError::AttributeNotFound(_)), Err(VmError::AttributeNotFound(_))) => None, (Ok(_), Ok(_)) => Some([ number(store, command, "wrapWidth")?, number(store, command, "wrapHeight")?, ]), (Err(error), _) | (_, Err(error)) => return Err(error), }; RenderCommand::Text { x, y, value: text(store, command, "text")?, size: size as u32, color, wrap_bounds, } } _ => return Err(invalid("unsupported paint command kind")), }; result .validate() .map_err(|_| invalid("invalid paint command geometry, text or color"))?; Ok(result) } #[derive(Debug, Default)] struct PaintOutput { commands: BTreeMap>, count: usize, } type Output = Arc>; #[derive(Debug)] struct Collect { desktop: ObjectRef, mount: u64, output: Output, } impl NativeCallable for Collect { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let [Value::Object(result)] = args else { return Err(invalid("paint must return a DisplayList")); }; if !cx.store().is_instance( *result, named_type(cx.store(), self.desktop, "DisplayList")?, )? { return Err(invalid("paint must return a DisplayList")); } let commands = object(cx.store().get_attr(*result, "commands")?)?; let expected = named_type(cx.store(), self.desktop, "PaintCommand")?; let commands = object_list::items(cx.store(), commands)? .iter() .map(|command| decode(cx.store(), *command, expected)) .collect::, _>>()?; let mut output = self .output .lock() .map_err(|_| invalid("paint output poisoned"))?; if output.count + commands.len() > 10_000 { return Err(invalid("paint command budget exceeded")); } output.count += commands.len(); output.commands.insert(self.mount, commands); Ok(NativeResult::Return(Value::Nil)) } } #[derive(Debug)] struct Prepare { desktop: ObjectRef, mounts: Vec<(u64, WeakObjectRef)>, output: Output, } impl NativeCallable for Prepare { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { let advance = cx.allocate(ContextNativeFunction::new( "next paint occurrence", Advance { desktop: self.desktop, mounts: self.mounts.clone().into(), next: Mutex::new(0), output: self.output.clone(), }, vec![self.desktop], ))?; let function = crate::vm_sequence::repeat_call(cx, advance, "source paint", self.desktop)?; Ok(NativeResult::Invoke { function, arguments: vec![], }) } } #[derive(Debug)] struct Advance { desktop: ObjectRef, mounts: Arc<[(u64, WeakObjectRef)]>, next: Mutex, output: Output, } impl NativeCallable for Advance { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { let occurrence = { let mut next = self .next .lock() .map_err(|_| invalid("paint cursor poisoned"))?; let occurrence = self.mounts.get(*next).copied(); *next += usize::from(occurrence.is_some()); occurrence }; let Some((id, weak)) = occurrence else { return Ok(NativeResult::Return(Value::Bool(false))); }; let mount = cx .upgrade(weak)? .ok_or(invalid("paint Mount has expired"))?; let data = cx.store().payload::(mount, "Mount")?.clone(); if !data.mounted { return Err(invalid("paint Mount is detached")); } let definition = cx .store() .type_of(data.block)? .ok_or(invalid("untyped paint target"))?; let function = cx .store() .lookup_method(definition, "paint")? .ok_or(invalid("missing paint method"))?; if crate::native_visual::is_intrinsic(cx.store(), function, "paint", self.desktop)? { return Ok(NativeResult::Return(Value::Bool(true))); } let bounds = cx.new_object()?; attach_desktop_type(cx, bounds, "Bounds", self.desktop)?; for (name, value) in [ ("x", data.geometry.x), ("y", data.geometry.y), ("width", data.geometry.width), ("height", data.geometry.height), ] { cx.set_attr(bounds, name, Value::Number((value as f64).into()))?; } let collector = cx.allocate(ContextNativeFunction::new( "collect source paint", Collect { desktop: self.desktop, mount: id, output: self.output.clone(), }, vec![self.desktop], ))?; let function = crate::vm_sequence::collect_and_continue( cx, collector, function, vec![ Value::Object(data.block), Value::Object(mount), Value::Object(bounds), ], "paint occurrence", self.desktop, )?; Ok(NativeResult::Invoke { function, arguments: vec![], }) } } pub struct Draft { pub frame: Frame, pub reads: Vec, pub slices: usize, } #[must_use = "pending paint must be resumed or cancelled on its owning VM"] pub enum Progress { Pending(Box), Complete(Box), } /// Only unpublished native commands and an owning prepare root cross a pause. pub struct Work { desktop: ObjectRef, prepare: ObjectRef, output: Output, root: Option>, frame: Frame, slices: usize, } impl Work { pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result { self.slices += 1; let progress = vm.resume_read_only_call(slice); self.accept(vm, progress) } pub fn cancel(self, vm: &mut Vm) -> Result<()> { let cancelled = vm.cancel_read_only_call(); let released = vm.release(self.prepare); cancelled?; released?; Ok(()) } fn accept( self, vm: &mut Vm, progress: Result, ) -> Result { match progress { Ok(endor_core::ExecutionProgress::Pending { .. }) => { Ok(Progress::Pending(Box::new(self))) } Ok(endor_core::ExecutionProgress::Complete { value, dependencies, .. }) => { vm.release(self.prepare)?; vm.release_value(value)?; finish(vm, self, dependencies) } Err(error) => { let diagnostic = crate::endor_host::runtime_error(vm, error); self.cancel(vm)?; Err(diagnostic) } } } } pub fn start( vm: &mut Vm, desktop: ObjectRef, mounts: Vec<(u64, WeakObjectRef)>, root: Option>, frame: Frame, slice: usize, ) -> Result { anyhow::ensure!(slice > 0, "paint slice must be positive"); anyhow::ensure!( !vm.has_suspended_call() && !vm.object_store().is_read_only(), "cannot start paint while another read-only execution is active" ); let output = Arc::new(Mutex::new(PaintOutput::default())); let prepare = vm.allocate(ContextNativeFunction::new( "prepare source paint", Prepare { desktop, mounts, output: output.clone(), }, vec![desktop], ))?; let work = Work { desktop, prepare, output, root, frame, slices: 1, }; let result = vm.start_read_only_call( prepare, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, slice, ); work.accept(vm, result) } fn finish(vm: &mut Vm, mut work: Work, reads: Vec) -> Result { let output = work .output .lock() .map_err(|_| anyhow::anyhow!("paint output poisoned"))?; if output.commands.is_empty() { return Ok(Progress::Complete(Box::new(Draft { frame: work.frame, reads: Vec::new(), slices: work.slices, }))); } let root = work.root.as_deref(); let desktop = work.desktop; let frame = &mut work.frame; let mut nodes = HashMap::new(); let mut pending: Vec<_> = root.into_iter().collect(); while let Some(node) = pending.pop() { nodes.insert(node.block_id.as_str(), node); pending.extend(node.children().iter().map(|node| node.as_ref())); } let mut commands = Vec::new(); let mut clips = Vec::new(); for mount in &frame.mounts { let before = commands.len(); if let Some(custom) = output.commands.get(&mount.id) { commands.extend(custom.iter().cloned()); } else { let node = nodes .get(mount.block_id.as_str()) .ok_or_else(|| anyhow::anyhow!("paint projection has no node"))?; if let Some(command) = layout::paint_native( node, mount.x, mount.y, Size { width: mount.width, height: mount.height, }, ) { commands.push(command); } } clips.extend(std::iter::repeat_n(mount.clip, commands.len() - before)); if commands.len() > 10_000 { bail!("paint command budget exceeded"); } } frame.commands = commands; frame.command_clips = clips; let registry = object(vm.get_attr(desktop, "types")?)?; let reads = reads .into_iter() .filter(|dependency| { let object = dependency.object(); object != desktop && object != registry && vm .object(object) .is_ok_and(|object| !object.payload().as_any().is::()) }) .collect(); Ok(Progress::Complete(Box::new(Draft { frame: work.frame, reads, slices: work.slices, }))) }