use std::collections::HashMap;
use fusevm::Op;
use crate::compiler::{ext, CompileError, Compiler, Scope};
use crate::list;
use crate::parser::{Script, Word};
#[derive(Clone, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub default: Option<String>,
}
#[derive(Clone, PartialEq, Eq)]
pub struct Signature {
pub params: Vec<Param>,
pub variadic: bool,
pub required: usize,
}
impl Signature {
pub fn fixed(&self) -> usize {
self.params.len() - usize::from(self.variadic)
}
pub fn usage(&self, name: &str) -> String {
let mut out = name.to_string();
for (i, p) in self.params.iter().enumerate() {
out.push(' ');
if self.variadic && i + 1 == self.params.len() {
out.push_str("?arg ...?");
} else if p.default.is_some() {
out.push('?');
out.push_str(&p.name);
out.push('?');
} else {
out.push_str(&p.name);
}
}
out
}
}
pub fn parse_signature(proc_name: &str, spec: &str) -> Result<Signature, String> {
let mut params: Vec<Param> = Vec::new();
for element in list::split(spec)? {
let fields = list::split(&element)?;
let param = match fields.as_slice() {
[name] => Param {
name: name.clone(),
default: None,
},
[name, default] => Param {
name: name.clone(),
default: Some(default.clone()),
},
[] => return Err("argument with no name".to_string()),
_ => {
return Err(format!(
"too many fields in argument specifier \"{element}\""
))
}
};
if param.name.is_empty() {
return Err("argument with no name".to_string());
}
if param.name.ends_with(')') && param.name.contains('(') {
return Err(format!(
"formal parameter \"{}\" is an array element",
param.name
));
}
if params.iter().any(|p| p.name == param.name) {
return Err(format!(
"procedure \"{proc_name}\" has argument \"{}\" defined twice",
param.name
));
}
params.push(param);
}
let variadic = params.last().is_some_and(|p| p.name == "args");
let fixed = params.len() - usize::from(variadic);
let required = params[..fixed]
.iter()
.rposition(|p| p.default.is_none())
.map_or(0, |i| i + 1);
Ok(Signature {
params,
variadic,
required,
})
}
pub fn prescan(procs: &mut HashMap<String, Signature>, script: &Script) {
for cmd in &script.commands {
let [head, name, spec, _body] = cmd.words.as_slice() else {
continue;
};
if head.as_literal() != Some("proc") {
continue;
}
let (Some(name), Some(spec)) = (name.as_literal(), spec.as_literal()) else {
continue;
};
if let Ok(sig) = parse_signature(name, spec) {
procs.insert(name.to_string(), sig);
}
}
}
impl Compiler {
pub(crate) fn cmd_proc(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [name_w, spec_w, body_w] = args else {
return self.error("wrong # args: should be \"proc name args body\"");
};
if !self.top_level {
return self.error("\"proc\" is only supported at the top level of a script");
}
let name = self.literal_of(name_w, "procedure name")?.to_string();
if Compiler::BUILTINS.contains(&name.as_str()) {
return self.error(format!(
"redefining the built-in command \"{name}\" is not supported"
));
}
if self.coros.contains(&name) {
return self.error(format!(
"procedure \"{name}\" collides with a coroutine of the same name, which is \
not supported"
));
}
let spec = self.literal_of(spec_w, "argument list")?.to_string();
let sig = match parse_signature(&name, &spec) {
Ok(sig) => sig,
Err(msg) => return self.error(msg),
};
if !self.defined.insert(name.clone()) {
return self.error(format!(
"procedure \"{name}\" is redefined, which is not supported"
));
}
let slots = u8::try_from(sig.params.len())
.map_err(|_| {
self.err(format!(
"procedure \"{name}\" has more than 255 formal parameters"
))
})?
.into();
self.procs.insert(name.clone(), sig.clone());
let body = self.body_of(body_w)?;
let skip = self.emit(Op::Jump(usize::MAX), 0);
let entry = self.b.current_pos();
let outer_depth = std::mem::replace(&mut self.depth, slots);
let outer_loops = std::mem::take(&mut self.loops);
let outer_catch = std::mem::replace(&mut self.catch_depth, 0);
let outer_scope = self.scope.replace(scope_for(&sig));
let outer_top = std::mem::replace(&mut self.top_level, false);
let outer_static = std::mem::replace(&mut self.static_ctx, false);
for slot in (0..slots).rev() {
self.emit(Op::SetSlot(slot as u16), -1);
}
let compiled = match &body {
crate::compiler::Body::Script(script) => self.script_value(script),
crate::compiler::Body::Deferred(msg) => {
let msg = msg.clone();
self.raise_at_run_time(&msg)
}
};
self.emit(Op::ReturnValue, -1);
self.depth = outer_depth;
self.loops = outer_loops;
self.catch_depth = outer_catch;
let body_scope = std::mem::replace(&mut self.scope, outer_scope);
self.top_level = outer_top;
self.static_ctx = outer_static;
compiled?;
if let Some(scope) = body_scope {
self.b.set_sub_slot_names(entry, slot_names_of(&scope));
}
let after = self.b.current_pos();
self.b.patch_jump(skip, after);
let name_idx = self.b.add_name(&name);
self.b.add_sub_entry(name_idx, entry);
self.push_empty();
Ok(())
}
pub(crate) fn call_proc(&mut self, name: &str, args: &[Word]) -> Result<(), CompileError> {
let slots = self.push_actuals(name, args)?;
let name_idx = self.b.add_name(name);
self.emit(Op::Call(name_idx, slots as u8), 1 - slots as i32);
Ok(())
}
pub(crate) fn push_actuals(
&mut self,
name: &str,
args: &[Word],
) -> Result<usize, CompileError> {
let sig = self.procs.get(name).cloned().expect("known procedure");
let fixed = sig.fixed();
if args.len() < sig.required || (!sig.variadic && args.len() > fixed) {
return self.error(format!("wrong # args: should be \"{}\"", sig.usage(name)));
}
for i in 0..fixed {
match args.get(i) {
Some(w) => self.word(w)?,
None => {
let default = sig.params[i].default.clone().expect("defaulted parameter");
self.push_text(&default);
}
}
}
if sig.variadic {
let extra = &args[fixed.min(args.len())..];
let count = u8::try_from(extra.len()).map_err(|_| {
self.err(format!(
"more than 255 arguments collected into \"args\" of \"{name}\""
))
})?;
for w in extra {
self.word(w)?;
}
self.emit(Op::Extended(ext::LIST, count), 1 - extra.len() as i32);
}
Ok(sig.params.len())
}
pub(crate) fn cmd_return(&mut self, args: &[Word]) -> Result<(), CompileError> {
if self.scope.is_none() {
return self.error("\"return\" outside of a procedure is not supported");
}
if self.catch_depth > 0 {
return self.error("\"return\" out of a \"catch\" script is not supported");
}
let mut rest = args;
let mut code = "ok";
if let [first, value, tail @ ..] = args {
if first.as_literal() == Some("-code") {
code = self.literal_of(value, "return code")?;
rest = tail;
}
}
if let Some(w) = rest.first() {
if w.as_literal().is_some_and(|t| t.starts_with('-')) && rest.len() > 1 {
return self.error(format!(
"return option \"{}\" is not supported",
w.as_literal().unwrap_or_default()
));
}
}
let result = match rest {
[] => None,
[v] => Some(v),
_ => return self.error("wrong # args: should be \"return ?-code code? ?result?\""),
};
match code {
"ok" | "0" => {
match result {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
self.emit(Op::ReturnValue, -1);
}
"error" | "1" => {
match result {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
self.emit(Op::Extended(ext::ERROR, 0), -1);
}
other => {
return self.error(format!(
"return -code \"{other}\" is not supported; only \"ok\" and \"error\" are"
))
}
}
self.push_empty();
Ok(())
}
pub(crate) fn cmd_global(&mut self, args: &[Word]) -> Result<(), CompileError> {
for w in args {
let name = self.var_name_of(w)?;
let Some(scope) = self.scope.as_mut() else {
continue;
};
if scope.locals.contains_key(&name) {
return self.error(format!("variable \"{name}\" already exists"));
}
scope.globals.insert(name);
}
self.push_empty();
Ok(())
}
}
fn slot_names_of(scope: &crate::compiler::Scope) -> Vec<String> {
let mut names = vec![String::new(); scope.next_slot as usize];
for (name, &slot) in &scope.locals {
if let Some(at) = names.get_mut(slot as usize) {
*at = name.clone();
}
}
names
}
fn scope_for(sig: &Signature) -> Scope {
let mut scope = Scope::default();
for (i, p) in sig.params.iter().enumerate() {
scope.locals.insert(p.name.clone(), i as u16);
}
scope.next_slot = sig.params.len() as u16;
scope
}