use std::collections::HashMap;
use std::sync::Arc;
use fusevm::{Frame, Op, Value, VM};
use crate::compiler::{ext, literal_value, CompileError, Compiler, Scope};
use crate::list;
use crate::parser::{Script, Word};
use crate::runtime::{to_tcl_string, Shared, TclError};
#[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,
pub body: Option<String>,
}
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,
body: None,
})
}
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(mut sig) = parse_signature(name, spec) {
sig.body = body.as_literal().map(str::to_string);
procs.insert(name.to_string(), sig);
}
}
}
#[derive(Clone)]
pub(crate) struct RuntimeProc {
chunk: Arc<fusevm::Chunk>,
entry: usize,
sig: Signature,
}
type ChunkKey = (u64, usize);
fn chunk_key(chunk: &fusevm::Chunk) -> ChunkKey {
(chunk.op_hash, chunk.ops.len())
}
fn table_key(name: &str) -> &str {
crate::cmd_namespace::store_key(name)
}
pub(crate) fn define_op(interp: &Shared, vm: &mut VM) -> Result<(), TclError> {
let entry = vm.pop();
let spec = to_tcl_string(&vm.pop());
let name = to_tcl_string(&vm.pop());
let entry = match entry {
Value::Int(n) if n >= 0 && (n as usize) < vm.chunk.ops.len() => n as usize,
other => {
return Err(TclError::plain(format!(
"procedure \"{name}\" has no body at {}",
to_tcl_string(&other)
)))
}
};
let sig = parse_signature(&name, &spec).map_err(TclError::plain)?;
let mut state = interp.lock().expect("interpreter lock");
let chunk = running_chunk(&state, vm);
let defined = RuntimeProc { chunk, entry, sig };
state.commands.insert(table_key(&name).to_string(), defined);
drop(state);
vm.push(Value::Str(std::sync::Arc::new(String::new())));
Ok(())
}
fn running_chunk(state: &crate::runtime::State, vm: &VM) -> Arc<fusevm::Chunk> {
match state.running.last() {
Some(chunk) if chunk_key(chunk) == chunk_key(&vm.chunk) => Arc::clone(chunk),
_ => Arc::new(vm.chunk.clone()),
}
}
pub(crate) fn call_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
let line = match values.first() {
Some(Value::Int(n)) => *n as usize,
_ => 0,
};
let name = to_tcl_string(&values[1]);
invoke(interp, vm, &name, &values[2..], line)
}
fn invoke(
interp: &Shared,
vm: &mut VM,
name: &str,
args: &[Value],
line: usize,
) -> Result<(), TclError> {
let defined = defined_proc(interp, name);
dispatch(interp, vm, name, args, line, defined)
}
fn defined_proc(interp: &Shared, name: &str) -> Option<RuntimeProc> {
let state = interp.lock().expect("interpreter lock");
state.commands.get(table_key(name)).cloned()
}
fn located(vm: &VM, line: usize) -> Option<usize> {
(vm.frames.len() <= 1).then_some(line)
}
fn dispatch(
interp: &Shared,
vm: &mut VM,
name: &str,
args: &[Value],
line: usize,
defined: Option<RuntimeProc>,
) -> Result<(), TclError> {
let at = located(vm, line);
let here = move |msg: String| TclError {
msg,
line: at,
code: crate::runtime::TCL_ERROR,
level: 0,
};
match defined {
Some(p) if chunk_key(&p.chunk) == chunk_key(&vm.chunk) => {
enter(vm, name, &p, args).map_err(here)
}
Some(p) => enter_elsewhere(interp, vm, name, &p, args).map_err(|e| match e.line {
Some(_) => e,
None => here(e.msg),
}),
None => foreign(interp, vm, name, args).map_err(here),
}
}
fn enter(vm: &mut VM, name: &str, p: &RuntimeProc, args: &[Value]) -> Result<(), String> {
let actuals = actuals(name, &p.sig, args)?;
let base = vm.stack.len();
for value in actuals {
vm.push(value);
}
vm.frames.push(Frame {
return_ip: vm.ip,
stack_base: base,
slots: Vec::new(),
entry_ip: Some(p.entry),
});
vm.ip = p.entry;
Ok(())
}
fn actuals(name: &str, sig: &Signature, args: &[Value]) -> Result<Vec<Value>, String> {
let fixed = sig.fixed();
if args.len() < sig.required || (!sig.variadic && args.len() > fixed) {
return Err(format!("wrong # args: should be \"{}\"", sig.usage(name)));
}
let mut out = Vec::with_capacity(sig.params.len());
for i in 0..fixed {
out.push(match args.get(i) {
Some(v) => v.clone(),
None => {
let default = sig.params[i]
.default
.as_deref()
.expect("defaulted parameter");
literal_value(default)
}
});
}
if sig.variadic {
let rest: Vec<String> = args[fixed.min(args.len())..]
.iter()
.map(to_tcl_string)
.collect();
out.push(Value::Str(Arc::new(list::join(&rest))));
}
Ok(out)
}
fn enter_elsewhere(
interp: &Shared,
vm: &mut VM,
name: &str,
p: &RuntimeProc,
args: &[Value],
) -> Result<(), TclError> {
let actuals = actuals(name, &p.sig, args).map_err(TclError::plain)?;
let chunk = Arc::clone(&p.chunk);
let entry = p.entry;
let value = crate::runtime::with_written_back(interp, vm, |interp| {
crate::runtime::call_in_chunk(interp, &chunk, entry, actuals)
})?;
vm.push(value);
Ok(())
}
fn foreign(interp: &Shared, vm: &mut VM, name: &str, args: &[Value]) -> Result<(), String> {
#[cfg(feature = "tk")]
{
crate::runtime::sync_out(interp, vm)?;
crate::runtime::flush_globals(vm, interp);
let outcome = crate::tk::dispatch::invoke(name, args);
crate::runtime::reseed_globals(vm, interp);
crate::runtime::sync_in(interp, vm);
vm.push(Value::Str(std::sync::Arc::new(outcome?)));
Ok(())
}
#[cfg(not(feature = "tk"))]
{
let _ = (interp, vm, args);
Err(format!("invalid command name \"{name}\""))
}
}
pub(crate) fn expand_call_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
let line = match values.first() {
Some(Value::Int(n)) => *n as usize,
_ => 0,
};
let at = located(vm, line);
let here = move |msg: String| TclError {
msg,
line: at,
code: crate::runtime::TCL_ERROR,
level: 0,
};
let words = splice(&values[1..]).map_err(here)?;
let Some((first, args)) = words.split_first() else {
vm.push(Value::Str(Arc::new(String::new())));
return Ok(());
};
let name = to_tcl_string(first);
let defined = defined_proc(interp, &name);
if defined.is_none() && crate::names::is_command(&name) {
return as_script(interp, vm, &words).map_err(|e| here(e.msg));
}
dispatch(interp, vm, &name, args, line, defined)
}
fn splice(pairs: &[Value]) -> Result<Vec<Value>, String> {
let mut out = Vec::with_capacity(pairs.len() / 2);
for pair in pairs.chunks(2) {
let [flag, value] = pair else {
return Err("malformed expanded call".to_string());
};
if matches!(flag, Value::Int(0)) {
out.push(value.clone());
continue;
}
for element in list::split(&to_tcl_string(value))? {
out.push(Value::Str(Arc::new(element)));
}
}
Ok(out)
}
fn as_script(interp: &Shared, vm: &mut VM, words: &[Value]) -> Result<(), TclError> {
let text: Vec<String> = words.iter().map(to_tcl_string).collect();
let src = list::join(&text);
let value = crate::runtime::with_written_back(interp, vm, |interp| {
crate::runtime::run_source(interp, &src)
})?;
vm.push(value);
Ok(())
}
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\"");
};
let at_top = self.top_level;
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 mut sig = match parse_signature(&name, &spec) {
Ok(sig) => sig,
Err(msg) => return self.error(msg),
};
sig.body = body_w.as_literal().map(str::to_string);
if !at_top {
self.seen_runtime.insert(name.clone());
} else 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();
if at_top {
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?;
let after = self.b.current_pos();
self.b.patch_jump(skip, after);
if let Some(scope) = body_scope {
self.b.set_sub_slot_names(entry, slot_names_of(&scope));
self.publish_slot_names(&scope, entry, after);
}
if at_top {
let name_idx = self.b.add_name(&name);
self.b.add_sub_entry(name_idx, entry);
}
self.push_str(&name);
self.push_str(&spec);
self.push_value(Value::Int(entry as i64));
self.emit(Op::Extended(ext::PROC_DEFINE, 3), -2);
Ok(())
}
pub(crate) fn call_runtime(&mut self, name: &str, args: &[Word]) -> Result<(), CompileError> {
let count = u8::try_from(args.len() + 2)
.map_err(|_| self.err(format!("more than 253 arguments to the command \"{name}\"")))?;
self.push_value(Value::Int(self.command_line as i64));
self.push_str(name);
for arg in args {
self.word(arg)?;
}
self.emit(Op::Extended(ext::DYN_CALL, count), 1 - count as i32);
Ok(())
}
pub(crate) fn call_expanded(&mut self, words: &[Word]) -> Result<(), CompileError> {
let count = u8::try_from(1 + 2 * words.len()).map_err(|_| {
self.err("more than 126 words in a command with {*} argument expansion".to_string())
})?;
self.push_value(Value::Int(self.command_line as i64));
for w in words {
self.emit(Op::LoadInt(i64::from(w.expand)), 1);
self.word_value(w)?;
}
self.emit(Op::Extended(ext::EXPAND_CALL, count), 1 - count as i32);
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> {
let mut rest = args;
let mut code = crate::runtime::TCL_OK;
let mut level = 1;
while let [first, value, tail @ ..] = rest {
match first.as_literal() {
Some("-code") => {
let text = self.literal_of(value, "return code")?.to_string();
code = match text.as_str() {
"ok" => crate::runtime::TCL_OK,
"error" => crate::runtime::TCL_ERROR,
"return" => crate::runtime::TCL_RETURN,
"break" => crate::runtime::TCL_BREAK,
"continue" => crate::runtime::TCL_CONTINUE,
n => match n.parse() {
Ok(n) => n,
Err(_) => {
return self.error(format!(
"bad completion code \"{n}\": must be ok, error, return, break, \
continue, or an integer"
))
}
},
};
}
Some("-level") => {
let text = self.literal_of(value, "return level")?.to_string();
level = match text.parse::<i32>() {
Ok(n) if n >= 0 => n,
_ => {
return self.error(format!(
"bad -level value: expected non-negative integer \
but got \"{text}\""
))
}
};
}
Some(other) if other.starts_with('-') => {
return self.error(format!("return option \"{other}\" is not supported"))
}
_ => break,
}
rest = tail;
}
let result = match rest {
[] => None,
[v] => Some(v),
_ => {
return self.error(
"wrong # args: should be \"return ?-code code? ?-level level? ?result?\"",
)
}
};
let plain = level == 1 && code == crate::runtime::TCL_OK && self.catch_depth == 0;
if plain && self.scope.is_some() {
match result {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
self.emit(Op::ReturnValue, -1);
self.push_empty();
return Ok(());
}
if plain && self.scope.is_none() {
match result {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
self.emit(Op::ReturnValue, -1);
self.push_empty();
return Ok(());
}
match result {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
self.emit(Op::LoadInt(i64::from(code)), 1);
self.emit(Op::LoadInt(i64::from(level)), 1);
self.emit(Op::Extended(ext::RAISE, 0), -3);
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
}
pub(crate) 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
}