From 555787e912e9a9b6c008e00a2d22b07e0ae7d79e Mon Sep 17 00:00:00 2001 From: Ryan Daum Date: Sat, 6 Jan 2024 11:19:15 -0500 Subject: [PATCH] VM refactor phase 1 Moves mutable state off the VM itself, and into a passed-in 'state' parameter. --- crates/db/src/tuplebox/tuples/slotbox.rs | 11 +- crates/db/src/tuplebox/tuples/slotted_page.rs | 2 +- crates/db/src/tuplebox/tx/transaction.rs | 2 +- crates/db/src/tuplebox/tx/working_set.rs | 2 +- crates/kernel/src/builtins/bf_objects.rs | 55 +- crates/kernel/src/builtins/bf_server.rs | 42 +- crates/kernel/src/builtins/mod.rs | 11 +- crates/kernel/src/tasks/mod.rs | 2 +- crates/kernel/src/tasks/moo_vm_host.rs | 86 +-- crates/kernel/src/tasks/scheduler.rs | 2 +- crates/kernel/src/tasks/vm_host.rs | 2 +- crates/kernel/src/vm/activation.rs | 4 + crates/kernel/src/vm/exec_state.rs | 165 ++++++ crates/kernel/src/vm/mod.rs | 140 +---- crates/kernel/src/vm/vm_call.rs | 178 +++--- crates/kernel/src/vm/vm_execute.rs | 505 +++++++++++------- crates/kernel/src/vm/vm_unwind.rs | 85 +-- crates/kernel/src/vm/vm_util.rs | 150 +----- 18 files changed, 765 insertions(+), 679 deletions(-) create mode 100644 crates/kernel/src/vm/exec_state.rs diff --git a/crates/db/src/tuplebox/tuples/slotbox.rs b/crates/db/src/tuplebox/tuples/slotbox.rs index f5c028dc..75274bef 100644 --- a/crates/db/src/tuplebox/tuples/slotbox.rs +++ b/crates/db/src/tuplebox/tuples/slotbox.rs @@ -197,8 +197,7 @@ impl SlotBox { allocator .available_page_space .iter() - .map(|(_, ps)| ps.pages()) - .flatten() + .flat_map(|(_, ps)| ps.pages()) .collect() } } @@ -384,7 +383,7 @@ impl Inner { } // Out of room, need to allocate a new page. - return self.alloc(relation_id, page_size); + self.alloc(relation_id, page_size) } fn finish_alloc( @@ -489,7 +488,7 @@ impl PageSpace { .entries .binary_search_by(|entry| decode(*entry).1.cmp(&available)); - return match found { + match found { // Exact match, highly unlikely, but possible. Ok(entry_num) => { // We only want the lower 64 bits, ala @@ -500,14 +499,14 @@ impl PageSpace { // Out of room, our caller will need to allocate a new page. Err(position) if position == self.entries.len() => { // If we didn't find a page with enough space, then we need to allocate a new page. - return None; + None } // Found a page we add to. Err(entry_num) => { let pid = self.entries[entry_num] as u64; Some((pid as PageId, entry_num)) } - }; + } } fn finish(&mut self, offset: usize, page_remaining_bytes: usize) { diff --git a/crates/db/src/tuplebox/tuples/slotted_page.rs b/crates/db/src/tuplebox/tuples/slotted_page.rs index 99242f7c..07133594 100644 --- a/crates/db/src/tuplebox/tuples/slotted_page.rs +++ b/crates/db/src/tuplebox/tuples/slotted_page.rs @@ -284,7 +284,7 @@ impl<'a> SlottedPage<'a> { // Add the index entry and expand the index region. let mut index_entry = self.get_index_entry_mut(self.header_mut().num_slots as SlotId); - index_entry.as_mut().alloc(content_position as usize, size); + index_entry.as_mut().alloc(content_position, size); // Update the header let header = self.header_mut(); diff --git a/crates/db/src/tuplebox/tx/transaction.rs b/crates/db/src/tuplebox/tx/transaction.rs index 648af79d..606097a3 100644 --- a/crates/db/src/tuplebox/tx/transaction.rs +++ b/crates/db/src/tuplebox/tx/transaction.rs @@ -89,7 +89,7 @@ impl Transaction { let mut working_set = self.working_set.write().await; let commit_set = self .db - .prepare_commit_set(commit_ts, &working_set.as_ref().unwrap()) + .prepare_commit_set(commit_ts, working_set.as_ref().unwrap()) .await?; match self.db.try_commit(commit_set).await { Ok(_) => { diff --git a/crates/db/src/tuplebox/tx/working_set.rs b/crates/db/src/tuplebox/tx/working_set.rs index a2344ada..3ba4ec09 100644 --- a/crates/db/src/tuplebox/tx/working_set.rs +++ b/crates/db/src/tuplebox/tx/working_set.rs @@ -145,7 +145,7 @@ impl WorkingSet { // By performing the seek, we'll materialize the tuples into our local working set, which // will in turn update the codomain index for those tuples. for tuple in tuples_for_codomain { - let _ = self.seek_by_domain(&db, relation_id, tuple.domain()).await; + let _ = self.seek_by_domain(db, relation_id, tuple.domain()).await; } let relation = Self::get_relation_mut(relation_id, &self.schema, &mut self.relations); diff --git a/crates/kernel/src/builtins/bf_objects.rs b/crates/kernel/src/builtins/bf_objects.rs index 1caff68d..b0045e31 100644 --- a/crates/kernel/src/builtins/bf_objects.rs +++ b/crates/kernel/src/builtins/bf_objects.rs @@ -135,7 +135,7 @@ async fn bf_create<'a>(bf_args: &mut BfCallState<'a>) -> Result { }; let tramp = bf_args - .vm + .exec_state .top() .bf_trampoline .unwrap_or(BF_CREATE_OBJECT_TRAMPOLINE_START_CALL_INITIALIZE); @@ -166,10 +166,10 @@ async fn bf_create<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "initialize".to_string(), location: new_obj, this: new_obj, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, trampoline: Some(BF_CREATE_OBJECT_TRAMPOLINE_DONE), command: None, @@ -178,7 +178,7 @@ async fn bf_create<'a>(bf_args: &mut BfCallState<'a>) -> Result { } BF_CREATE_OBJECT_TRAMPOLINE_DONE => { // The trampoline argument is the object we just created. - let Some(new_obj) = bf_args.vm.top().bf_trampoline_arg.clone() else { + let Some(new_obj) = bf_args.exec_state.top().bf_trampoline_arg.clone() else { panic!("Missing/invalid trampoline argument for bf_create"); }; Ok(Ret(new_obj)) @@ -211,7 +211,7 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { // object, so we'll do it manually here. 'outer: loop { - let tramp = bf_args.vm.top().bf_trampoline; + let tramp = bf_args.exec_state.top().bf_trampoline; match tramp { None => { // Starting out, we need to call "recycle" on the object, if it exists. @@ -255,10 +255,10 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "recycle".to_string(), location: *obj, this: *obj, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, trampoline: Some(BF_RECYCLE_TRAMPOLINE_CALL_EXITFUNC), trampoline_arg: Some(contents), @@ -267,9 +267,9 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { } Err(WorldStateError::VerbNotFound(_, _)) => { // Short-circuit fake-tramp state change. - bf_args.vm.top_mut().bf_trampoline = + bf_args.exec_state.top_mut().bf_trampoline = Some(BF_RECYCLE_TRAMPOLINE_CALL_EXITFUNC); - bf_args.vm.top_mut().bf_trampoline_arg = Some(contents); + bf_args.exec_state.top_mut().bf_trampoline_arg = Some(contents); // Fall through to the next case. } Err(e) => { @@ -282,15 +282,16 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { // Check the arguments, which must be a list of objects. IF it's empty, we can // move onto DONE_MOVE, if not, take the head of the list, and call :exitfunc on it // (if it exists), and then back to this state. - let contents = bf_args.vm.top().bf_trampoline_arg.clone().unwrap(); + let contents = bf_args.exec_state.top().bf_trampoline_arg.clone().unwrap(); let Variant::List(contents) = contents.variant() else { panic!("Invalid trampoline argument for bf_recycle"); }; 'inner: loop { debug!(?obj, contents = ?contents, "Calling :exitfunc for objects contents"); if contents.is_empty() { - bf_args.vm.top_mut().bf_trampoline_arg = None; - bf_args.vm.top_mut().bf_trampoline = Some(BF_RECYCLE_TRAMPOLINE_DONE_MOVE); + bf_args.exec_state.top_mut().bf_trampoline_arg = None; + bf_args.exec_state.top_mut().bf_trampoline = + Some(BF_RECYCLE_TRAMPOLINE_DONE_MOVE); continue 'outer; } let (head_obj, contents) = contents.pop_front(); @@ -306,7 +307,7 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { .await else { // If there's no :exitfunc, we can just move on to the next object. - bf_args.vm.top_mut().bf_trampoline_arg = Some(contents); + bf_args.exec_state.top_mut().bf_trampoline_arg = Some(contents); continue 'inner; }; // Call :exitfunc on the head object. @@ -317,10 +318,10 @@ async fn bf_recycle<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "exitfunc".to_string(), location: *head_obj, this: *head_obj, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![v_objid(*obj)], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, trampoline: Some(BF_RECYCLE_TRAMPOLINE_CALL_EXITFUNC), trampoline_arg: Some(contents), @@ -399,7 +400,7 @@ async fn bf_move<'a>(bf_args: &mut BfCallState<'a>) -> Result { // 3 => return v_none let mut tramp = bf_args - .vm + .exec_state .top() .bf_trampoline .unwrap_or(BF_MOVE_TRAMPOLINE_START_ACCEPT); @@ -428,10 +429,10 @@ async fn bf_move<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "accept".to_string(), location: *whereto, this: *whereto, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![v_objid(*what)], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, trampoline: Some(BF_MOVE_TRAMPOLINE_MOVE_CALL_EXITFUNC), trampoline_arg: None, @@ -459,7 +460,7 @@ async fn bf_move<'a>(bf_args: &mut BfCallState<'a>) -> Result { // Accept verb has been called, and returned. Check the result. Should be on stack, // unless short-circuited, in which case we assume *false* let result = if !shortcircuit { - bf_args.vm.top().peek_top().unwrap() + bf_args.exec_state.top().peek_top().unwrap() } else { v_int(0) }; @@ -504,10 +505,10 @@ async fn bf_move<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "exitfunc".to_string(), location: original_location, this: original_location, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![v_objid(*what)], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, command: None, trampoline: Some(BF_MOVE_TRAMPOLINE_CALL_ENTERFUNC), @@ -548,10 +549,10 @@ async fn bf_move<'a>(bf_args: &mut BfCallState<'a>) -> Result { verb_name: "enterfunc".to_string(), location: *whereto, this: *whereto, - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, args: vec![v_objid(*what)], argstr: "".to_string(), - caller: bf_args.vm.top().this, + caller: bf_args.exec_state.top().this, }, command: None, trampoline: Some(3), @@ -667,7 +668,7 @@ async fn bf_set_player_flag<'a>(bf_args: &mut BfCallState<'a>) -> Result(bf_args: &mut BfCallState<'a>) -> Result { .map_err(world_state_err)?; Ok(Ret(v_list( - players - .iter() - .map(v_objid) - .collect::>() - .as_slice(), + players.iter().map(v_objid).collect::>().as_slice(), ))) } bf_declare!(players, bf_players); diff --git a/crates/kernel/src/builtins/bf_server.rs b/crates/kernel/src/builtins/bf_server.rs index f0546178..ab2827d6 100644 --- a/crates/kernel/src/builtins/bf_server.rs +++ b/crates/kernel/src/builtins/bf_server.rs @@ -70,7 +70,7 @@ async fn bf_notify<'a>(bf_args: &mut BfCallState<'a>) -> Result { .check_obj_owner_perms(*player) .map_err(world_state_err)?; - let event = NarrativeEvent::notify_text(bf_args.vm.caller(), msg.to_string()); + let event = NarrativeEvent::notify_text(bf_args.exec_state.caller(), msg.to_string()); if let Err(send_error) = bf_args.session.send_event(*player, event).await { warn!( "Unable to send message to player: #{}: {}", @@ -141,7 +141,7 @@ async fn bf_set_task_perms<'a>(bf_args: &mut BfCallState<'a>) -> Result(bf_args: &mut BfCallState<'a>) -> Result { } // We have to exempt ourselves from the callers list. - let callers = bf_args.vm.callers()[1..].to_vec(); + let callers = bf_args.exec_state.callers()[1..].to_vec(); Ok(Ret(v_list( &callers .iter() @@ -184,7 +184,7 @@ async fn bf_task_id<'a>(bf_args: &mut BfCallState<'a>) -> Result { return Err(E_INVARG); } - Ok(Ret(v_int(bf_args.vm.top().task_id as i64))) + Ok(Ret(v_int(bf_args.exec_state.top().task_id as i64))) } bf_declare!(task_id, bf_task_id); @@ -392,12 +392,12 @@ async fn bf_read<'a>(bf_args: &mut BfCallState<'a>) -> Result { let Variant::Obj(requested_player) = bf_args.args[0].variant() else { return Err(E_INVARG); }; - let player = bf_args.vm.top().player; + let player = bf_args.exec_state.top().player; if *requested_player != player { // We log this because we'd like to know if cores are trying to do this. warn!( requested_player = ?requested_player, - caller = ?bf_args.vm.caller(), + caller = ?bf_args.exec_state.caller(), ?player, "read() called with non-current player"); return Err(E_INVARG); @@ -419,7 +419,7 @@ async fn bf_queued_tasks<'a>(bf_args: &mut BfCallState<'a>) -> Result(bf_args: &mut BfCallState<'a>) -> Result // Not sure this is *exactly* what MOO does, but it's close enough for now. let victim_task_id = *victim_task_id as TaskId; - if victim_task_id == bf_args.vm.top().task_id { + if victim_task_id == bf_args.exec_state.top().task_id { return Ok(VmInstr(ExecutionResult::Complete(v_none()))); } @@ -481,7 +481,7 @@ async fn bf_kill_task<'a>(bf_args: &mut BfCallState<'a>) -> Result bf_args .scheduler_sender .send(( - bf_args.vm.top().task_id, + bf_args.exec_state.top().task_id, SchedulerControlMsg::KillTask { victim_task_id, sender_permissions: bf_args.task_perms().await.map_err(world_state_err)?, @@ -517,7 +517,7 @@ async fn bf_resume<'a>(bf_args: &mut BfCallState<'a>) -> Result { let task_id = *resume_task_id as TaskId; // Resuming ourselves makes no sense, it's not suspended. E_INVARG. - if task_id == bf_args.vm.top().task_id { + if task_id == bf_args.exec_state.top().task_id { return Err(E_INVARG); } @@ -525,7 +525,7 @@ async fn bf_resume<'a>(bf_args: &mut BfCallState<'a>) -> Result { bf_args .scheduler_sender .send(( - bf_args.vm.top().task_id, + bf_args.exec_state.top().task_id, SchedulerControlMsg::ResumeTask { queued_task_id: task_id, sender_permissions: bf_args.task_perms().await.map_err(world_state_err)?, @@ -594,7 +594,7 @@ async fn bf_boot_player<'a>(bf_args: &mut BfCallState<'a>) -> Result(bf_args: &mut BfCallState<'a>) -> Result(bf_args: &mut BfCallState<'a>) -> Result { }; let tramp = bf_args - .vm + .exec_state .top() .bf_trampoline .unwrap_or(BF_SERVER_EVAL_TRAMPOLINE_START_INITIALIZE); @@ -775,13 +783,13 @@ async fn bf_eval<'a>(bf_args: &mut BfCallState<'a>) -> Result { // setup-for-eval result here. return Ok(VmInstr(ExecutionResult::PerformEval { permissions: bf_args.task_perms_who(), - player: bf_args.vm.top().player, + player: bf_args.exec_state.top().player, program, })); } BF_SERVER_EVAL_TRAMPOLINE_RESUME => { // Value must be on stack, and we then wrap that up in the {success, value} tuple. - let value = bf_args.vm.pop(); + let value = bf_args.exec_state.pop(); Ok(Ret(v_list(&[v_bool(true), value]))) } _ => { diff --git a/crates/kernel/src/builtins/mod.rs b/crates/kernel/src/builtins/mod.rs index 823cdb1d..d18edc76 100644 --- a/crates/kernel/src/builtins/mod.rs +++ b/crates/kernel/src/builtins/mod.rs @@ -37,7 +37,7 @@ use moor_values::var::Var; use crate::tasks::sessions::Session; use crate::tasks::task_messages::SchedulerControlMsg; use crate::tasks::TaskId; -use crate::vm::{ExecutionResult, VM}; +use crate::vm::{ExecutionResult, VMExecState}; /// The arguments and other state passed to a built-in function. pub struct BfCallState<'a> { @@ -45,8 +45,9 @@ pub struct BfCallState<'a> { pub(crate) name: String, /// Arguments passed to the function. pub(crate) args: Vec, - /// Reference back to the VM, to be able to retrieve stack frames and other state. - pub(crate) vm: &'a mut VM, + /// The current execution state of this task in this VM, including the stack + /// so that BFs can inspect and manipulate it. + pub(crate) exec_state: &'a mut VMExecState, /// Handle to the current database transaction. pub(crate) world_state: &'a mut dyn WorldState, /// For connection / message management. @@ -61,11 +62,11 @@ pub struct BfCallState<'a> { impl BfCallState<'_> { pub fn caller_perms(&self) -> Objid { - self.vm.caller_perms() + self.exec_state.caller_perms() } pub fn task_perms_who(&self) -> Objid { - self.vm.task_perms() + self.exec_state.task_perms() } pub async fn task_perms(&self) -> Result { let who = self.task_perms_who(); diff --git a/crates/kernel/src/tasks/mod.rs b/crates/kernel/src/tasks/mod.rs index bd54163d..81a551ac 100644 --- a/crates/kernel/src/tasks/mod.rs +++ b/crates/kernel/src/tasks/mod.rs @@ -56,7 +56,7 @@ pub mod vm_test_utils { use crate::tasks::sessions::Session; use crate::tasks::vm_host::{VMHost, VMHostResponse}; use crate::tasks::VerbCall; - use crate::vm::vm_execute::VmExecParams; + use crate::vm::VmExecParams; use moor_values::model::world_state::WorldState; use moor_values::var::Var; use moor_values::SYSTEM_OBJECT; diff --git a/crates/kernel/src/tasks/moo_vm_host.rs b/crates/kernel/src/tasks/moo_vm_host.rs index 3c459c76..42a52e8d 100644 --- a/crates/kernel/src/tasks/moo_vm_host.rs +++ b/crates/kernel/src/tasks/moo_vm_host.rs @@ -19,9 +19,9 @@ use crate::tasks::task_messages::SchedulerControlMsg; use crate::tasks::vm_host::VMHostResponse::{AbortLimit, ContinueOk, DispatchFork, Suspend}; use crate::tasks::vm_host::{VMHost, VMHostResponse}; use crate::tasks::{TaskId, VerbCall}; -use crate::vm::vm_execute::VmExecParams; -use crate::vm::vm_unwind::FinallyReason; +use crate::vm::VmExecParams; use crate::vm::{ExecutionResult, Fork, VerbExecutionRequest, VM}; +use crate::vm::{FinallyReason, VMExecState}; use async_trait::async_trait; use moor_compiler::labels::Name; use moor_compiler::opcode::Program; @@ -40,6 +40,7 @@ use tracing::{trace, warn}; /// A 'host' for running the MOO virtual machine inside a task. pub struct MooVmHost { vm: VM, + exec_state: VMExecState, /// The maximum stack depth for this task max_stack_depth: usize, /// The amount of ticks (opcode executions) allotted to this task @@ -61,10 +62,12 @@ impl MooVmHost { scheduler_control_sender: UnboundedSender<(TaskId, SchedulerControlMsg)>, ) -> Self { let vm = VM::new(); + let exec_state = VMExecState::new(); let (run_watch_send, run_watch_recv) = tokio::sync::watch::channel(false); // Created in an initial suspended state. Self { vm, + exec_state, max_stack_depth, max_ticks, max_time, @@ -122,8 +125,10 @@ impl VMHost for MooVmHost { self.start_execution(task_id, call_request).await } async fn start_fork(&mut self, task_id: TaskId, fork_request: Fork, suspended: bool) { - self.vm.tick_count = 0; - self.vm.exec_fork_vector(fork_request, task_id).await; + self.exec_state.tick_count = 0; + self.vm + .exec_fork_vector(&mut self.exec_state, fork_request, task_id) + .await; self.run_watch_send.send(!suspended).unwrap(); } /// Start execution of a verb request. @@ -132,18 +137,18 @@ impl VMHost for MooVmHost { task_id: TaskId, verb_execution_request: VerbExecutionRequest, ) { - self.vm.start_time = Some(SystemTime::now()); - self.vm.tick_count = 0; + self.exec_state.start_time = Some(SystemTime::now()); + self.exec_state.tick_count = 0; self.vm - .exec_call_request(task_id, verb_execution_request) + .exec_call_request(&mut self.exec_state, task_id, verb_execution_request) .await; self.run_watch_send.send(true).unwrap(); } async fn start_eval(&mut self, task_id: TaskId, player: Objid, program: Program) { - self.vm.start_time = Some(SystemTime::now()); - self.vm.tick_count = 0; + self.exec_state.start_time = Some(SystemTime::now()); + self.exec_state.tick_count = 0; self.vm - .exec_eval_request(task_id, player, player, program) + .exec_eval_request(&mut self.exec_state, task_id, player, player, program) .await; self.run_watch_send.send(true).unwrap(); } @@ -158,7 +163,7 @@ impl VMHost for MooVmHost { .unwrap(); // Check ticks and seconds, and abort the task if we've exceeded the limits. - let time_left = match self.vm.start_time { + let time_left = match self.exec_state.start_time { Some(start_time) => { let elapsed = start_time.elapsed().expect("Could not get elapsed time"); if elapsed > self.max_time { @@ -168,23 +173,26 @@ impl VMHost for MooVmHost { } None => None, }; - if self.vm.tick_count >= self.max_ticks { - return AbortLimit(AbortLimitReason::Ticks(self.vm.tick_count)); + if self.exec_state.tick_count >= self.max_ticks { + return AbortLimit(AbortLimitReason::Ticks(self.exec_state.tick_count)); } - let exec_params = VmExecParams { + let mut exec_params = VmExecParams { world_state, session: self.sessions.clone(), scheduler_sender: self.scheduler_control_sender.clone(), max_stack_depth: self.max_stack_depth, - ticks_left: self.max_ticks - self.vm.tick_count, + ticks_left: self.max_ticks - self.exec_state.tick_count, time_left, }; - let pre_exec_tick_count = self.vm.tick_count; + let pre_exec_tick_count = self.exec_state.tick_count; // Actually invoke the VM, asking it to loop until it's ready to yield back to us. - let mut result = self.vm.exec(exec_params, self.max_ticks).await; + let mut result = self + .vm + .exec(&mut exec_params, &mut self.exec_state, self.max_ticks) + .await; - let post_exec_tick_count = self.vm.tick_count; + let post_exec_tick_count = self.exec_state.tick_count; trace!( task_id, executed_ticks = post_exec_tick_count - pre_exec_tick_count, @@ -204,8 +212,8 @@ impl VMHost for MooVmHost { } => { trace!(task_id, call = ?call, "Task continue, call into verb"); - self.vm.top_mut().bf_trampoline_arg = trampoline_arg; - self.vm.top_mut().bf_trampoline = trampoline; + self.exec_state.top_mut().bf_trampoline_arg = trampoline_arg; + self.exec_state.top_mut().bf_trampoline = trampoline; let program = Self::decode_program( resolved_verb.verbdef().binary_type(), @@ -220,7 +228,9 @@ impl VMHost for MooVmHost { program, }; - self.vm.exec_call_request(task_id, call_request).await; + self.vm + .exec_call_request(&mut self.exec_state, task_id, call_request) + .await; return ContinueOk; } ExecutionResult::PerformEval { @@ -229,7 +239,7 @@ impl VMHost for MooVmHost { program, } => { self.vm - .exec_eval_request(0, permissions, player, program) + .exec_eval_request(&mut self.exec_state, 0, permissions, player, program) .await; return ContinueOk; } @@ -242,7 +252,7 @@ impl VMHost for MooVmHost { session: self.sessions.clone(), max_stack_depth: self.max_stack_depth, scheduler_sender: self.scheduler_control_sender.clone(), - ticks_left: self.max_ticks - self.vm.tick_count, + ticks_left: self.max_ticks - self.exec_state.tick_count, time_left, }; // Ask the VM to execute the builtin function. @@ -250,7 +260,12 @@ impl VMHost for MooVmHost { // After this we will loop around and check the result. result = self .vm - .call_builtin_function(bf_offset, &args, &mut exec_params) + .call_builtin_function( + &mut self.exec_state, + bf_offset, + &args, + &mut exec_params, + ) .await; continue; } @@ -295,9 +310,9 @@ impl VMHost for MooVmHost { /// Resume what you were doing after suspension. async fn resume_execution(&mut self, value: Var) { // coming back from suspend, we need a return value to feed back to `bf_suspend` - self.vm.top_mut().push(value); - self.vm.start_time = Some(SystemTime::now()); - self.vm.tick_count = 0; + self.exec_state.top_mut().push(value); + self.exec_state.start_time = Some(SystemTime::now()); + self.exec_state.tick_count = 0; self.run_watch_send.send(true).unwrap(); } fn is_running(&self) -> bool { @@ -313,28 +328,31 @@ impl VMHost for MooVmHost { } } fn set_variable(&mut self, task_id_var: Name, value: Var) { - self.vm + self.exec_state .top_mut() .set_var_offset(task_id_var, value) .expect("Could not set forked task id"); } fn permissions(&self) -> Objid { - self.vm.top().permissions + self.exec_state.top().permissions } fn verb_name(&self) -> String { - self.vm.top().verb_name.clone() + self.exec_state.top().verb_name.clone() } fn verb_definer(&self) -> Objid { - self.vm.top().verb_definer() + self.exec_state.top().verb_definer() } fn this(&self) -> Objid { - self.vm.top().this + self.exec_state.top().this } fn line_number(&self) -> usize { - self.vm.top().find_line_no(self.vm.top().pc).unwrap_or(0) + self.exec_state + .top() + .find_line_no(self.exec_state.top().pc) + .unwrap_or(0) } fn args(&self) -> Vec { - self.vm.top().args.clone() + self.exec_state.top().args.clone() } } diff --git a/crates/kernel/src/tasks/scheduler.rs b/crates/kernel/src/tasks/scheduler.rs index 272be831..dc5fa596 100644 --- a/crates/kernel/src/tasks/scheduler.rs +++ b/crates/kernel/src/tasks/scheduler.rs @@ -42,8 +42,8 @@ use crate::tasks::sessions::Session; use crate::tasks::task::Task; use crate::tasks::task_messages::{SchedulerControlMsg, TaskControlMsg, TaskStart}; use crate::tasks::{TaskDescription, TaskId}; -use crate::vm::vm_unwind::UncaughtException; use crate::vm::Fork; +use crate::vm::UncaughtException; use moor_compiler::codegen::compile; use moor_compiler::CompileError; diff --git a/crates/kernel/src/tasks/vm_host.rs b/crates/kernel/src/tasks/vm_host.rs index 31d88408..3bf601cf 100644 --- a/crates/kernel/src/tasks/vm_host.rs +++ b/crates/kernel/src/tasks/vm_host.rs @@ -17,7 +17,7 @@ use std::time::Duration; use crate::tasks::command_parse::ParsedCommand; use crate::tasks::scheduler::AbortLimitReason; use crate::tasks::{TaskId, VerbCall}; -use crate::vm::vm_unwind::UncaughtException; +use crate::vm::UncaughtException; use crate::vm::{Fork, VerbExecutionRequest}; use async_trait::async_trait; use moor_compiler::labels::Name; diff --git a/crates/kernel/src/vm/activation.rs b/crates/kernel/src/vm/activation.rs index 541c6677..aa05ed63 100644 --- a/crates/kernel/src/vm/activation.rs +++ b/crates/kernel/src/vm/activation.rs @@ -66,6 +66,10 @@ pub(crate) struct HandlerLabel { } /// Activation frame for the call stack. +// TODO: move everything MOO-specific into a sub-struct as an attribute: +// catch handlers +// opcode stream +// trampoline args? #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct Activation { /// The task ID of the task that owns this VM and this stack of activations. diff --git a/crates/kernel/src/vm/exec_state.rs b/crates/kernel/src/vm/exec_state.rs new file mode 100644 index 00000000..0c71b1fa --- /dev/null +++ b/crates/kernel/src/vm/exec_state.rs @@ -0,0 +1,165 @@ +use crate::vm::activation::{Activation, Caller}; +use moor_compiler::labels::{Label, Name}; +use moor_compiler::opcode::Op; +use moor_values::var::objid::Objid; +use moor_values::var::Var; +use moor_values::NOTHING; +use std::time::SystemTime; + +/// Represents the state of VM execution. +/// The actual "VM" remains stateless and could be potentially re-used for multiple tasks, +/// and swapped out at each level of the activation stack for different runtimes. +/// e.g. a MOO VM, a WASM VM, a JS VM, etc. but all having access to the same shared state. +pub struct VMExecState { + /// The stack of activation records / stack frames. + /// (For language runtimes that keep their own stack, this is simply the "entry" point + /// for the function invocation.) + pub(crate) stack: Vec, + /// The number of ticks that have been executed so far. + pub(crate) tick_count: usize, + /// The time at which the task was started. + pub(crate) start_time: Option, +} + +impl Default for VMExecState { + fn default() -> Self { + Self::new() + } +} + +impl VMExecState { + pub fn new() -> Self { + Self { + stack: vec![], + tick_count: 0, + start_time: None, + } + } + + /// Return the callers stack, in the format expected by the `callers` built-in function. + pub(crate) fn callers(&self) -> Vec { + let mut callers_iter = self.stack.iter().rev(); + callers_iter.next(); // skip the top activation, that's our current frame + + let mut callers = vec![]; + for activation in callers_iter { + let verb_name = activation.verb_name.clone(); + let definer = activation.verb_definer(); + let player = activation.player; + let line_number = 0; // TODO: fix after decompilation support + let this = activation.this; + let perms = activation.permissions; + let programmer = if activation.bf_index.is_some() { + NOTHING + } else { + perms + }; + callers.push(Caller { + verb_name, + definer, + player, + line_number, + this, + programmer, + }); + } + callers + } + + pub(crate) fn top_mut(&mut self) -> &mut Activation { + self.stack.last_mut().expect("activation stack underflow") + } + + pub(crate) fn top(&self) -> &Activation { + self.stack.last().expect("activation stack underflow") + } + + /// Return the object that called the current activation. + pub(crate) fn caller(&self) -> Objid { + let stack_iter = self.stack.iter().rev(); + for activation in stack_iter { + if activation.bf_index.is_some() { + continue; + } + return activation.this; + } + NOTHING + } + + /// Return the activation record of the caller of the current activation. + pub(crate) fn parent_activation_mut(&mut self) -> &mut Activation { + let len = self.stack.len(); + self.stack + .get_mut(len - 2) + .expect("activation stack underflow") + } + + /// Return the permissions of the caller of the current activation. + pub(crate) fn caller_perms(&self) -> Objid { + // Filter out builtins. + let mut stack_iter = self.stack.iter().rev().filter(|a| a.bf_index.is_none()); + // caller is the frame just before us. + stack_iter.next(); + stack_iter.next().map(|a| a.permissions).unwrap_or(NOTHING) + } + + /// Return the permissions of the current task, which is the "starting" + /// permissions of the current task, but note that this can be modified by + /// the `set_task_perms` built-in function. + pub(crate) fn task_perms(&self) -> Objid { + let stack_top = self.stack.iter().rev().find(|a| a.bf_index.is_none()); + stack_top.map(|a| a.permissions).unwrap_or(NOTHING) + } + + /// Update the permissions of the current task, as called by the `set_task_perms` + /// built-in. + pub(crate) fn set_task_perms(&mut self, perms: Objid) { + self.top_mut().permissions = perms; + } + + /// Pop a value off the value stack. + pub(crate) fn pop(&mut self) -> Var { + self.top_mut().pop().unwrap_or_else(|| { + panic!( + "stack underflow, activation depth: {} PC: {}", + self.stack.len(), + self.top().pc + ) + }) + } + + /// Push a value onto the value stack + pub(crate) fn push(&mut self, v: &Var) { + self.top_mut().push(v.clone()) + } + + /// Non-destructively peek in the value stack at the given offset. + pub(crate) fn peek(&self, amt: usize) -> Vec { + self.top().peek(amt) + } + + /// Return the top of the value stack. + pub(crate) fn peek_top(&self) -> Var { + self.top().peek_top().expect("stack underflow") + } + + /// Return the next opcode in the program stream. + pub(crate) fn next_op(&mut self) -> Option { + self.top_mut().next_op() + } + + /// Jump to the given label. + pub(crate) fn jump(&mut self, label: Label) { + self.top_mut().jump(label) + } + + /// Return the value of a local variable. + pub(crate) fn get_env(&self, id: Name) -> Option<&Var> { + self.top().environment.get(id.0 as usize) + } + + /// Set the value of a local variable. + pub(crate) fn set_env(&mut self, id: Name, v: &Var) { + self.top_mut().environment.insert(id.0 as usize, v.clone()); + } +} diff --git a/crates/kernel/src/vm/mod.rs b/crates/kernel/src/vm/mod.rs index 0ff07e89..6d7cf828 100644 --- a/crates/kernel/src/vm/mod.rs +++ b/crates/kernel/src/vm/mod.rs @@ -12,139 +12,37 @@ // this program. If not, see . // -/// A LambdaMOO 1.8.x compatibl(ish) virtual machine. -/// Executes opcodes which are essentially 1:1 with LambdaMOO's. -/// Aims to be semantically identical, so as to be able to run existing LambdaMOO compatible cores -/// without blocking issues. +//! A LambdaMOO 1.8.x compatibl(ish) virtual machine. +//! Executes opcodes which are essentially 1:1 with LambdaMOO's. +//! Aims to be semantically identical, so as to be able to run existing LambdaMOO compatible cores +//! without blocking issues. + use std::sync::Arc; -use std::time::{Duration, SystemTime}; -use moor_values::model::verb_info::VerbInfo; -use moor_values::var::objid::Objid; -use moor_values::var::Var; +use moor_compiler::builtins::BUILTIN_DESCRIPTORS; use crate::builtins::bf_server::BfNoop; use crate::builtins::BuiltinFunction; -use crate::tasks::command_parse::ParsedCommand; -use crate::tasks::VerbCall; -use crate::vm::activation::Activation; -use crate::vm::vm_unwind::FinallyReason; -use moor_compiler::builtins::BUILTIN_DESCRIPTORS; -use moor_compiler::labels::{Name, Offset}; -use moor_compiler::opcode::Program; -pub mod vm_call; -pub mod vm_execute; +pub(crate) mod activation; +pub(crate) mod exec_state; +pub(crate) mod vm_call; +pub(crate) mod vm_execute; pub(crate) mod vm_unwind; pub(crate) mod vm_util; -mod activation; +// Exports to the rest of the kernel +pub use exec_state::VMExecState; +pub use vm_call::VerbExecutionRequest; +pub use vm_execute::{ExecutionResult, Fork, VmExecParams}; +pub use vm_unwind::{FinallyReason, UncaughtException}; #[cfg(test)] mod vm_test; pub struct VM { - /// The stack of activation records / stack frames. - pub(crate) stack: Vec, /// The set of built-in functions, indexed by their Name offset in the variable stack. pub(crate) builtins: Vec>, - /// The number of ticks that have been executed so far. - pub(crate) tick_count: usize, - /// The time at which the VM was started. - pub(crate) start_time: Option, -} - -/// The set of parameters for a VM-requested fork. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Fork { - /// The player. This is in the activation as well, but it's nicer to have it up here and - /// explicit - pub(crate) player: Objid, - /// The permissions context for the forked task. - pub(crate) progr: Objid, - /// The task ID of the task that forked us - pub(crate) parent_task_id: usize, - /// The time to delay before starting the forked task, if any. - pub(crate) delay: Option, - /// A copy of the activation record from the task that forked us. - pub(crate) activation: Activation, - /// The unique fork vector offset into the fork vector for the executing binary held in the - /// activation record. This is copied into the main vector and execution proceeds from there, - /// instead. - pub(crate) fork_vector_offset: Offset, - /// The (optional) variable label where the task ID of the new task should be stored, in both - /// the parent activation and the new task's activation. - pub task_id: Option, -} - -/// The set of parameters for a scheduler-requested *resolved* verb method dispatch. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct VerbExecutionRequest { - /// The applicable permissions. - pub permissions: Objid, - /// The resolved verb. - pub resolved_verb: VerbInfo, - /// The call parameters that were used to resolve the verb. - pub call: VerbCall, - /// The parsed user command that led to this verb dispatch, if any. - pub command: Option, - /// The decoded MOO Binary that contains the verb to be executed. - pub program: Program, -} - -#[derive(Eq, PartialEq, Debug, Clone)] -pub enum ExecutionResult { - /// Execution of this call stack is complete. - Complete(Var), - /// All is well. The task should let the VM continue executing. - More, - /// An exception was raised during execution. - Exception(FinallyReason), - /// Request dispatch to another verb - ContinueVerb { - /// The applicable permissions context. - permissions: Objid, - /// The requested verb. - resolved_verb: VerbInfo, - /// The call parameters that were used to resolve the verb. - call: VerbCall, - /// The parsed user command that led to this verb dispatch, if any. - command: Option, - /// What to set the 'trampoline' to (if anything) when the verb returns. - /// If this is set, the builtin function that issued this ContinueVerb will be re-called - /// and the bf_trampoline argument on its activation record will be set to this value. - /// This is usually used to drive a state machine through a series of actions on a builtin - /// as it calls out to verbs. - trampoline: Option, - /// Likewise, along with the trampoline # above, this can be set with an optional argument - /// that can be used to pass data back to the builtin function that issued this request. - trampoline_arg: Option, - }, - /// Request dispatch of a new task as a fork - DispatchFork(Fork), - /// Request dispatch of a builtin function with the given arguments. - ContinueBuiltin { - bf_func_num: usize, - arguments: Vec, - }, - /// Request that this task be suspended for a duration of time. - /// This leads to the task performing a commit, being suspended for a delay, and then being - /// resumed under a new transaction. - /// If the duration is None, then the task is suspended indefinitely, until it is killed or - /// resumed using `resume()` or `kill_task()`. - Suspend(Option), - /// Request input from the client. - NeedInput, - /// Request `eval` execution, which is a kind of special activation creation where we've already - /// been given the program to execute instead of having to look it up. - PerformEval { - /// The permissions context for the eval. - permissions: Objid, - /// The player who is performing the eval. - player: Objid, - /// The program to execute. - program: Program, - }, } impl Default for VM { @@ -154,19 +52,13 @@ impl Default for VM { } impl VM { - #[tracing::instrument()] pub fn new() -> Self { let mut builtins: Vec> = Vec::with_capacity(BUILTIN_DESCRIPTORS.len()); for _ in 0..BUILTIN_DESCRIPTORS.len() { builtins.push(Arc::new(BfNoop {})) } - let mut vm = Self { - stack: vec![], - builtins, - tick_count: 0, - start_time: None, - }; + let mut vm = Self { builtins }; vm.register_bf_server(); vm.register_bf_num(); diff --git a/crates/kernel/src/vm/vm_call.rs b/crates/kernel/src/vm/vm_call.rs index ae8a03ae..dbf36ecc 100644 --- a/crates/kernel/src/vm/vm_call.rs +++ b/crates/kernel/src/vm/vm_call.rs @@ -22,13 +22,15 @@ use moor_values::var::{v_int, Var}; use crate::builtins::bf_server::BF_SERVER_EVAL_TRAMPOLINE_RESUME; use crate::builtins::{BfCallState, BfRet}; +use crate::tasks::command_parse::ParsedCommand; use crate::tasks::{TaskId, VerbCall}; use crate::vm::activation::Activation; -use crate::vm::vm_execute::VmExecParams; use crate::vm::vm_unwind::FinallyReason; -use crate::vm::{ExecutionResult, Fork, VerbExecutionRequest, VM}; +use crate::vm::{ExecutionResult, Fork, VM}; +use crate::vm::{VMExecState, VmExecParams}; use moor_compiler::builtins::BUILTIN_DESCRIPTORS; use moor_compiler::opcode::Program; +use moor_values::model::verb_info::VerbInfo; pub(crate) fn args_literal(args: &[Var]) -> String { args.iter() @@ -37,6 +39,21 @@ pub(crate) fn args_literal(args: &[Var]) -> String { .join(", ") } +/// The set of parameters for a scheduler-requested *resolved* verb method dispatch. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct VerbExecutionRequest { + /// The applicable permissions. + pub permissions: Objid, + /// The resolved verb. + pub resolved_verb: VerbInfo, + /// The call parameters that were used to resolve the verb. + pub call: VerbCall, + /// The parsed user command that led to this verb dispatch, if any. + pub command: Option, + /// The decoded MOO Binary that contains the verb to be executed. + pub program: Program, +} + impl VM { /// Entry point for preparing a verb call for execution, invoked from the CallVerb opcode /// Seek the verb and prepare the call parameters. @@ -44,8 +61,9 @@ impl VM { /// The call params will be returned back to the task in the scheduler, which will then dispatch /// back through to `do_method_call` pub(crate) async fn prepare_call_verb( - &mut self, - state: &mut dyn WorldState, + &self, + vm_state: &mut VMExecState, + world_state: &mut dyn WorldState, this: Objid, verb_name: &str, args: &[Var], @@ -54,46 +72,39 @@ impl VM { verb_name: verb_name.to_string(), location: this, this, - player: self.top().player, + player: vm_state.top().player, args: args.to_vec(), // caller her is current-activation 'this', not activation caller() ... // unless we're a builtin, in which case we're #-1. argstr: "".to_string(), - caller: self.caller(), + caller: vm_state.caller(), }; - debug!( - line = self.top().find_line_no(self.top().pc).unwrap_or(0), - caller_perms = ?self.top().permissions, - caller = ?self.caller(), - this = ?this, - player = ?self.top().player, - "Verb call: {}:{}({})", - this, - verb_name, - args_literal(args), - ); - let self_valid = state + let self_valid = world_state .valid(this) .await .expect("Error checking object validity"); if !self_valid { - return self.push_error(E_INVIND); + return self.push_error(vm_state, E_INVIND); } // Find the callable verb ... - let verb_info = match state - .find_method_verb_on(self.top().permissions, this, verb_name) + let verb_info = match world_state + .find_method_verb_on(vm_state.top().permissions, this, verb_name) .await { Ok(vi) => vi, Err(WorldStateError::ObjectPermissionDenied) => { - return self.push_error(E_PERM); + return self.push_error(vm_state, E_PERM); } Err(WorldStateError::VerbPermissionDenied) => { - return self.push_error(E_PERM); + return self.push_error(vm_state, E_PERM); } Err(WorldStateError::VerbNotFound(_, _)) => { - return self.push_error_msg(E_VERBNF, format!("Verb \"{}\" not found", verb_name)); + return self.push_error_msg( + vm_state, + E_VERBNF, + format!("Verb \"{}\" not found", verb_name), + ); } Err(e) => { panic!("Unexpected error from find_method_verb_on: {:?}", e) @@ -107,7 +118,7 @@ impl VM { permissions, resolved_verb: verb_info, call, - command: self.top().command.clone(), + command: vm_state.top().command.clone(), trampoline: None, trampoline_arg: None, } @@ -117,35 +128,36 @@ impl VM { /// version. /// TODO this should be done up in task.rs instead. let's add a new ExecutionResult for it. pub(crate) async fn prepare_pass_verb( - &mut self, - state: &mut dyn WorldState, + &self, + vm_state: &mut VMExecState, + world_state: &mut dyn WorldState, args: &[Var], ) -> ExecutionResult { // get parent of verb definer object & current verb name. - let definer = self.top().verb_definer(); - let permissions = self.top().permissions; - let parent = state + let definer = vm_state.top().verb_definer(); + let permissions = vm_state.top().permissions; + let parent = world_state .parent_of(permissions, definer) .await .expect("unable to lookup parent"); - let verb = self.top().verb_name.to_string(); + let verb = vm_state.top().verb_name.to_string(); // call verb on parent, but with our current 'this' - trace!(task_id = self.top().task_id, verb, ?definer, ?parent); + trace!(task_id = vm_state.top().task_id, verb, ?definer, ?parent); - let Ok(vi) = state + let Ok(vi) = world_state .find_method_verb_on(permissions, parent, verb.as_str()) .await else { - return self.raise_error(E_VERBNF); + return self.raise_error(vm_state, E_VERBNF); }; - let caller = self.caller(); + let caller = vm_state.caller(); let call = VerbCall { verb_name: verb, location: parent, - this: self.top().this, - player: self.top().player, + this: vm_state.top().this, + player: vm_state.top().player, args: args.to_vec(), argstr: "".to_string(), caller, @@ -155,7 +167,7 @@ impl VM { permissions, resolved_verb: vi, call, - command: self.top().command.clone(), + command: vm_state.top().command.clone(), trampoline: None, trampoline_arg: None, } @@ -164,45 +176,45 @@ impl VM { /// Entry point from scheduler for actually beginning the dispatch of a method execution /// (non-command) in this VM. /// Actually creates the activation record and puts it on the stack. - pub async fn exec_call_request(&mut self, task_id: TaskId, call_request: VerbExecutionRequest) { - debug!( - caller = ?call_request.call.caller, - this = ?call_request.call.this, - player = ?call_request.call.player, - "Verb call: {}:{}({})", - call_request.call.this, - call_request.call.verb_name, - args_literal(call_request.call.args.as_slice()), - ); - + pub async fn exec_call_request( + &self, + vm_state: &mut VMExecState, + task_id: TaskId, + call_request: VerbExecutionRequest, + ) { let a = Activation::for_call(task_id, call_request); - - self.stack.push(a); + vm_state.stack.push(a); } pub async fn exec_eval_request( - &mut self, + &self, + vm_state: &mut VMExecState, task_id: TaskId, permissions: Objid, player: Objid, program: Program, ) { - if !self.stack.is_empty() { + if !vm_state.stack.is_empty() { // We need to set up a trampoline to return back into `bf_eval` - self.top_mut().bf_trampoline_arg = None; - self.top_mut().bf_trampoline = Some(BF_SERVER_EVAL_TRAMPOLINE_RESUME); + vm_state.top_mut().bf_trampoline_arg = None; + vm_state.top_mut().bf_trampoline = Some(BF_SERVER_EVAL_TRAMPOLINE_RESUME); } let a = Activation::for_eval(task_id, permissions, player, program); - self.stack.push(a); + vm_state.stack.push(a); } /// Prepare a new stack & call hierarchy for invocation of a forked task. /// Called (ultimately) from the scheduler as the result of a fork() call. /// We get an activation record which is a copy of where it was borked from, and a new Program /// which is the new task's code, derived from a fork vector in the original task. - pub(crate) async fn exec_fork_vector(&mut self, fork_request: Fork, task_id: usize) { + pub(crate) async fn exec_fork_vector( + &self, + vm_state: &mut VMExecState, + fork_request: Fork, + task_id: usize, + ) { // Set the activation up with the new task ID, and the new code. let mut a = fork_request.activation; a.task_id = task_id; @@ -215,18 +227,19 @@ impl VM { // TODO how to set the task_id in the parent activation, as we no longer have a reference // to it? - self.stack = vec![a]; + vm_state.stack = vec![a]; } /// Call into a builtin function. pub(crate) async fn call_builtin_function<'a>( - &mut self, + &self, + vm_state: &mut VMExecState, bf_func_num: usize, args: &[Var], exec_args: &mut VmExecParams<'a>, ) -> ExecutionResult { if bf_func_num >= self.builtins.len() { - return self.raise_error(E_VARNF); + return self.raise_error(vm_state, E_VARNF); } let bf = self.builtins[bf_func_num].clone(); @@ -234,24 +247,24 @@ impl VM { "Calling builtin: {}({}) caller_perms: {}", BUILTIN_DESCRIPTORS[bf_func_num].name, args_literal(args), - self.top().permissions + vm_state.top().permissions ); let args = args.to_vec(); // Push an activation frame for the builtin function. - let flags = self.top().verb_info.verbdef().flags(); - self.stack.push(Activation::for_bf_call( - self.top().task_id, + let flags = vm_state.top().verb_info.verbdef().flags(); + vm_state.stack.push(Activation::for_bf_call( + vm_state.top().task_id, bf_func_num, BUILTIN_DESCRIPTORS[bf_func_num].name.as_str(), args.clone(), // We copy the flags from the calling verb, that will determine error handling 'd' // behaviour below. flags, - self.top().player, + vm_state.top().player, )); let mut bf_args = BfCallState { - vm: self, + exec_state: vm_state, name: BUILTIN_DESCRIPTORS[bf_func_num].name.clone(), world_state: exec_args.world_state, session: exec_args.session.clone(), @@ -262,8 +275,10 @@ impl VM { }; let call_results = match bf.call(&mut bf_args).await { - Ok(BfRet::Ret(result)) => self.unwind_stack(FinallyReason::Return(result.clone())), - Err(e) => self.push_bf_error(e), + Ok(BfRet::Ret(result)) => { + self.unwind_stack(vm_state, FinallyReason::Return(result.clone())) + } + Err(e) => self.push_bf_error(vm_state, e), Ok(BfRet::VmInstr(vmi)) => vmi, }; @@ -273,28 +288,29 @@ impl VM { /// We're returning into a builtin function, which is all set up at the top of the stack. pub(crate) async fn reenter_builtin_function<'a>( - &mut self, - exec_args: VmExecParams<'a>, + &self, + vm_state: &mut VMExecState, + exec_args: &mut VmExecParams<'a>, ) -> ExecutionResult { trace!( - bf_index = self.top().bf_index, + bf_index = vm_state.top().bf_index, "Reentering builtin function" ); // Functions that did not set a trampoline are assumed to be complete, so we just unwind. // Note: If there was an error that required unwinding, we'll have already done that, so // we can assume a *value* here not, an error. - let Some(_) = self.top_mut().bf_trampoline else { - let return_value = self.top_mut().pop().unwrap(); + let Some(_) = vm_state.top_mut().bf_trampoline else { + let return_value = vm_state.top_mut().pop().unwrap(); - return self.unwind_stack(FinallyReason::Return(return_value)); + return self.unwind_stack(vm_state, FinallyReason::Return(return_value)); }; - let bf = self.builtins[self.top().bf_index.unwrap()].clone(); - let verb_name = self.top().verb_name.clone(); + let bf = self.builtins[vm_state.top().bf_index.unwrap()].clone(); + let verb_name = vm_state.top().verb_name.clone(); let sessions = exec_args.session.clone(); - let args = self.top().args.clone(); + let args = vm_state.top().args.clone(); let mut bf_args = BfCallState { - vm: self, + exec_state: vm_state, name: verb_name, world_state: exec_args.world_state, session: sessions, @@ -305,8 +321,10 @@ impl VM { }; match bf.call(&mut bf_args).await { - Ok(BfRet::Ret(result)) => self.unwind_stack(FinallyReason::Return(result.clone())), - Err(e) => self.push_bf_error(e), + Ok(BfRet::Ret(result)) => { + self.unwind_stack(vm_state, FinallyReason::Return(result.clone())) + } + Err(e) => self.push_bf_error(vm_state, e), Ok(BfRet::VmInstr(vmi)) => vmi, } } diff --git a/crates/kernel/src/vm/vm_execute.rs b/crates/kernel/src/vm/vm_execute.rs index 65a0d774..d52ce10a 100644 --- a/crates/kernel/src/vm/vm_execute.rs +++ b/crates/kernel/src/vm/vm_execute.rs @@ -14,54 +14,136 @@ use std::sync::Arc; use std::time::Duration; - use tokio::sync::mpsc::UnboundedSender; + +use moor_compiler::labels::{Name, Offset}; use tracing::trace; +use crate::tasks::command_parse::ParsedCommand; +use crate::tasks::sessions::Session; +use crate::tasks::task_messages::SchedulerControlMsg; +use crate::tasks::{TaskId, VerbCall}; +use moor_compiler::opcode::{Op, Program, ScatterLabel}; +use moor_values::model::verb_info::VerbInfo; use moor_values::model::world_state::WorldState; use moor_values::var::error::Error; use moor_values::var::error::Error::{E_ARGS, E_DIV, E_INVARG, E_MAXREC, E_RANGE, E_TYPE, E_VARNF}; +use moor_values::var::objid::Objid; use moor_values::var::variant::Variant; use moor_values::var::{v_bool, v_empty_list, v_int, v_list, v_none, v_obj, Var}; -use crate::tasks::sessions::Session; -use crate::tasks::task_messages::SchedulerControlMsg; -use crate::tasks::TaskId; -use crate::vm::activation::HandlerType; +use crate::vm::activation::{Activation, HandlerType}; use crate::vm::vm_unwind::{FinallyReason, UncaughtException}; -use crate::vm::{ExecutionResult, Fork, VM}; -use moor_compiler::opcode::{Op, ScatterLabel}; +use crate::vm::{VMExecState, VM}; + +/// The set of parameters for a VM-requested fork. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fork { + /// The player. This is in the activation as well, but it's nicer to have it up here and + /// explicit + pub(crate) player: Objid, + /// The permissions context for the forked task. + pub(crate) progr: Objid, + /// The task ID of the task that forked us + pub(crate) parent_task_id: usize, + /// The time to delay before starting the forked task, if any. + pub(crate) delay: Option, + /// A copy of the activation record from the task that forked us. + pub(crate) activation: Activation, + /// The unique fork vector offset into the fork vector for the executing binary held in the + /// activation record. This is copied into the main vector and execution proceeds from there, + /// instead. + pub(crate) fork_vector_offset: Offset, + /// The (optional) variable label where the task ID of the new task should be stored, in both + /// the parent activation and the new task's activation. + pub task_id: Option, +} + +/// Represents the set of parameters passed to the VM for execution. +pub struct VmExecParams<'a> { + pub world_state: &'a mut dyn WorldState, + pub session: Arc, + pub scheduler_sender: UnboundedSender<(TaskId, SchedulerControlMsg)>, + pub max_stack_depth: usize, + pub ticks_left: usize, + pub time_left: Option, +} +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ExecutionResult { + /// Execution of this call stack is complete. + Complete(Var), + /// All is well. The task should let the VM continue executing. + More, + /// An exception was raised during execution. + Exception(FinallyReason), + /// Request dispatch to another verb + ContinueVerb { + /// The applicable permissions context. + permissions: Objid, + /// The requested verb. + resolved_verb: VerbInfo, + /// The call parameters that were used to resolve the verb. + call: VerbCall, + /// The parsed user command that led to this verb dispatch, if any. + command: Option, + /// What to set the 'trampoline' to (if anything) when the verb returns. + /// If this is set, the builtin function that issued this ContinueVerb will be re-called + /// and the bf_trampoline argument on its activation record will be set to this value. + /// This is usually used to drive a state machine through a series of actions on a builtin + /// as it calls out to verbs. + trampoline: Option, + /// Likewise, along with the trampoline # above, this can be set with an optional argument + /// that can be used to pass data back to the builtin function that issued this request. + trampoline_arg: Option, + }, + /// Request dispatch of a new task as a fork + DispatchFork(Fork), + /// Request dispatch of a builtin function with the given arguments. + ContinueBuiltin { + bf_func_num: usize, + arguments: Vec, + }, + /// Request that this task be suspended for a duration of time. + /// This leads to the task performing a commit, being suspended for a delay, and then being + /// resumed under a new transaction. + /// If the duration is None, then the task is suspended indefinitely, until it is killed or + /// resumed using `resume()` or `kill_task()`. + Suspend(Option), + /// Request input from the client. + NeedInput, + /// Request `eval` execution, which is a kind of special activation creation where we've already + /// been given the program to execute instead of having to look it up. + PerformEval { + /// The permissions context for the eval. + permissions: Objid, + /// The player who is performing the eval. + player: Objid, + /// The program to execute. + program: Program, + }, +} macro_rules! binary_bool_op { - ( $self:ident, $op:tt ) => { - let rhs = $self.pop(); - let lhs = $self.pop(); + ( $state:ident, $op:tt ) => { + let rhs = $state.pop(); + let lhs = $state.pop(); let result = if lhs $op rhs { 1 } else { 0 }; - $self.push(&v_int(result)) + $state.push(&v_int(result)) }; } macro_rules! binary_var_op { - ( $self:ident, $op:tt ) => { - let rhs = $self.pop(); - let lhs = $self.pop(); + ( $vm:ident, $state:ident, $op:tt ) => { + let rhs = $state.pop(); + let lhs = $state.pop(); let result = lhs.$op(&rhs); match result { - Ok(result) => $self.push(&result), - Err(err_code) => return $self.push_error(err_code), + Ok(result) => $state.push(&result), + Err(err_code) => return $vm.push_error($state, err_code), } }; } -pub struct VmExecParams<'a> { - pub world_state: &'a mut dyn WorldState, - pub session: Arc, - pub scheduler_sender: UnboundedSender<(TaskId, SchedulerControlMsg)>, - pub max_stack_depth: usize, - pub ticks_left: usize, - pub time_left: Option, -} - pub(crate) fn one_to_zero_index(v: &Var) -> Result { let Variant::Int(index) = v.variant() else { return Err(E_TYPE); @@ -76,65 +158,66 @@ pub(crate) fn one_to_zero_index(v: &Var) -> Result { impl VM { /// Main VM opcode execution. The actual meat of the machine. pub async fn exec<'a>( - &mut self, - mut exec_params: VmExecParams<'a>, + &self, + exec_params: &mut VmExecParams<'a>, + state: &mut VMExecState, tick_slice: usize, ) -> ExecutionResult { // Before executing, check stack depth... - if self.stack.len() >= exec_params.max_stack_depth { + if state.stack.len() >= exec_params.max_stack_depth { // Absolutely raise-unwind an error here instead of just offering it as a potential // return value if this is a non-d verb. At least I think this the right thing to do? - return self.throw_error(E_MAXREC); + return self.throw_error(state, E_MAXREC); } // If the current activation frame is a builtin function, we need to jump back into it, // but increment the trampoline counter, as it means we're returning into it after // executing elsewhere. It will be up to the function to interpret the counter. // Functions that did not set a trampoline are assumed to be complete. - if !self.stack.is_empty() && self.top().bf_index.is_some() { - return self.reenter_builtin_function(exec_params).await; + if !state.stack.is_empty() && state.top().bf_index.is_some() { + return self.reenter_builtin_function(state, exec_params).await; } // Try to consume & execute as many opcodes as we can without returning back to the task // scheduler, for efficiency reasons... - while self.tick_count < tick_slice { + while state.tick_count < tick_slice { // Otherwise, start poppin' opcodes. // We panic here if we run out of opcodes, as that means there's a bug in either the // compiler or in opcode execution. - let op = self.next_op().expect( + let op = state.next_op().expect( "Unexpected program termination; opcode stream should end with RETURN or DONE", ); - self.tick_count += 1; + state.tick_count += 1; trace!( - pc = self.top().pc, + pc = state.top().pc, ?op, - this = ?self.top().this, - player = ?self.top().player, - stack = ?self.top().valstack, - tick_count = self.tick_count, + this = ?state.top().this, + player = ?state.top().player, + stack = ?state.top().valstack, + tick_count = state.tick_count, tick_slice, "exec" ); match op { Op::If(label) | Op::Eif(label) | Op::IfQues(label) | Op::While(label) => { - let cond = self.pop(); + let cond = state.pop(); if !cond.is_true() { - self.jump(label); + state.jump(label); } } Op::Jump { label } => { - self.jump(label); + state.jump(label); } Op::WhileId { id, end_label: label, } => { - self.set_env(id, &self.peek_top()); - let cond = self.pop(); + state.set_env(id, &state.peek_top()); + let cond = state.pop(); if !cond.is_true() { - self.jump(label); + state.jump(label); } } Op::ForList { @@ -145,32 +228,32 @@ impl VM { // TODO LambdaMOO had optimization here where it would only peek and update. // But I had some difficulty getting stack values right, so will do this simpler // for now and revisit later. - let (count, list) = (&self.pop(), &self.pop()); + let (count, list) = (&state.pop(), &state.pop()); let Variant::Int(count) = count.variant() else { // If the result of raising error was just to push the value -- that is, we // didn't 'throw' and unwind the stack -- we need to get out of the loop. // So we preemptively jump (here and below for List) and then raise the error. - self.jump(label); - return self.raise_error(E_TYPE); + state.jump(label); + return self.raise_error(state, E_TYPE); }; let count = *count as usize; let Variant::List(l) = list.variant() else { - self.jump(label); - return self.raise_error(E_TYPE); + state.jump(label); + return self.raise_error(state, E_TYPE); }; // If we've exhausted the list, pop the count and list and jump out. if count >= l.len() { - self.jump(label); + state.jump(label); continue; } // Track iteration count for range; set id to current list element for the count, // then increment the count, rewind the program counter to the top of the loop, and // continue. - self.set_env(id, &l[count]); - self.push(list); - self.push(&v_int((count + 1) as i64)); + state.set_env(id, &l[count]); + state.push(list); + state.push(&v_int((count + 1) as i64)); } Op::ForRange { end_label: label, @@ -180,7 +263,7 @@ impl VM { // TODO LambdaMOO had optimization here where it would only peek and update. // But I had some difficulty getting stack values right, so will do this simpler // for now and revisit later. - let (to, from) = (&self.pop(), &self.pop()); + let (to, from) = (&state.pop(), &state.pop()); // TODO: LambdaMOO has special handling for MAXINT/MAXOBJ // Given we're 64-bit this is highly unlikely to ever be a concern for us, but @@ -189,14 +272,14 @@ impl VM { let next_val = match (to.variant(), from.variant()) { (Variant::Int(to_i), Variant::Int(from_i)) => { if from_i > to_i { - self.jump(label); + state.jump(label); continue; } v_int(from_i + 1) } (Variant::Obj(to_o), Variant::Obj(from_o)) => { if from_o.0 > to_o.0 { - self.jump(label); + state.jump(label); continue; } v_obj(from_o.0 + 1) @@ -205,286 +288,287 @@ impl VM { // Make sure we've jumped out of the loop before raising the error, // because in verbs that aren't `d' we could end up continuing on in // the loop (with a messed up stack) otherwise. - self.jump(label); - return self.raise_error(E_TYPE); + state.jump(label); + return self.raise_error(state, E_TYPE); } }; - self.set_env(id, from); - self.push(&next_val); - self.push(to); + state.set_env(id, from); + state.push(&next_val); + state.push(to); } Op::Pop => { - self.pop(); + state.pop(); } Op::Val(val) => { - self.push(&val); + state.push(&val); } Op::Imm(slot) => { // TODO Peek ahead to see if the next operation is 'pop' and if so, just throw away. // MOO uses this to optimize verbdoc/comments, etc. - match self.top().lookahead() { + match state.top().lookahead() { Some(Op::Pop) => { // skip - self.top_mut().skip(); + state.top_mut().skip(); continue; } _ => { - let value = self.top().program.literals[slot.0 as usize].clone(); - self.push(&value); + let value = state.top().program.literals[slot.0 as usize].clone(); + state.push(&value); } } } - Op::MkEmptyList => self.push(&v_empty_list()), + Op::MkEmptyList => state.push(&v_empty_list()), Op::ListAddTail => { - let tail = self.pop(); - let list = self.pop(); + let tail = state.pop(); + let list = state.pop(); let Variant::List(list) = list.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; // TODO: quota check SVO_MAX_LIST_CONCAT -> E_QUOTA - self.push(&list.push(&tail)); + state.push(&list.push(&tail)); } Op::ListAppend => { - let tail = self.pop(); - let list = self.pop(); + let tail = state.pop(); + let list = state.pop(); let Variant::List(list) = list.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; let Variant::List(tail) = tail.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; // TODO: quota check SVO_MAX_LIST_CONCAT -> E_QUOTA let new_list = list.iter().chain(tail.iter()); - self.push(&v_list(&new_list.cloned().collect::>())); + state.push(&v_list(&new_list.cloned().collect::>())); } Op::IndexSet => { // collection[index] = value - let value = self.pop(); /* rhs value */ + let value = state.pop(); /* rhs value */ // Index into range, must be int. - let index = self.pop(); + let index = state.pop(); - let lhs = self.pop(); /* lhs except last index, should be list or str */ + let lhs = state.pop(); /* lhs except last index, should be list or str */ let i = match one_to_zero_index(&index) { Ok(i) => i, - Err(e) => return self.push_error(e), + Err(e) => return self.push_error(state, e), }; match lhs.index_set(i, &value) { Ok(v) => { - self.push(&v); + state.push(&v); } Err(e) => { - return self.push_error(e); + return self.push_error(state, e); } } } Op::MakeSingletonList => { - let v = self.pop(); - self.push(&v_list(&[v])) + let v = state.pop(); + state.push(&v_list(&[v])) } Op::PutTemp => { - self.top_mut().temp = self.peek_top(); + state.top_mut().temp = state.peek_top(); } Op::PushTemp => { - let tmp = self.top().temp.clone(); - self.push(&tmp); - self.top_mut().temp = v_none(); + let tmp = state.top().temp.clone(); + state.push(&tmp); + state.top_mut().temp = v_none(); } Op::Eq => { - binary_bool_op!(self, ==); + binary_bool_op!(state, ==); } Op::Ne => { - binary_bool_op!(self, !=); + binary_bool_op!(state, !=); } Op::Gt => { - binary_bool_op!(self, >); + binary_bool_op!(state, >); } Op::Lt => { - binary_bool_op!(self, <); + binary_bool_op!(state, <); } Op::Ge => { - binary_bool_op!(self, >=); + binary_bool_op!(state, >=); } Op::Le => { - binary_bool_op!(self, <=); + binary_bool_op!(state, <=); } Op::In => { - let lhs = self.pop(); - let rhs = self.pop(); + let lhs = state.pop(); + let rhs = state.pop(); let r = lhs.index_in(&rhs); if let Variant::Err(e) = r.variant() { - return self.push_error(*e); + return self.push_error(state, *e); } - self.push(&r); + state.push(&r); } Op::Mul => { - binary_var_op!(self, mul); + binary_var_op!(self, state, mul); } Op::Sub => { - binary_var_op!(self, sub); + binary_var_op!(self, state, sub); } Op::Div => { // Explicit division by zero check to raise E_DIV. // Note that LambdaMOO consider 1/0.0 to be E_DIV, but Rust permits it, creating // `inf`. I'll follow Rust's lead here, unless it leads to problems. - let divargs = self.peek(2); + let divargs = state.peek(2); if let Variant::Int(0) = divargs[1].variant() { - return self.push_error(E_DIV); + return self.push_error(state, E_DIV); }; - binary_var_op!(self, div); + binary_var_op!(self, state, div); } Op::Add => { - binary_var_op!(self, add); + binary_var_op!(self, state, add); } Op::Exp => { - binary_var_op!(self, pow); + binary_var_op!(self, state, pow); } Op::Mod => { - binary_var_op!(self, modulus); + binary_var_op!(self, state, modulus); } Op::And(label) => { - let v = self.peek_top().is_true(); + let v = state.peek_top().is_true(); if !v { - self.jump(label) + state.jump(label) } else { - self.pop(); + state.pop(); } } Op::Or(label) => { - let v = self.peek_top().is_true(); + let v = state.peek_top().is_true(); if v { - self.jump(label); + state.jump(label); } else { - self.pop(); + state.pop(); } } Op::Not => { - let v = !self.pop().is_true(); - self.push(&v_bool(v)); + let v = !state.pop().is_true(); + state.push(&v_bool(v)); } Op::UnaryMinus => { - let v = self.pop(); + let v = state.pop(); match v.negative() { - Err(e) => return self.push_error(e), - Ok(v) => self.push(&v), + Err(e) => return self.push_error(state, e), + Ok(v) => state.push(&v), } } Op::Push(ident) => { - let Some(v) = self.get_env(ident) else { - return self.push_error(E_VARNF); + let Some(v) = state.get_env(ident) else { + return self.push_error(state, E_VARNF); }; - self.push(&v.clone()); + state.push(&v.clone()); } Op::Put(ident) => { - let v = self.peek_top(); - self.set_env(ident, &v); + let v = state.peek_top(); + state.set_env(ident, &v); } Op::PushRef => { - let peek = self.peek(2); + let peek = state.peek(2); let (index, list) = (peek[1].clone(), peek[0].clone()); let index = match one_to_zero_index(&index) { Ok(i) => i, - Err(e) => return self.push_error(e), + Err(e) => return self.push_error(state, e), }; match list.index(index) { - Err(e) => return self.push_error(e), - Ok(v) => self.push(&v), + Err(e) => return self.push_error(state, e), + Ok(v) => state.push(&v), } } Op::Ref => { - let index = self.pop(); - let l = self.pop(); + let index = state.pop(); + let l = state.pop(); let index = match one_to_zero_index(&index) { Ok(i) => i, - Err(e) => return self.push_error(e), + Err(e) => return self.push_error(state, e), }; match l.index(index) { - Err(e) => return self.push_error(e), - Ok(v) => self.push(&v), + Err(e) => return self.push_error(state, e), + Ok(v) => state.push(&v), } } Op::RangeRef => { - let (to, from, base) = (self.pop(), self.pop(), self.pop()); + let (to, from, base) = (state.pop(), state.pop(), state.pop()); match (to.variant(), from.variant()) { (Variant::Int(to), Variant::Int(from)) => match base.range(*from, *to) { - Err(e) => return self.push_error(e), - Ok(v) => self.push(&v), + Err(e) => return self.push_error(state, e), + Ok(v) => state.push(&v), }, - (_, _) => return self.push_error(E_TYPE), + (_, _) => return self.push_error(state, E_TYPE), }; } Op::RangeSet => { - let (value, to, from, base) = (self.pop(), self.pop(), self.pop(), self.pop()); + let (value, to, from, base) = + (state.pop(), state.pop(), state.pop(), state.pop()); match (to.variant(), from.variant()) { (Variant::Int(to), Variant::Int(from)) => { match base.rangeset(value, *from, *to) { - Err(e) => return self.push_error(e), - Ok(v) => self.push(&v), + Err(e) => return self.push_error(state, e), + Ok(v) => state.push(&v), } } _ => { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); } } } Op::GPut { id } => { - self.set_env(id, &self.peek_top()); + state.set_env(id, &state.peek_top()); } Op::GPush { id } => { - let Some(v) = self.get_env(id) else { - return self.push_error(E_VARNF); + let Some(v) = state.get_env(id) else { + return self.push_error(state, E_VARNF); }; - self.push(&v.clone()); + state.push(&v.clone()); } Op::Length(offset) => { - let vsr = &self.top().valstack; + let vsr = &state.top().valstack; let v = &vsr[offset.0]; match v.len() { - Ok(v) => self.push(&v), - Err(e) => return self.push_error(e), + Ok(v) => state.push(&v), + Err(e) => return self.push_error(state, e), } } Op::GetProp => { - let (propname, obj) = (self.pop(), self.pop()); + let (propname, obj) = (state.pop(), state.pop()); return self - .resolve_property(exec_params.world_state, propname, obj) + .resolve_property(state, exec_params.world_state, propname, obj) .await; } Op::PushGetProp => { - let peeked = self.peek(2); + let peeked = state.peek(2); let (propname, obj) = (peeked[1].clone(), peeked[0].clone()); return self - .resolve_property(exec_params.world_state, propname, obj) + .resolve_property(state, exec_params.world_state, propname, obj) .await; } Op::PutProp => { - let (rhs, propname, obj) = (self.pop(), self.pop(), self.pop()); + let (rhs, propname, obj) = (state.pop(), state.pop(), state.pop()); return self - .set_property(exec_params.world_state, propname, obj, rhs) + .set_property(state, exec_params.world_state, propname, obj, rhs) .await; } Op::Fork { id, fv_offset } => { // Delay time should be on stack - let time = self.pop(); + let time = state.pop(); let Variant::Int(time) = time.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; if *time < 0 { - return self.push_error(E_INVARG); + return self.push_error(state, E_INVARG); } let delay = (*time != 0).then(|| Duration::from_secs(*time as u64)); - let new_activation = self.top().clone(); + let new_activation = state.top().clone(); let fork = Fork { - player: self.top().player, - progr: self.top().permissions, - parent_task_id: self.top().task_id, + player: state.top().player, + progr: state.top().permissions, + parent_task_id: state.top().task_id, delay, activation: new_activation, fork_vector_offset: fv_offset, @@ -493,66 +577,75 @@ impl VM { return ExecutionResult::DispatchFork(fork); } Op::Pass => { - let args = self.pop(); + let args = state.pop(); let Variant::List(args) = args.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; return self - .prepare_pass_verb(exec_params.world_state, &args[..]) + .prepare_pass_verb(state, exec_params.world_state, &args[..]) .await; } Op::CallVerb => { - let (args, verb, obj) = (self.pop(), self.pop(), self.pop()); + let (args, verb, obj) = (state.pop(), state.pop(), state.pop()); let (args, verb, obj) = match (args.variant(), verb.variant(), obj.variant()) { (Variant::List(l), Variant::Str(s), Variant::Obj(o)) => (l, s, o), _ => { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); } }; return self - .prepare_call_verb(exec_params.world_state, *obj, verb.as_str(), &args[..]) + .prepare_call_verb( + state, + exec_params.world_state, + *obj, + verb.as_str(), + &args[..], + ) .await; } Op::Return => { - let ret_val = self.pop(); - return self.unwind_stack(FinallyReason::Return(ret_val)); + let ret_val = state.pop(); + return self.unwind_stack(state, FinallyReason::Return(ret_val)); } Op::Return0 => { - return self.unwind_stack(FinallyReason::Return(v_int(0))); + return self.unwind_stack(state, FinallyReason::Return(v_int(0))); } Op::Done => { - return self.unwind_stack(FinallyReason::Return(v_none())); + return self.unwind_stack(state, FinallyReason::Return(v_none())); } Op::FuncCall { id } => { // Pop arguments, should be a list. - let args = self.pop(); + let args = state.pop(); let Variant::List(args) = args.variant() else { - return self.push_error(E_ARGS); + return self.push_error(state, E_ARGS); }; return self - .call_builtin_function(id.0 as usize, &args[..], &mut exec_params) + .call_builtin_function(state, id.0 as usize, &args[..], exec_params) .await; } Op::PushLabel(label) => { - self.top_mut() + state + .top_mut() .push_handler_label(HandlerType::CatchLabel(label)); } Op::TryFinally(label) => { - self.top_mut() + state + .top_mut() .push_handler_label(HandlerType::Finally(label)); } Op::Catch(_) => { - self.top_mut().push_handler_label(HandlerType::Catch(1)); + state.top_mut().push_handler_label(HandlerType::Catch(1)); } Op::TryExcept { num_excepts } => { - self.top_mut() + state + .top_mut() .push_handler_label(HandlerType::Catch(num_excepts)); } Op::EndCatch(label) | Op::EndExcept(label) => { let is_catch = op == Op::EndCatch(label); - let v = if is_catch { self.pop() } else { v_none() }; + let v = if is_catch { state.pop() } else { v_none() }; - let handler = self + let handler = state .top_mut() .pop_applicable_handler() .expect("Missing handler for try/catch/except"); @@ -561,26 +654,26 @@ impl VM { }; for _i in 0..num_excepts { - self.pop(); /* code list */ - self.top_mut().handler_stack.pop(); + state.pop(); /* code list */ + state.top_mut().handler_stack.pop(); } if is_catch { - self.push(&v); + state.push(&v); } - self.jump(label); + state.jump(label); } Op::EndFinally => { - let Some(finally_handler) = self.top_mut().pop_applicable_handler() else { + let Some(finally_handler) = state.top_mut().pop_applicable_handler() else { panic!("Missing handler for try/finally") }; let HandlerType::Finally(_) = finally_handler.handler_type else { panic!("Handler is not a finally handler") }; - self.push(&v_int(0) /* fallthrough */); - self.push(&v_int(0)); + state.push(&v_int(0) /* fallthrough */); + state.push(&v_int(0)); } Op::Continue => { - let why = self.pop(); + let why = state.pop(); let Variant::Int(why) = why.variant() else { panic!("'why' is not an integer representing a FinallyReason"); }; @@ -594,7 +687,7 @@ impl VM { | FinallyReason::Uncaught(UncaughtException { .. }) | FinallyReason::Return(_) | FinallyReason::Exit { .. } => { - return self.unwind_stack(why); + return self.unwind_stack(state, why); } FinallyReason::Abort => { panic!("Unexpected FINALLY_ABORT in Continue") @@ -602,11 +695,11 @@ impl VM { } } Op::ExitId(label) => { - self.jump(label); + state.jump(label); continue; } Op::Exit { stack, label } => { - return self.unwind_stack(FinallyReason::Exit { stack, label }); + return self.unwind_stack(state, FinallyReason::Exit { stack, label }); } Op::Scatter { nargs, @@ -617,16 +710,16 @@ impl VM { .. } => { let have_rest = rest <= nargs; - let rhs = self.peek_top(); + let rhs = state.peek_top(); let Variant::List(rhs_values) = rhs.variant() else { - self.pop(); - return self.push_error(E_TYPE); + state.pop(); + return self.push_error(state, E_TYPE); }; let len = rhs_values.len(); if len < nreq || !have_rest && len > nargs { - self.pop(); - return self.push_error(E_ARGS); + state.pop(); + return self.push_error(state, E_ARGS); } assert_eq!(nargs, labels.len()); @@ -649,22 +742,22 @@ impl VM { v.push(rest.clone()); } let rest = v_list(&v); - self.set_env(*id, &rest); + state.set_env(*id, &rest); } ScatterLabel::Required(id) => { let Some(arg) = args_iter.next() else { - return self.push_error(E_ARGS); + return self.push_error(state, E_ARGS); }; - self.set_env(*id, arg); + state.set_env(*id, arg); } ScatterLabel::Optional(id, jump_to) => { if nopt_avail > 0 { nopt_avail -= 1; let Some(arg) = args_iter.next() else { - return self.push_error(E_ARGS); + return self.push_error(state, E_ARGS); }; - self.set_env(*id, arg); + state.set_env(*id, arg); } else if jump_where.is_none() && jump_to.is_some() { jump_where = *jump_to; } @@ -672,14 +765,14 @@ impl VM { } } match jump_where { - None => self.jump(done), - Some(jump_where) => self.jump(jump_where), + None => state.jump(done), + Some(jump_where) => state.jump(jump_where), } } Op::CheckListForSplice => { - let Variant::List(_) = self.peek_top().variant() else { - self.pop(); - return self.push_error(E_TYPE); + let Variant::List(_) = state.peek_top().variant() else { + state.pop(); + return self.push_error(state, E_TYPE); }; } } diff --git a/crates/kernel/src/vm/vm_unwind.rs b/crates/kernel/src/vm/vm_unwind.rs index c32ec6e0..d7b245b2 100644 --- a/crates/kernel/src/vm/vm_unwind.rs +++ b/crates/kernel/src/vm/vm_unwind.rs @@ -13,7 +13,7 @@ // use bincode::{Decode, Encode}; -use tracing::{debug, trace}; +use tracing::trace; use moor_values::model::verbs::VerbFlag; use moor_values::var::error::{Error, ErrorPack}; @@ -22,7 +22,7 @@ use moor_values::var::{v_err, v_int, v_list, v_none, v_objid, v_str, Var}; use moor_values::NOTHING; use crate::vm::activation::{Activation, HandlerType}; -use crate::vm::{ExecutionResult, VM}; +use crate::vm::{ExecutionResult, VMExecState, VM}; use moor_compiler::builtins::BUILTIN_DESCRIPTORS; use moor_compiler::labels::{Label, Offset}; @@ -85,11 +85,11 @@ impl FinallyReason { impl VM { /// Find the currently active catch handler for a given error code, if any. /// Then return the stack offset (from now) of the activation frame containing the handler. - fn find_handler_active(&self, raise_code: Error) -> Option { + fn find_handler_active(&self, state: &mut VMExecState, raise_code: Error) -> Option { // Scan activation frames and their stacks, looking for the first _Catch we can find. - let mut frame = self.stack.len() - 1; + let mut frame = state.stack.len() - 1; loop { - let activation = &self.stack.get(frame)?; + let activation = &state.stack.get(frame)?; for handler in &activation.handler_stack { if let HandlerType::Catch(cnt) = handler.handler_type { // Found one, now scan forwards from 'cnt' backwards in the valstack looking for either the first @@ -162,11 +162,11 @@ impl VM { } /// Compose a backtrace list of strings for an error, starting from the current stack frame. - fn error_backtrace_list(&self, raise_msg: &str) -> Vec { + fn error_backtrace_list(&self, state: &mut VMExecState, raise_msg: &str) -> Vec { // Walk live activation frames and produce a written representation of a traceback for each // frame. let mut backtrace_list = vec![]; - for (i, a) in self.stack.iter().rev().enumerate() { + for (i, a) in state.stack.iter().rev().enumerate() { let mut pieces = vec![]; if i != 0 { pieces.push("... called from ".to_string()); @@ -200,61 +200,61 @@ impl VM { /// Raise an error. /// Finds the catch handler for the given error if there is one, and unwinds the stack to it. /// If there is no handler, creates an 'Uncaught' reason with backtrace, and unwinds with that. - fn raise_error_pack(&mut self, p: ErrorPack) -> ExecutionResult { + fn raise_error_pack(&self, state: &mut VMExecState, p: ErrorPack) -> ExecutionResult { trace!(error = ?p, "raising error"); // Look for first active catch handler's activation frame and its (reverse) offset in the activation stack. - let handler_activ = self.find_handler_active(p.code); + let handler_activ = self.find_handler_active(state, p.code); let why = if let Some(handler_active_num) = handler_activ { FinallyReason::Raise { code: p.code, msg: p.msg, - stack: self.make_stack_list(&self.stack, handler_active_num), + stack: self.make_stack_list(&state.stack, handler_active_num), } } else { FinallyReason::Uncaught(UncaughtException { code: p.code, msg: p.msg.clone(), value: p.value, - stack: self.make_stack_list(&self.stack, 0), - backtrace: self.error_backtrace_list(p.msg.as_str()), + stack: self.make_stack_list(&state.stack, 0), + backtrace: self.error_backtrace_list(state, p.msg.as_str()), }) }; - self.unwind_stack(why) + self.unwind_stack(state, why) } /// Push an error to the stack and raise it. - pub(crate) fn push_error(&mut self, code: Error) -> ExecutionResult { + pub(crate) fn push_error(&self, state: &mut VMExecState, code: Error) -> ExecutionResult { trace!(?code, "push_error"); - self.push(&v_err(code)); + state.push(&v_err(code)); // Check 'd' bit of running verb. If it's set, we raise the error. Otherwise nope. - if let Some(activation) = self.stack.last() { + if let Some(activation) = state.stack.last() { if activation .verb_info .verbdef() .flags() .contains(VerbFlag::Debug) { - return self.raise_error_pack(code.make_error_pack(None)); + return self.raise_error_pack(state, code.make_error_pack(None)); } } ExecutionResult::More } /// Same as push_error, but for returns from builtin functions. - pub(crate) fn push_bf_error(&mut self, code: Error) -> ExecutionResult { + pub(crate) fn push_bf_error(&self, state: &mut VMExecState, code: Error) -> ExecutionResult { trace!(?code, "push_bf_error"); // No matter what, the error value has to be on the stack of the *calling* verb, not on this // frame; as we are incapable of doing anything with it, we'll never pop it, being a builtin // function. If we stack_unwind, it will propagate to parent. Otherwise, it will be popped // by the parent anyways. - self.parent_activation_mut().push(v_err(code)); + state.parent_activation_mut().push(v_err(code)); // Check 'd' bit of running verb. If it's set, we raise the error. Otherwise nope. // Filter out frames for builtin invocations - let verb_frame = self.stack.iter().rev().find(|a| a.bf_index.is_none()); + let verb_frame = state.stack.iter().rev().find(|a| a.bf_index.is_none()); if let Some(activation) = verb_frame { if activation .verb_info @@ -262,30 +262,35 @@ impl VM { .flags() .contains(VerbFlag::Debug) { - return self.raise_error_pack(code.make_error_pack(None)); + return self.raise_error_pack(state, code.make_error_pack(None)); } } // If we're not unwinding, we need to pop the builtin function's activation frame. - self.stack.pop(); + state.stack.pop(); ExecutionResult::More } /// Push an error to the stack with a description and raise it. - pub(crate) fn push_error_msg(&mut self, code: Error, msg: String) -> ExecutionResult { + pub(crate) fn push_error_msg( + &self, + state: &mut VMExecState, + code: Error, + msg: String, + ) -> ExecutionResult { trace!(?code, msg, "push_error_msg"); - self.push(&v_err(code)); + state.push(&v_err(code)); - self.raise_error(code) + self.raise_error(state, code) } /// Only raise an error if the 'd' bit is set on the running verb. Most times this is what we /// want. - pub(crate) fn raise_error(&mut self, code: Error) -> ExecutionResult { + pub(crate) fn raise_error(&self, state: &mut VMExecState, code: Error) -> ExecutionResult { trace!(?code, "maybe_raise_error"); // Check 'd' bit of running verb. If it's set, we raise the error. Otherwise nope. // Filter out frames for builtin invocations - let verb_frame = self.stack.iter().rev().find(|a| a.bf_index.is_none()); + let verb_frame = state.stack.iter().rev().find(|a| a.bf_index.is_none()); if let Some(activation) = verb_frame { if activation .verb_info @@ -293,16 +298,16 @@ impl VM { .flags() .contains(VerbFlag::Debug) { - return self.raise_error_pack(code.make_error_pack(None)); + return self.raise_error_pack(state, code.make_error_pack(None)); } } ExecutionResult::More } /// Explicitly raise an error, regardless of the 'd' bit. - pub(crate) fn throw_error(&mut self, code: Error) -> ExecutionResult { + pub(crate) fn throw_error(&self, state: &mut VMExecState, code: Error) -> ExecutionResult { trace!(?code, "raise_error"); - self.raise_error_pack(code.make_error_pack(None)) + self.raise_error_pack(state, code.make_error_pack(None)) } /// Unwind the stack with the given reason and return an execution result back to the VM loop @@ -310,12 +315,13 @@ impl VM { /// Contains all the logic for handling the various reasons for exiting a verb execution: /// * Error raises of various kinds /// * Return values - pub(crate) fn unwind_stack(&mut self, why: FinallyReason) -> ExecutionResult { - debug!(?why, this = ?self.top().this, from = self.top().verb_name, - line = self.top().find_line_no(self.top().pc).unwrap_or(0), - "unwind_stack"); + pub(crate) fn unwind_stack( + &self, + state: &mut VMExecState, + why: FinallyReason, + ) -> ExecutionResult { // Walk activation stack from bottom to top, tossing frames as we go. - while let Some(a) = self.stack.last_mut() { + while let Some(a) = state.stack.last_mut() { while a.valstack.pop().is_some() { // Check the handler stack to see if we've hit a finally or catch handler that // was registered for this position in the value stack. @@ -386,7 +392,7 @@ impl VM { // the returned value up out of the interpreter loop. // Otherwise pop off this activation, and continue unwinding. if let FinallyReason::Return(value) = &why { - if self.stack.len() == 1 { + if state.stack.len() == 1 { return ExecutionResult::Complete(value.clone()); } } @@ -402,9 +408,9 @@ impl VM { return ExecutionResult::Exception(why); } - self.stack.pop().expect("Stack underflow"); + state.stack.pop().expect("Stack underflow"); - if self.stack.is_empty() { + if state.stack.is_empty() { return ExecutionResult::Complete(v_none()); } // TODO builtin function unwinding stuff @@ -414,8 +420,7 @@ impl VM { // (Unless we're the final activation, in which case that should have been handled // above) if let FinallyReason::Return(value) = &why { - self.push(value); - trace!(value = ?value, verb_name = self.top().verb_name, "RETURN"); + state.push(value); return ExecutionResult::More; } } diff --git a/crates/kernel/src/vm/vm_util.rs b/crates/kernel/src/vm/vm_util.rs index 5beba606..bfef381e 100644 --- a/crates/kernel/src/vm/vm_util.rs +++ b/crates/kernel/src/vm/vm_util.rs @@ -12,54 +12,51 @@ // this program. If not, see . // -use moor_values::NOTHING; use tracing::debug; use moor_values::model::world_state::WorldState; use moor_values::var::error::Error::{E_INVIND, E_TYPE}; -use moor_values::var::objid::Objid; use moor_values::var::variant::Variant; use moor_values::var::Var; -use crate::vm::activation::{Activation, Caller}; -use crate::vm::{ExecutionResult, VM}; -use moor_compiler::labels::{Label, Name}; -use moor_compiler::opcode::Op; +use crate::vm::{ExecutionResult, VMExecState, VM}; impl VM { /// VM-level property resolution. pub(crate) async fn resolve_property( - &mut self, - state: &mut dyn WorldState, + &self, + state: &mut VMExecState, + world_state: &mut dyn WorldState, propname: Var, obj: Var, ) -> ExecutionResult { let Variant::Str(propname) = propname.variant() else { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); }; let Variant::Obj(obj) = obj.variant() else { - return self.push_error(E_INVIND); + return self.push_error(state, E_INVIND); }; - let result = state - .retrieve_property(self.top().permissions, *obj, propname.as_str()) + let result = world_state + .retrieve_property(state.top().permissions, *obj, propname.as_str()) .await; let v = match result { Ok(v) => v, Err(e) => { debug!(obj = ?obj, propname = propname.as_str(), "Error resolving property"); - return self.push_error(e.to_error_code()); + return self.push_error(state, e.to_error_code()); } }; - self.push(&v); + state.push(&v); ExecutionResult::More } /// VM-level property assignment pub(crate) async fn set_property( - &mut self, - state: &mut dyn WorldState, + &self, + state: &mut VMExecState, + world_state: &mut dyn WorldState, propname: Var, obj: Var, value: Var, @@ -67,133 +64,22 @@ impl VM { let (propname, obj) = match (propname.variant(), obj.variant()) { (Variant::Str(propname), Variant::Obj(obj)) => (propname, obj), (_, _) => { - return self.push_error(E_TYPE); + return self.push_error(state, E_TYPE); } }; - let update_result = state - .update_property(self.top().permissions, *obj, propname.as_str(), &value) + let update_result = world_state + .update_property(state.top().permissions, *obj, propname.as_str(), &value) .await; match update_result { Ok(()) => { - self.push(&value); + state.push(&value); } Err(e) => { - return self.push_error(e.to_error_code()); + return self.push_error(state, e.to_error_code()); } } ExecutionResult::More } - - /// Return the callers stack, in the format expected by the `callers` built-in function. - pub(crate) fn callers(&self) -> Vec { - let mut callers_iter = self.stack.iter().rev(); - callers_iter.next(); // skip the top activation, that's our current frame - - let mut callers = vec![]; - for activation in callers_iter { - let verb_name = activation.verb_name.clone(); - let definer = activation.verb_definer(); - let player = activation.player; - let line_number = 0; // TODO: fix after decompilation support - let this = activation.this; - let perms = activation.permissions; - let programmer = if activation.bf_index.is_some() { - NOTHING - } else { - perms - }; - callers.push(Caller { - verb_name, - definer, - player, - line_number, - this, - programmer, - }); - } - callers - } - - pub(crate) fn top_mut(&mut self) -> &mut Activation { - self.stack.last_mut().expect("activation stack underflow") - } - - pub(crate) fn top(&self) -> &Activation { - self.stack.last().expect("activation stack underflow") - } - - pub(crate) fn caller_perms(&self) -> Objid { - // Filter out builtins. - let mut stack_iter = self.stack.iter().rev().filter(|a| a.bf_index.is_none()); - // caller is the frame just before us. - stack_iter.next(); - stack_iter.next().map(|a| a.permissions).unwrap_or(NOTHING) - } - - pub(crate) fn task_perms(&self) -> Objid { - let stack_top = self.stack.iter().rev().find(|a| a.bf_index.is_none()); - stack_top.map(|a| a.permissions).unwrap_or(NOTHING) - } - - pub(crate) fn set_task_perms(&mut self, perms: Objid) { - self.top_mut().permissions = perms; - } - - pub(crate) fn caller(&self) -> Objid { - let stack_iter = self.stack.iter().rev(); - for activation in stack_iter { - if activation.bf_index.is_some() { - continue; - } - return activation.this; - } - NOTHING - } - - pub(crate) fn parent_activation_mut(&mut self) -> &mut Activation { - let len = self.stack.len(); - self.stack - .get_mut(len - 2) - .expect("activation stack underflow") - } - - pub(crate) fn pop(&mut self) -> Var { - self.top_mut().pop().unwrap_or_else(|| { - panic!( - "stack underflow, activation depth: {} PC: {}", - self.stack.len(), - self.top().pc - ) - }) - } - - pub(crate) fn push(&mut self, v: &Var) { - self.top_mut().push(v.clone()) - } - - pub(crate) fn next_op(&mut self) -> Option { - self.top_mut().next_op() - } - - pub(crate) fn jump(&mut self, label: Label) { - self.top_mut().jump(label) - } - - pub(crate) fn get_env(&self, id: Name) -> Option<&Var> { - self.top().environment.get(id.0 as usize) - } - - pub(crate) fn set_env(&mut self, id: Name, v: &Var) { - self.top_mut().environment.insert(id.0 as usize, v.clone()); - } - - pub(crate) fn peek(&self, amt: usize) -> Vec { - self.top().peek(amt) - } - - pub(crate) fn peek_top(&self) -> Var { - self.top().peek_top().expect("stack underflow") - } }