From 8dfb8c88fe75776a8d107b35e948adaf3658b549 Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Sat, 5 Jul 2025 22:40:11 -0400 Subject: [PATCH] improve panic message, slightly --- assets/test_prelude.ld | 38 ++++++++-- may_2025_thoughts.md | 3 +- pkg/rudus.js | 2 +- pkg/rudus_bg.wasm | 4 +- src/errors.rs | 2 +- src/lib.rs | 8 +- src/vm.rs | 166 ++++++++++++++++++++++++----------------- src/world.rs | 60 ++++++++++----- 8 files changed, 182 insertions(+), 101 deletions(-) diff --git a/assets/test_prelude.ld b/assets/test_prelude.ld index 83d26b5..90a0291 100644 --- a/assets/test_prelude.ld +++ b/assets/test_prelude.ld @@ -1035,7 +1035,7 @@ fn spawn! { } fn yield! { - "Forces a process to yield." + "Forces a process to yield, allowing other processes to execute." () -> base :process (:yield) } @@ -1045,10 +1045,13 @@ fn alive? { } fn link! { - "Creates a link between two processes. There are two types of links: `:report`, which sends a message to pid1 when pid2 dies; and `:panic`, which causes a panic in one when the other dies. The default is `:report`." - (pid1 as :keyword, pid2 as :keyword) -> link! (pid1, pid2, :report) - (pid1 as :keyword, pid2 as :keyword, :report) -> base :process (:link_report, pid1, pid2) - (pid1 as :keyword, pid2 as :keyword, :panic) -> base :process (:link_panic, pid1, pid2) + "Links this process to another process. When either one dies--panics or returns--both are shut down." + (pid as :keyword) -> base :process (:link, pid) +} + +fn monitor! { + "Subscribes this process to another process's exit signals. There are two possibilities: a panic or a return. Exit signals are in the form of `(:exit, pid, (:ok, value)/(:err, msg))`." + (pid as :keyword) -> base :process (:monitor, pid) } fn flush! { @@ -1061,6 +1064,25 @@ fn sleep! { (ms as :number) -> base :process (:sleep, ms) } +fn await! { + "Parks the current process until it receives an exit signal from the passed process. Returns the result of a successful execution or panics if the awaited process panics." + (pid as :keyword) -> { + monitor! (pid) + receive { + (:exit, _, (:ok, result)) -> result + (:exit, _, (:err, msg)) -> { + panic! "Monitored process {pid} panicked with {msg}" } + } + } +} + +fn heed! { + "Parks the current process until it receives a reply, and returns whatever is replied. Causes a panic if it gets anything other than a `(:reply, result)` tuple." + () -> receive { + (:reply, result) -> result + } +} + & TODO: make this more robust, to handle multiple pending requests w/o data races fn request_fetch! { (pid as :keyword, url as :string) -> { @@ -1427,7 +1449,11 @@ fn llist { flush! & wip actor functions - & link! + link! + monitor! + await! + heed! + spawn_turtle! & shared memory w/ rust diff --git a/may_2025_thoughts.md b/may_2025_thoughts.md index 425fa1e..790ab38 100644 --- a/may_2025_thoughts.md +++ b/may_2025_thoughts.md @@ -1249,5 +1249,6 @@ And now, we build capacity for building projects. * [ ] allow for multiple turtles - [x] rework p5 function in `ludus.js` to properly parse commands targeted at different turtles - - [ ] write ludus code to spawn & manipulate turtle actors + - [x] write ludus code to spawn & manipulate turtle actors + - [ ] write linking logic so we can `await` turtles * [ ] do some sample multiturtle sketches diff --git a/pkg/rudus.js b/pkg/rudus.js index a563630..fe7b387 100644 --- a/pkg/rudus.js +++ b/pkg/rudus.js @@ -403,7 +403,7 @@ function __wbg_get_imports() { _assertBoolean(ret); return ret; }; - imports.wbg.__wbindgen_closure_wrapper8094 = function() { return logError(function (arg0, arg1, arg2) { + imports.wbg.__wbindgen_closure_wrapper8073 = function() { return logError(function (arg0, arg1, arg2) { const ret = makeMutClosure(arg0, arg1, 354, __wbg_adapter_20); return ret; }, arguments) }; diff --git a/pkg/rudus_bg.wasm b/pkg/rudus_bg.wasm index 6be3647..175aa8a 100644 --- a/pkg/rudus_bg.wasm +++ b/pkg/rudus_bg.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f87f5e2787813f9f269059eb624302a75a7b9a3d437771134256d34397309ee -size 16764917 +oid sha256:75ece955d2ce58175d7b1174f71a522c817398436678ee11a7019478e182f44c +size 16762988 diff --git a/src/errors.rs b/src/errors.rs index 496fab4..cb0f72f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -97,7 +97,7 @@ pub fn panic(panic: Panic) -> String { // console_log!("Ludus panicked!: {panic}"); // panic.call_stack.last().unwrap().chunk().dissasemble(); // console_log!("{:?}", panic.call_stack.last().unwrap().chunk().spans); - let mut msgs = vec!["Ludus panicked!".to_string()]; + let mut msgs = vec![]; let msg = match panic.msg { PanicMsg::Generic(ref s) => s, _ => &"no match".to_string(), diff --git a/src/lib.rs b/src/lib.rs index c854fe8..dcfdd8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,14 +163,14 @@ pub async fn ludus(src: String) { compiler.compile(); if DEBUG_SCRIPT_COMPILE { - println!("=== source code ==="); - println!("{src}"); + console_log!("=== source code ==="); + console_log!("{src}"); compiler.disassemble(); - println!("\n\n") + console_log!("\n\n") } if DEBUG_SCRIPT_RUN { - println!("=== vm run ==="); + console_log!("=== vm run ==="); } let vm_chunk = compiler.chunk; diff --git a/src/vm.rs b/src/vm.rs index 8fd3311..302289d 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -178,9 +178,10 @@ impl Creature { .map(|val| val.show()) .collect::>() .join("/"); - println!( + console_log!( "{:04}: [{inner}] ({register}) {} {{{mbx}}}", - self.last_code, self.pid + self.last_code, + self.pid ); } @@ -197,14 +198,6 @@ impl Creature { // add it to our cloned stack let mut call_stack = self.call_stack.clone(); call_stack.push(frame); - // console_log!( - // "{}", - // call_stack - // .iter() - // .map(|s| s.to_string()) - // .collect::>() - // .join("\n") - // ); //make a panic let panic = Panic { msg, @@ -214,9 +207,10 @@ impl Creature { // and gtfo self.result = Some(Err(panic)); self.r#yield = true; + self.broadcast_panic(); } - fn panic_with(&mut self, msg: String) { + pub fn panic_with(&mut self, msg: String) { self.panic(PanicMsg::Generic(msg)); } @@ -254,52 +248,84 @@ impl Creature { if self.pid == pid { self.mbx.push_back(msg.clone()); } else { - self.zoo.as_ref().borrow_mut().send_msg(pid, msg); + self.zoo.borrow_mut().send_msg(pid, msg); } self.push(Value::Keyword("ok")); } - // TODO: fix these based on what I decide about `link` & `monitor` - fn link_report(&mut self, parent: Value, child: Value) { - let (Value::Keyword(parent), Value::Keyword(child)) = (parent, child) else { - unreachable!("expected keyword pids in link_report"); + fn broadcast_panic(&mut self) { + console_log!("broadcasting panic in {}", self.pid); + let Err(panic) = self.result.clone().unwrap() else { + unreachable!("expected panic in broadcast panic"); }; - let child = if child == self.pid { - self - } else { - &mut self.zoo.borrow_mut().catch(child) - }; - child.parents.push(parent); + let msg = Rc::new(crate::errors::panic(panic)); + for pid in self.parents.clone() { + self.send_msg( + Value::Keyword(pid), + Value::tuple(vec![ + Value::Keyword("exit"), + Value::Keyword(self.pid), + Value::tuple(vec![Value::Keyword("err"), Value::String(msg.clone())]), + ]), + ) + } + for pid in self.siblings.clone() { + self.zoo.borrow_mut().kill_linked(pid); + } } - fn link_panic(&mut self, left_pid: Value, right_pid: Value) { - let (Value::Keyword(left_id), Value::Keyword(right_id)) = (left_pid, right_pid) else { - unreachable!("expected keyword pids in link_panic"); + fn broadcast_return(&mut self) { + // console_log!("broadcasting return from {}", self.pid); + //TODO: work around this clone? + for pid in self.parents.clone() { + let result_tuple = Value::tuple(vec![ + Value::Keyword("ok"), + self.result.clone().unwrap().unwrap(), + ]); + let exit_signal = Value::tuple(vec![ + Value::Keyword("exit"), + Value::Keyword(self.pid), + result_tuple, + ]); + // console_log!("sending exit signal {exit_signal}"); + self.send_msg(Value::Keyword(pid), exit_signal); + } + for pid in self.siblings.clone() { + self.zoo.borrow_mut().kill(pid); + } + } + + // TODO: fix these based on what I decide about `link` & `monitor` + fn monitor(&mut self, other: Value) { + // console_log!("monitoring {other}"); + let Value::Keyword(other) = other else { + unreachable!("expected keyword pid in monitor"); + }; + if other != self.pid { + let mut other = self.zoo.borrow_mut().catch(other); + other.parents.push(self.pid); + self.zoo.borrow_mut().release(other); + // console_log!("{} added to parents", self.pid); + } + self.push(Value::Keyword("ok")); + } + + fn link(&mut self, other: Value) { + let Value::Keyword(other) = other else { + unreachable!("expected keyword pid in link"); }; // if we're linking to ourselves, we don't need to do anything - if left_id == self.pid && right_id == self.pid { - return; - } - let mut zoo = self.zoo.borrow_mut(); - // fancy footwork w/ cases: we can't catch ourselves in the zoo - if left_id == self.pid { - self.siblings.push(right_id); - let mut right = zoo.catch(right_id); - right.siblings.push(self.pid); - } else if right_id == self.pid { - self.siblings.push(left_id); - let mut left = zoo.catch(left_id); - left.siblings.push(self.pid); - } else { - let mut left = zoo.catch(left_id); - let mut right = zoo.catch(right_id); - left.siblings.push(right_id); - right.siblings.push(left_id); + if other != self.pid { + self.siblings.push(other); + let mut other = self.zoo.borrow_mut().catch(other); + other.siblings.push(self.pid); + self.zoo.borrow_mut().release(other); } + self.push(Value::Keyword("ok")); } fn handle_msg(&mut self, args: Vec) { - println!("message received by {}: {}", self.pid, args[0]); + // console_log!("message received by {}: {}", self.pid, args[0]); let Value::Keyword(msg) = args.first().unwrap() else { return self.panic_with("malformed message to Process".to_string()); }; @@ -310,12 +336,12 @@ impl Creature { let f = args[1].clone(); let proc = Creature::spawn(f, self.zoo.clone(), self.debug); let id = self.zoo.as_ref().borrow_mut().put(proc); - console_log!("spawning new process {id}!"); + // console_log!("spawning new process {id}!"); self.push(Value::Keyword(id)); } "yield" => { self.r#yield = true; - println!("yielding from {}", self.pid); + console_log!("yielding from {}", self.pid); self.push(Value::Keyword("ok")); } "alive" => { @@ -329,12 +355,12 @@ impl Creature { self.push(Value::False) } } - "link_panic" => self.link_panic(args[1].clone(), args[2].clone()), - "link_report" => self.link_report(args[1].clone(), args[2].clone()), + "link" => self.link(args[1].clone()), + "monitor" => self.monitor(args[1].clone()), "flush" => { let msgs = self.mbx.iter().cloned().collect::>(); let msgs = Vector::from(msgs); - println!( + console_log!( "delivering messages: {}", msgs.iter() .map(|x| x.show()) @@ -342,11 +368,11 @@ impl Creature { .join(" | ") ); self.mbx = VecDeque::new(); - println!("flushing messages in {}", self.pid); + console_log!("flushing messages in {}", self.pid); self.push(Value::List(Box::new(msgs))); } "sleep" => { - println!("sleeping {} for {}", self.pid, args[1]); + console_log!("sleeping {} for {}", self.pid, args[1]); let Value::Number(ms) = args[1] else { unreachable!() }; @@ -359,28 +385,29 @@ impl Creature { } pub fn interpret(&mut self) { - console_log!("starting process {}", self.pid); - console_log!( - "mbx: {}", - self.mbx - .iter() - .map(|x| x.show()) - .collect::>() - .join(" | ") - ); + // console_log!("starting process {}", self.pid); + // console_log!( + // "mbx: {}", + // self.mbx + // .iter() + // .map(|x| x.show()) + // .collect::>() + // .join(" | ") + // ); loop { if self.at_end() { let result = self.stack.pop().unwrap(); - // println!("process {} has returned {result}", self.pid); + self.broadcast_return(); + // console_log!("process :{} has returned {result}", self.pid); self.result = Some(Ok(result)); return; } if self.r#yield { - console_log!("yielding from {}", self.pid); + // console_log!("yielding from {}", self.pid); return; } if self.reductions >= MAX_REDUCTIONS { - // println!( + // console_log!( // "process {} is yielding after {MAX_REDUCTIONS} reductions", // self.pid // ); @@ -730,7 +757,7 @@ impl Creature { let dict = match self.get_value_at(dict_idx) { Value::Dict(dict) => dict, value => { - println!( + console_log!( "internal Ludus error in function {}", self.frame.function.as_fn().name() ); @@ -1003,7 +1030,7 @@ impl Creature { let called = self.pop(); if self.debug { - println!( + console_log!( "=== tail call into {called}/{arity} from {} ===", self.frame.function.as_fn().name() ); @@ -1142,7 +1169,7 @@ impl Creature { let called = self.pop(); if self.debug { - println!("=== calling into {called}/{arity} ==="); + console_log!("=== calling into {called}/{arity} ==="); } match called { @@ -1263,7 +1290,7 @@ impl Creature { } Return => { if self.debug { - println!("== returning from {} ==", self.frame.function.show()) + console_log!("== returning from {} ==", self.frame.function.show()) } let mut value = Value::Nothing; swap(&mut self.register[0], &mut value); @@ -1274,14 +1301,15 @@ impl Creature { self.push(value); } None => { - println!("process {} has returned with {}", self.pid, value); + // console_log!("process {} has returned with {}", self.pid, value); self.result = Some(Ok(value)); + self.broadcast_return(); return; } } } Print => { - println!("{}", self.pop().show()); + console_log!("{}", self.pop().show()); self.push(Value::Keyword("ok")); } SetUpvalue => { diff --git a/src/world.rs b/src/world.rs index 2a55487..ae2b020 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,9 +1,11 @@ +use chumsky::primitive::NoneOf; + use crate::chunk::Chunk; use crate::value::{Value, Key}; use crate::vm::Creature; use crate::panic::Panic; use crate::errors::panic; -use crate::js::{random, now}; +use crate::js::*; use crate::io::{MsgOut, MsgIn, do_io}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; @@ -82,6 +84,7 @@ pub struct Zoo { sleeping: HashMap<&'static str, f64>, active_idx: usize, active_id: &'static str, + msgs: Vec, } impl Zoo { @@ -95,6 +98,7 @@ impl Zoo { sleeping: HashMap::new(), active_idx: 0, active_id: "", + msgs: vec![], } } @@ -131,6 +135,11 @@ impl Zoo { } } + pub fn kill_linked(&mut self, id: &'static str) { + self.msgs.push(format!("process :{id} terminated by linked process")); + self.kill_list.push(id); + } + pub fn kill(&mut self, id: &'static str) { self.kill_list.push(id); } @@ -161,7 +170,7 @@ impl Zoo { pub fn clean_up(&mut self) { while let Some(id) = self.kill_list.pop() { if let Some(idx) = self.ids.get(id) { - println!("buried process {id}"); + console_log!("process :{id} terminated"); self.procs[*idx] = Status::Empty; self.empty.push(*idx); self.ids.remove(id); @@ -172,14 +181,14 @@ impl Zoo { self.sleeping .retain(|_, wakeup_time| now() < *wakeup_time); - println!( - "currently sleeping processes: {}", - self.sleeping - .keys() - .map(|id| id.to_string()) - .collect::>() - .join(" | ") - ); + // console_log!( + // "currently sleeping processes: {}", + // self.sleeping + // .keys() + // .map(|id| id.to_string()) + // .collect::>() + // .join(" | ") + // ); } pub fn catch(&mut self, id: &'static str) -> Creature { @@ -362,9 +371,24 @@ impl World { if let Some(fetch) = self.make_fetch_happen() { outbox.push(fetch); } + if let Some(msgs) = self.flush_zoo() { + outbox.push(msgs); + } outbox } + fn flush_zoo(&mut self) -> Option { + let mut zoo_msgs = vec![]; + let mut zoo = self.zoo.borrow_mut(); + swap(&mut zoo_msgs, &mut zoo.msgs); + if zoo_msgs.is_empty() { + None + } else { + let inner = zoo_msgs.into_iter().map(Value::string).collect::>(); + Some(MsgOut::Console(Value::list(inner))) + } + } + fn make_fetch_happen(&self) -> Option { let out = self.buffers.fetch_out(); let working = RefCell::new(Value::Interned("")); @@ -406,11 +430,13 @@ impl World { // TODO: if we have a panic, actually add the panic message to the console let result = self.active_result().clone().unwrap(); self.result = Some(result.clone()); - let result_msg = match result { - Ok(value) => MsgOut::Complete(Value::string(value.show())), - Err(p) => MsgOut::Error(panic(p)) - }; - outbox.push(result_msg); + match result { + Ok(value) => outbox.push(MsgOut::Complete(Value::string(value.show()))), + Err(p) => { + outbox.push(MsgOut::Console(Value::list(imbl::vector!(Value::string("Ludus panicked!".to_string()))))); + outbox.push(MsgOut::Error(panic(p))) + } + } outbox } @@ -466,8 +492,8 @@ impl World { fn report_process_end(&mut self) { let result = self.active_result().clone().unwrap(); let msg = match result { - Ok(value) => format!("process {} returned with {}", self.active_id().unwrap(), value.show()), - Err(panic) => format!("process {} panicked with {}", self.active_id().unwrap(), crate::errors::panic(panic)) + Ok(value) => format!("Process {} returned with {}", self.active_id().unwrap(), value.show()), + Err(panic) => format!("Process {} panicked with {}", self.active_id().unwrap(), crate::errors::panic(panic)) }; self.send_ludus_msg(msg); }