Implement command queue, and let match table interact with it

It can be called from Lua.
This commit is contained in:
Condorra 2024-09-26 23:30:43 +10:00
parent b1bf0f317a
commit ff840c04c2
6 changed files with 249 additions and 27 deletions

View File

@ -1,7 +1,12 @@
use crate::{ use crate::{
echo_to_term_frame, lua_engine::LuaState, parsing::parse_commands, GlobalMemoCell, TermFrame, echo_to_term_frame,
lua_engine::LuaState,
parsing::{parse_commands, ParsedCommand},
GlobalMemoCell, TermFrame,
}; };
use itertools::{join, Itertools}; use itertools::Itertools;
use wasm_bindgen::JsValue;
use web_sys::console;
pub fn debrace(inp: &str) -> &str { pub fn debrace(inp: &str) -> &str {
let v = inp.trim(); let v = inp.trim();
@ -16,11 +21,10 @@ fn reentrant_command_handler(
lua_state: &mut LuaState, lua_state: &mut LuaState,
globals: &GlobalMemoCell, globals: &GlobalMemoCell,
term_frame: &TermFrame, term_frame: &TermFrame,
command_in: &str, commands_in: &[ParsedCommand],
) { ) {
echo_to_term_frame(globals, term_frame, "\r").unwrap_or(());
lua_state.set_current_frame(term_frame); lua_state.set_current_frame(term_frame);
for command in parse_commands(command_in).commands { for command in commands_in {
match command.split_out_command() { match command.split_out_command() {
None => (), None => (),
Some((cmd, rest)) => { Some((cmd, rest)) => {
@ -34,13 +38,9 @@ fn reentrant_command_handler(
} }
} }
} else if let Ok(repeat_count) = command_rest.parse::<u16>() { } else if let Ok(repeat_count) = command_rest.parse::<u16>() {
let cmds = &[rest];
for _ in 0..repeat_count { for _ in 0..repeat_count {
reentrant_command_handler( reentrant_command_handler(lua_state, globals, term_frame, cmds);
lua_state,
globals,
term_frame,
&join(rest.arguments.iter(), " "),
);
} }
} else { } else {
match lua_state.execute_command(command_rest, &rest) { match lua_state.execute_command(command_rest, &rest) {
@ -69,15 +69,53 @@ fn reentrant_command_handler(
} }
pub fn command_handler(globals: &GlobalMemoCell, term_frame: &TermFrame, command_in: &str) { pub fn command_handler(globals: &GlobalMemoCell, term_frame: &TermFrame, command_in: &str) {
match globals.lua_engine.try_borrow_mut() { echo_to_term_frame(globals, term_frame, "\r").unwrap_or(());
Err(_) => echo_to_term_frame( {
globals, let mut cq = globals.command_queue.borrow_mut();
term_frame, for cmd in parse_commands(command_in).commands {
"Attempt to re-enter command handler during processing.\r\n", cq.push_back((term_frame.clone(), cmd));
) }
.unwrap_or(()), // Ignore error handling error. }
Ok(mut lua_state_m) => { execute_queue(globals);
reentrant_command_handler(&mut lua_state_m, globals, term_frame, command_in) }
pub fn execute_queue(globals: &GlobalMemoCell) {
let mut steps: u64 = 0;
const STEP_LIMIT: u64 = 500;
loop {
let queue_head = globals.command_queue.borrow_mut().pop_front();
match queue_head {
None => return,
Some((frame, command_in)) => {
steps += 1;
match globals.lua_engine.try_borrow_mut() {
Err(_) => console::log_1(&JsValue::from_str(
"Can't borrow lua_engine when executing queue!",
)),
Ok(mut lua_state_m) => {
reentrant_command_handler(
&mut lua_state_m,
globals,
&frame,
&[command_in.clone()],
);
if steps > STEP_LIMIT {
let new_queue = globals.command_queue.take();
if !new_queue.is_empty() {
echo_to_term_frame(
globals,
&frame,
&format!("Executing queued actions resulted in more than {} steps. This usually means a command is creating similar commands in a loop. The following commands were dropped from the queue to stop execution: {}.",
STEP_LIMIT,
&new_queue.iter().map(
|(_fr, cmd)| cmd.to_string())
.join("; "))).unwrap_or(());
return;
}
}
}
}
}
} }
} }
} }

View File

@ -10,7 +10,13 @@ use piccolo::{
use yew::UseStateSetter; use yew::UseStateSetter;
use crate::{ use crate::{
id_intern::intern_id, parsing::ParsedCommand, GlobalLayoutCell, GlobalMemoCell, TermFrame, id_intern::intern_id,
match_table::{
create_match_table, match_table_add, match_table_lua_table, match_table_remove,
match_table_try_run_sub,
},
parsing::ParsedCommand,
GlobalLayoutCell, GlobalMemoCell, TermFrame,
}; };
pub struct LuaState { pub struct LuaState {
@ -145,6 +151,7 @@ pub fn install_lua_globals(
register_command!(alias); register_command!(alias);
register_command!(close_mud); register_command!(close_mud);
register_command!(connect_mud); register_command!(connect_mud);
register_command!(create_match_table);
register_command!(delete_mud); register_command!(delete_mud);
register_command!(echo); register_command!(echo);
register_command!(echo_frame); register_command!(echo_frame);
@ -244,6 +251,18 @@ pub fn install_lua_globals(
register_class_function!(frameroute_class_table, "new", new_frameroute); register_class_function!(frameroute_class_table, "new", new_frameroute);
register_class_function!(frameroute_class_table, "route", frameroute_route); register_class_function!(frameroute_class_table, "route", frameroute_route);
let match_table_class_table = Table::new(&ctx);
classes_table.set(ctx, "match_table", match_table_class_table)?;
match_table_class_table.set(ctx, MetaMethod::Index, match_table_class_table)?;
register_class_function!(match_table_class_table, "add", match_table_add);
register_class_function!(match_table_class_table, "remove", match_table_remove);
register_class_function!(match_table_class_table, "lua_table", match_table_lua_table);
register_class_function!(
match_table_class_table,
"try_run_sub",
match_table_try_run_sub
);
Ok(()) Ok(())
}) })
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;

View File

@ -9,6 +9,7 @@ use web_sys::console;
use yew::UseStateSetter; use yew::UseStateSetter;
use crate::{ use crate::{
command_handler::execute_queue,
id_intern::{intern_id, unintern_id}, id_intern::{intern_id, unintern_id},
telnet::{parse_telnet_buf, TelnetOutput}, telnet::{parse_telnet_buf, TelnetOutput},
websocket::{connect_websocket, send_message_to_mud, WebSocketId}, websocket::{connect_websocket, send_message_to_mud, WebSocketId},
@ -65,7 +66,12 @@ pub fn handle_websocket_output(
.map_err(|err| format!("{}", err)) .map_err(|err| format!("{}", err))
} }
pub fn handle_websocket_output_log_err(socket: &WebSocketId, engine: &mut LuaState, input: &[u8]) { pub fn handle_websocket_output_log_err(
socket: &WebSocketId,
globals: &GlobalMemoCell,
engine: &mut LuaState,
input: &[u8],
) {
match handle_websocket_output(socket, engine, input) { match handle_websocket_output(socket, engine, input) {
Ok(()) => {} Ok(()) => {}
Err(e) => console::log_2( Err(e) => console::log_2(
@ -73,6 +79,7 @@ pub fn handle_websocket_output_log_err(socket: &WebSocketId, engine: &mut LuaSta
&JsValue::from_str(&e), &JsValue::from_str(&e),
), ),
} }
execute_queue(globals);
} }
pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Result<(), String> { pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Result<(), String> {
@ -95,7 +102,11 @@ pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Re
.map_err(|err| format!("{}", err)) .map_err(|err| format!("{}", err))
} }
pub fn handle_websocket_close_log_err(socket: &WebSocketId, engine: &mut LuaState) { pub fn handle_websocket_close_log_err(
socket: &WebSocketId,
globals: &GlobalMemoCell,
engine: &mut LuaState,
) {
match handle_websocket_close(socket, engine) { match handle_websocket_close(socket, engine) {
Ok(()) => {} Ok(()) => {}
Err(e) => console::log_2( Err(e) => console::log_2(
@ -103,6 +114,7 @@ pub fn handle_websocket_close_log_err(socket: &WebSocketId, engine: &mut LuaStat
&JsValue::from_str(&e), &JsValue::from_str(&e),
), ),
} }
execute_queue(globals);
} }
pub(super) fn sendmud_raw<'gc>( pub(super) fn sendmud_raw<'gc>(

View File

@ -1,6 +1,8 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc; use std::rc::Rc;
use parsing::ParsedCommand;
use term_split::TermSplit; use term_split::TermSplit;
use yew::prelude::*; use yew::prelude::*;
@ -26,6 +28,7 @@ pub struct GlobalMemoState {
frame_registry: RefCell<RegisteredTermFrames>, frame_registry: RefCell<RegisteredTermFrames>,
lua_engine: RefCell<LuaState>, lua_engine: RefCell<LuaState>,
ws_registry: RefCell<RegisteredWebSockets>, ws_registry: RefCell<RegisteredWebSockets>,
command_queue: RefCell<VecDeque<(TermFrame, ParsedCommand)>>,
// A cache of the latest layout info (separate from the state). // A cache of the latest layout info (separate from the state).
// Updating this doesn't force a relayout, so only update the cache when // Updating this doesn't force a relayout, so only update the cache when
@ -66,6 +69,7 @@ fn app() -> Html {
let global_memo = use_memo((), |_| GlobalMemoState { let global_memo = use_memo((), |_| GlobalMemoState {
frame_registry: RegisteredTermFrames::new().into(), frame_registry: RegisteredTermFrames::new().into(),
ws_registry: RegisteredWebSockets::new().into(), ws_registry: RegisteredWebSockets::new().into(),
command_queue: VecDeque::new().into(),
lua_engine: LuaState::setup().expect("Can create interpreter").into(), lua_engine: LuaState::setup().expect("Can create interpreter").into(),
layout: RefCell::new((*global_layout).clone()), layout: RefCell::new((*global_layout).clone()),
}); });

View File

@ -1,13 +1,23 @@
use std::{collections::VecDeque, str::FromStr}; use std::{
collections::{BTreeSet, VecDeque},
str::FromStr,
};
use anyhow::bail; use anyhow::bail;
use gc_arena::{Collect, GcRefLock, Rootable};
use itertools::Itertools; use itertools::Itertools;
use piccolo::{Context, IntoValue, Table, Value}; use piccolo::{Callback, Context, IntoValue, Table, UserData, Value};
use regex::Regex; use regex::Regex;
use yew::UseStateSetter;
use crate::parsing::{parse_commands, quote_string, ArgumentGuard, ParsedArgument, ParsedCommand}; use crate::{
lua_engine::frames::try_unwrap_frame,
parsing::{parse_commands, quote_string, ArgumentGuard, ParsedArgument, ParsedCommand},
GlobalLayoutCell, GlobalMemoCell,
};
#[derive(Default, Debug)] #[derive(Default, Debug, Collect)]
#[collect(require_static)]
pub struct MatchSubTable { pub struct MatchSubTable {
contents: Vec<MatchRecord>, contents: Vec<MatchRecord>,
} }
@ -74,6 +84,7 @@ impl MatchSubTable {
} }
pub fn add_record(&mut self, match_text: &str, sub_text: &str) -> anyhow::Result<()> { pub fn add_record(&mut self, match_text: &str, sub_text: &str) -> anyhow::Result<()> {
self.remove_record(match_text).unwrap_or(());
let rex = Regex::new(match_text)?; let rex = Regex::new(match_text)?;
let parse_result = parse_commands(sub_text); let parse_result = parse_commands(sub_text);
@ -91,6 +102,27 @@ impl MatchSubTable {
}) })
.collect::<anyhow::Result<Vec<SubCommand>>>()?; .collect::<anyhow::Result<Vec<SubCommand>>>()?;
let vars: BTreeSet<String> = sub_commands
.iter()
.flat_map(|sc| {
sc.arguments.iter().flat_map(|arg| {
arg.text_parts.iter().filter_map(|tp| match tp {
SubTextPart::Variable(v) => Some(v.clone()),
_ => None,
})
})
})
.collect();
let max_captures = rex.captures_len();
let valid_vars: BTreeSet<String> = (0..max_captures)
.map(|n| n.to_string())
.chain(rex.capture_names().filter_map(|o| o.map(|n| n.to_string())))
.collect();
let invalid_vars: Vec<String> = vars.difference(&valid_vars).cloned().collect();
if !invalid_vars.is_empty() {
bail!("Invalid variables in substitution: {:?}", invalid_vars);
}
self.contents.push(MatchRecord { self.contents.push(MatchRecord {
match_text: match_text.to_owned(), match_text: match_text.to_owned(),
match_regex: rex, match_regex: rex,
@ -99,6 +131,19 @@ impl MatchSubTable {
}); });
Ok(()) Ok(())
} }
pub fn remove_record(&mut self, match_text: &str) -> anyhow::Result<()> {
match self
.contents
.iter()
.enumerate()
.find(|(_idx, rec)| rec.match_text == match_text)
{
None => bail!("No matching record found."),
Some((idx, _)) => self.contents.remove(idx),
};
Ok(())
}
} }
fn parsedarg_to_subarg(parsedarg: ParsedArgument) -> anyhow::Result<SubArgument> { fn parsedarg_to_subarg(parsedarg: ParsedArgument) -> anyhow::Result<SubArgument> {
@ -398,4 +443,106 @@ mod tests {
); );
assert_eq!(parse_commands(&ser_result).commands, vec![expected]); assert_eq!(parse_commands(&ser_result).commands, vec![expected]);
} }
#[test]
fn matchsubtable_rejects_invalid() {
let mut table: MatchSubTable = Default::default();
assert!(table
.add_record("^foo (?<bar>[a-z]+) baz", "$wrong")
.is_err())
}
}
pub fn create_match_table<'gc, 'a>(
ctx: Context<'gc>,
_global_memo: &'a GlobalMemoCell,
_global_layout: &'a UseStateSetter<GlobalLayoutCell>,
) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let _: () = stack.consume(ctx)?;
let user_data = UserData::<'gc>::new::<Rootable!['gcb => GcRefLock<'gcb, MatchSubTable>]>(
&ctx,
GcRefLock::new(&ctx, <MatchSubTable as Default>::default().into()),
);
let match_table_class: Table = ctx
.get_global::<Table>("classes")?
.get(ctx, "match_table")?;
user_data.set_metatable(&ctx, Some(match_table_class));
stack.push_back(user_data.into_value(ctx));
Ok(piccolo::CallbackReturn::Return)
})
}
pub fn match_table_add<'gc, 'a>(
ctx: Context<'gc>,
_global_memo: &'a GlobalMemoCell,
) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let (match_table, match_text, sub_text): (UserData, piccolo::String, piccolo::String) =
stack.consume(ctx)?;
match_table
.downcast::<Rootable!['gcb => GcRefLock<'gcb, MatchSubTable>]>()?
.borrow_mut(&ctx)
.add_record(match_text.to_str()?, sub_text.to_str()?)?;
Ok(piccolo::CallbackReturn::Return)
})
}
pub fn match_table_remove<'gc, 'a>(
ctx: Context<'gc>,
_global_memo: &'a GlobalMemoCell,
) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let (match_table, match_text): (UserData, piccolo::String) = stack.consume(ctx)?;
match_table
.downcast::<Rootable!['gcb => GcRefLock<'gcb, MatchSubTable>]>()?
.borrow_mut(&ctx)
.remove_record(match_text.to_str()?)?;
Ok(piccolo::CallbackReturn::Return)
})
}
pub fn match_table_lua_table<'gc, 'a>(
ctx: Context<'gc>,
_global_memo: &'a GlobalMemoCell,
) -> Callback<'gc> {
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let match_table: UserData = stack.consume(ctx)?;
stack.push_back(
match_table
.downcast::<Rootable!['gcb => GcRefLock<'gcb, MatchSubTable>]>()?
.borrow_mut(&ctx)
.to_value(ctx)?,
);
Ok(piccolo::CallbackReturn::Return)
})
}
pub fn match_table_try_run_sub<'gc, 'a>(
ctx: Context<'gc>,
global_memo: &'a GlobalMemoCell,
) -> Callback<'gc> {
let global_memo = global_memo.clone();
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
let (match_table, sub, frame): (UserData, piccolo::String, Value) = stack.consume(ctx)?;
let frame = try_unwrap_frame(ctx, &frame)?;
let cmds = match_table
.downcast::<Rootable!['gcb => GcRefLock<'gcb, MatchSubTable>]>()?
.borrow()
.try_sub(sub.to_str()?);
match cmds {
None => stack.push_back(false.into_value(ctx)),
Some(cmds) => {
let mut cq = global_memo.command_queue.borrow_mut();
for cmd in cmds.into_iter().rev() {
cq.push_front((frame.clone(), cmd));
}
stack.push_back(Value::Boolean(true))
}
}
Ok(piccolo::CallbackReturn::Return)
})
} }

View File

@ -65,6 +65,7 @@ pub fn connect_websocket(
if data.has_type::<ArrayBuffer>() { if data.has_type::<ArrayBuffer>() {
handle_websocket_output_log_err( handle_websocket_output_log_err(
&data_new_id, &data_new_id,
&data_globals,
&mut data_globals.lua_engine.borrow_mut(), &mut data_globals.lua_engine.borrow_mut(),
&Uint8Array::new(&data).to_vec(), &Uint8Array::new(&data).to_vec(),
); );
@ -100,6 +101,7 @@ pub fn connect_websocket(
closed_socket.retained_closures = None; closed_socket.retained_closures = None;
handle_websocket_close_log_err( handle_websocket_close_log_err(
&close_id, &close_id,
&close_globals,
&mut close_globals.lua_engine.borrow_mut(), &mut close_globals.lua_engine.borrow_mut(),
); );
} }