use std::collections::HashSet;
use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::compiler::{ext, CompileError, Compiler};
use crate::parser::{Part, Script, Word};
use crate::runtime::to_tcl_string;
#[derive(Debug)]
pub(crate) enum Request {
Create {
name: String,
command: String,
args: Vec<Value>,
},
Resume { name: String, args: Vec<Value> },
Yield(Value),
YieldTo { name: String, args: Vec<Value> },
}
pub(crate) fn is_op(id: u16) -> bool {
(ext::CORO_CREATE..=ext::CORO_INFO).contains(&id)
}
pub(crate) fn extension(vm: &mut VM, id: u16, arg: u8, current: Option<&str>) -> Option<Request> {
match id {
ext::CORO_INFO => {
let name = match current {
Some(name) => format!("::{name}"),
None => String::new(),
};
vm.push(Value::Str(Arc::new(name)));
None
}
ext::CORO_CREATE => {
let command = to_tcl_string(&vm.pop());
let name = to_tcl_string(&vm.pop());
Some(Request::Create {
name,
command,
args: pop_args(vm, arg),
})
}
ext::CORO_RESUME => {
let name = to_tcl_string(&vm.pop());
Some(Request::Resume {
name,
args: pop_args(vm, arg),
})
}
ext::CORO_YIELD => Some(Request::Yield(vm.pop())),
_ => {
let args = pop_args(vm, arg);
Some(Request::YieldTo {
name: to_tcl_string(&vm.pop()),
args,
})
}
}
}
fn pop_args(vm: &mut VM, count: u8) -> Vec<Value> {
let mut args = Vec::with_capacity(count as usize);
for _ in 0..count {
args.push(vm.pop());
}
args.reverse();
args
}
pub fn prescan(coros: &mut HashSet<String>, script: &Script) {
for cmd in &script.commands {
if cmd.words.first().and_then(Word::as_literal) == Some("coroutine") {
if let Some(name) = cmd.words.get(1).and_then(Word::as_literal) {
coros.insert(name.to_string());
}
}
for word in &cmd.words {
for part in &word.parts {
if let Part::Script(nested) = part {
prescan(coros, nested);
}
}
}
}
}
impl Compiler {
pub(crate) fn cmd_coroutine(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [name_w, command_w, actuals @ ..] = args else {
return self.error("wrong # args: should be \"coroutine name cmd ?arg ...?\"");
};
if !self.static_ctx {
return self.error(
"\"coroutine\" is only supported at the top level of a script, or in a command \
substitution in one",
);
}
let name = self.literal_of(name_w, "coroutine name")?.to_string();
let command = self.literal_of(command_w, "coroutine command")?.to_string();
if Compiler::BUILTINS.contains(&name.as_str()) {
return self.error(format!(
"redefining the built-in command \"{name}\" is not supported"
));
}
if self.procs.contains_key(&name) {
return self.error(format!(
"coroutine \"{name}\" collides with a procedure of the same name, which is not \
supported"
));
}
if Compiler::BUILTINS.contains(&command.as_str()) {
return self.error(format!(
"a coroutine of the built-in command \"{command}\" is not supported; its body \
must be a procedure this script defines"
));
}
if !self.procs.contains_key(&command) {
return self.error(format!("invalid command name \"{command}\""));
}
let slots = self.push_actuals(&command, actuals)?;
let count = u8::try_from(slots).map_err(|_| {
self.err(format!(
"procedure \"{command}\" has more than 255 formal parameters"
))
})?;
self.push_str(&name);
self.push_str(&command);
self.emit(Op::Extended(ext::CORO_CREATE, count), -(slots as i32) - 1);
Ok(())
}
pub(crate) fn call_coro(&mut self, name: &str, args: &[Word]) -> Result<(), CompileError> {
let count = u8::try_from(args.len()).map_err(|_| {
self.err(format!(
"more than 255 arguments to the coroutine \"{name}\""
))
})?;
for w in args {
self.word(w)?;
}
self.push_str(name);
self.emit(Op::Extended(ext::CORO_RESUME, count), -(args.len() as i32));
Ok(())
}
pub(crate) fn cmd_yield(&mut self, args: &[Word]) -> Result<(), CompileError> {
match args {
[] => self.push_empty(),
[value] => self.word(value)?,
_ => return self.error("wrong # args: should be \"yield ?value?\""),
}
self.emit(Op::Extended(ext::CORO_YIELD, 0), 0);
Ok(())
}
pub(crate) fn cmd_yieldto(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [target_w, actuals @ ..] = args else {
return self.error("wrong # args: should be \"yieldto command ?arg ...?\"");
};
if let Some(target) = target_w.as_literal() {
if !self.coros.contains(target) {
return self.error(format!(
"\"yieldto {target}\": ceding control to a command that is not a coroutine \
of this script is not supported"
));
}
}
let count = u8::try_from(actuals.len())
.map_err(|_| self.err("more than 255 arguments to \"yieldto\"".to_string()))?;
self.word(target_w)?;
for w in actuals {
self.word(w)?;
}
self.emit(
Op::Extended(ext::CORO_YIELDTO, count),
-(actuals.len() as i32),
);
Ok(())
}
pub(crate) fn info_coroutine(&mut self, args: &[Word]) -> Result<(), CompileError> {
if !args.is_empty() {
return self.error("wrong # args: should be \"info coroutine\"");
}
self.emit(Op::Extended(ext::CORO_INFO, 0), 1);
Ok(())
}
}