use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::assoc::{element_map, place_of};
use crate::compiler::{CompileError, Compiler, Place};
use crate::parser::Word;
use crate::runtime::to_tcl_string;
pub mod ext {
pub use crate::compiler::ext::INFO_BASE as BASE;
pub const EXISTS: u16 = BASE;
pub const COMPLETE: u16 = BASE + 1;
pub const NAMES: u16 = BASE + 2;
pub const ARGS: u16 = BASE + 3;
pub const DEFAULT: u16 = BASE + 4;
pub const ABOUT: u16 = BASE + 5;
pub const SET_SCRIPT: u16 = BASE + 6;
pub const LEVEL: u16 = BASE + 7;
pub const BODY: u16 = BASE + 8;
pub const FUNCTIONS: u16 = BASE + 9;
}
pub(crate) const COMMANDS: u8 = 0;
pub(crate) const PROCS: u8 = 1;
pub(crate) const GLOBALS: u8 = 2;
pub(crate) const VARS: u8 = 3;
pub(crate) const SET_OF: u8 = 4;
pub(crate) const FRAME_LOCALS: u8 = 5;
pub(crate) const ALWAYS: i64 = i64::MIN;
const SCRIPT: u8 = 0;
const EXECUTABLE: u8 = 1;
const HOSTNAME: u8 = 2;
const LIBRARY: u8 = 3;
const SHAREDLIBEXTENSION: u8 = 4;
const TCL_VERSION: &str = "9.0";
const TCL_PATCHLEVEL: &str = "9.0.4";
pub(crate) const SUBCOMMANDS: &[&str] = &[
"args",
"body",
"class",
"cmdcount",
"cmdtype",
"commands",
"complete",
"constant",
"consts",
"coroutine",
"default",
"errorstack",
"exists",
"frame",
"functions",
"globals",
"hostname",
"level",
"library",
"loaded",
"locals",
"nameofexecutable",
"object",
"patchlevel",
"procs",
"script",
"sharedlibextension",
"tclversion",
"vars",
];
impl Compiler {
pub(crate) fn cmd_info(&mut self, args: &[Word]) -> Result<(), CompileError> {
let Some((head, rest)) = args.split_first() else {
return self.error("wrong # args: should be \"info subcommand ?arg ...?\"");
};
let Some(sub) = head.as_literal() else {
return Err(self.deferrable_err("the subcommand of \"info\" must be a literal in this phase"));
};
let sub = resolve(sub).map_err(|msg| self.err(msg))?;
match sub {
"coroutine" => self.info_coroutine(rest),
"exists" => self.info_exists(rest),
"complete" => self.info_one(rest, ext::COMPLETE, "info complete command"),
"args" => self.info_one(rest, ext::ARGS, "info args procname"),
"body" => self.info_one(rest, ext::BODY, "info body procname"),
"default" => self.info_default(rest),
"commands" => self.info_names(rest, COMMANDS, "info commands ?pattern?"),
"procs" => self.info_names(rest, PROCS, "info procs ?pattern?"),
"globals" => self.info_names(rest, GLOBALS, "info globals ?pattern?"),
"vars" if self.scope.is_some() => {
let visible = self.visible_names();
self.info_candidates(rest, visible, "info vars ?pattern?")
}
"vars" => self.info_names(rest, VARS, "info vars ?pattern?"),
"locals" if self.scope.is_some() => {
let locals = self.local_names();
self.info_candidates(rest, locals, "info locals ?pattern?")
}
"locals" => self.info_names(rest, FRAME_LOCALS, "info locals ?pattern?"),
"level" => self.info_level(rest),
"functions" => self.info_about_list(rest, ext::FUNCTIONS, "info functions ?pattern?"),
"tclversion" => self.info_literal(rest, TCL_VERSION, "info tclversion"),
"patchlevel" => self.info_literal(rest, TCL_PATCHLEVEL, "info patchlevel"),
"script" => match rest {
[] => self.info_about(rest, SCRIPT, "info script ?filename?"),
[name] => {
self.word(name)?;
self.emit(Op::Extended(ext::SET_SCRIPT, 0), 0);
Ok(())
}
_ => self.error("wrong # args: should be \"info script ?filename?\""),
},
"nameofexecutable" => self.info_about(rest, EXECUTABLE, "info nameofexecutable"),
"hostname" => self.info_about(rest, HOSTNAME, "info hostname"),
"library" => self.info_about(rest, LIBRARY, "info library"),
"sharedlibextension" => {
self.info_about(rest, SHAREDLIBEXTENSION, "info sharedlibextension")
}
other => self.error(format!(
"info {other} is not supported yet: {}",
why_refused(other)
)),
}
}
fn info_exists(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [target] = args else {
return self.error("wrong # args: should be \"info exists varName\"");
};
let Some(target) = crate::assoc::target_of(target) else {
return self.dyn_exists(target);
};
match target {
crate::assoc::Target::Scalar(name) => {
let place = self.var_place_operand(&name);
self.emit(Op::LoadInt(place), 1);
self.emit(Op::Extended(ext::EXISTS, 0), 0);
}
crate::assoc::Target::Elem { name, index } => {
let place = self.var_place_operand(&name);
self.emit(Op::LoadInt(place), 1);
self.index_value(&index)?;
self.emit(Op::Extended(ext::EXISTS, 1), -1);
}
}
Ok(())
}
fn info_one(&mut self, args: &[Word], id: u16, usage: &str) -> Result<(), CompileError> {
let [only] = args else {
return self.error(format!("wrong # args: should be \"{usage}\""));
};
self.word(only)?;
self.emit(Op::Extended(id, 0), 0);
Ok(())
}
fn info_default(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [proc_w, arg_w, var_w] = args else {
return self.error("wrong # args: should be \"info default procname arg varname\"");
};
let target = self.target_of(var_w)?;
self.word(proc_w)?;
self.word(arg_w)?;
match target {
crate::assoc::Target::Scalar(name) => {
let place = self.var_place_operand(&name);
self.emit(Op::LoadInt(place), 1);
self.emit(Op::Extended(ext::DEFAULT, 0), -2);
}
crate::assoc::Target::Elem { name, index } => {
self.index_value(&index)?;
let place = self.var_place_operand(&name);
self.emit(Op::LoadInt(place), 1);
self.emit(Op::Extended(ext::DEFAULT, 1), -3);
}
}
Ok(())
}
fn info_level(&mut self, args: &[Word]) -> Result<(), CompileError> {
if !args.is_empty() {
return self.error(
"\"info level\" with a level number is not supported: no record of the command \
that entered a level is kept",
);
}
self.emit(Op::Extended(ext::LEVEL, 0), 1);
Ok(())
}
fn info_about_list(&mut self, args: &[Word], id: u16, usage: &str) -> Result<(), CompileError> {
match args {
[] => self.push_str("*"),
[pattern] => self.word(pattern)?,
_ => return self.error(format!("wrong # args: should be \"{usage}\"")),
}
self.emit(Op::Extended(id, 0), 0);
Ok(())
}
fn local_names(&self) -> Vec<String> {
let Some(scope) = self.scope.as_ref() else {
return Vec::new();
};
let mut names: Vec<String> = scope
.locals
.keys()
.filter(|n| !n.starts_with('\u{0}'))
.cloned()
.collect();
names.sort();
names
}
fn visible_names(&self) -> Vec<String> {
let Some(scope) = self.scope.as_ref() else {
return Vec::new();
};
let mut names = self.local_names();
names.extend(scope.globals.iter().cloned());
names.extend(scope.aliases.keys().cloned());
names.extend(scope.links.keys().cloned());
names.sort();
names.dedup();
names
}
fn info_candidates(
&mut self,
args: &[Word],
candidates: Vec<String>,
usage: &str,
) -> Result<(), CompileError> {
let declared = self.declared_names();
let places: Vec<String> = candidates
.iter()
.map(|name| {
if declared.contains(name) {
ALWAYS.to_string()
} else {
self.var_place(name).encode().to_string()
}
})
.collect();
self.push_str(&crate::list::join(&candidates));
self.push_str(&crate::list::join(&places));
match args {
[] => self.push_str("*"),
[pattern] => self.word(pattern)?,
_ => return self.error(format!("wrong # args: should be \"{usage}\"")),
}
self.emit(Op::Extended(ext::NAMES, SET_OF), -2);
Ok(())
}
fn declared_names(&self) -> std::collections::HashSet<String> {
let Some(scope) = self.scope.as_ref() else {
return std::collections::HashSet::new();
};
scope
.globals
.iter()
.cloned()
.chain(scope.aliases.keys().cloned())
.chain(scope.links.keys().cloned())
.collect()
}
fn info_names(&mut self, args: &[Word], which: u8, usage: &str) -> Result<(), CompileError> {
match args {
[] => {
self.push_empty();
self.emit(Op::LoadInt(0), 1);
}
[pattern] => {
self.word(pattern)?;
self.emit(Op::LoadInt(1), 1);
}
_ => return self.error(format!("wrong # args: should be \"{usage}\"")),
}
self.emit(Op::Extended(ext::NAMES, which), -1);
Ok(())
}
fn info_literal(&mut self, args: &[Word], text: &str, usage: &str) -> Result<(), CompileError> {
if !args.is_empty() {
return self.error(format!("wrong # args: should be \"{usage}\""));
}
self.push_str(text);
Ok(())
}
fn info_about(&mut self, args: &[Word], which: u8, usage: &str) -> Result<(), CompileError> {
if !args.is_empty() {
return self.error(format!("wrong # args: should be \"{usage}\""));
}
self.emit(Op::Extended(ext::ABOUT, which), 1);
Ok(())
}
}
fn resolve(given: &str) -> Result<&'static str, String> {
if let Some(exact) = SUBCOMMANDS.iter().find(|s| **s == given) {
return Ok(exact);
}
let hits: Vec<&&str> = SUBCOMMANDS
.iter()
.filter(|s| s.starts_with(given))
.collect();
match hits.as_slice() {
[one] => Ok(**one),
_ => Err(format!(
"unknown or ambiguous subcommand \"{given}\": must be {}, or {}",
SUBCOMMANDS[..SUBCOMMANDS.len() - 1].join(", "),
SUBCOMMANDS[SUBCOMMANDS.len() - 1]
)),
}
}
fn why_refused(sub: &str) -> &'static str {
match sub {
"frame" => {
"it reports on the stack of *commands*, and only the stack of call frames is kept"
}
"class" | "object" => "TclOO is not implemented",
"constant" | "consts" => "constant variables are not implemented",
"loaded" => "loadable extensions are not implemented",
"cmdcount" | "cmdtype" | "errorstack" => "the interpreter does not keep it",
_ => "not built yet",
}
}
pub(crate) fn extension(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::EXISTS => {
let index = if arg == 1 {
Some(to_tcl_string(&vm.pop()))
} else {
None
};
let operand = vm.pop();
let place = place_of_operand(&operand)?;
let set = is_set(vm, place, index.as_deref());
vm.push(Value::Int(i64::from(set)));
Ok(())
}
ext::COMPLETE => {
let text = to_tcl_string(&vm.pop());
vm.push(Value::Int(i64::from(is_complete(&text))));
Ok(())
}
ext::NAMES => Err("info names op reached the module handler".to_string()),
ext::ARGS => {
let name = to_tcl_string(&vm.pop());
let params = crate::runtime::proc_params(vm, &name)
.ok_or_else(|| format!("\"{name}\" isn't a procedure"))?;
let names: Vec<String> = params.params.into_iter().map(|(n, _)| n).collect();
vm.push(Value::Str(Arc::new(crate::list::join(&names))));
Ok(())
}
ext::BODY => {
let name = to_tcl_string(&vm.pop());
let body = crate::runtime::proc_body(vm, &name)
.ok_or_else(|| format!("\"{name}\" isn't a procedure"))?;
vm.push(Value::Str(Arc::new(body)));
Ok(())
}
ext::LEVEL => {
vm.push(Value::Int(crate::runtime::current_level(vm)));
Ok(())
}
ext::FUNCTIONS => {
let pattern = to_tcl_string(&vm.pop());
let mut names: Vec<String> = crate::expr_math::names()
.iter()
.filter(|name| crate::list::glob_match(&pattern, name))
.map(|name| (*name).to_string())
.collect();
names.sort();
vm.push(Value::Str(Arc::new(crate::list::join(&names))));
Ok(())
}
ext::DEFAULT => {
let place = place_of(vm);
let index = (arg == 1).then(|| to_tcl_string(&vm.pop()));
let arg_name = to_tcl_string(&vm.pop());
let proc_name = to_tcl_string(&vm.pop());
let params = crate::runtime::proc_params(vm, &proc_name)
.ok_or_else(|| format!("\"{proc_name}\" isn't a procedure"))?;
let found = params
.params
.into_iter()
.find(|(n, _)| *n == arg_name)
.ok_or_else(|| {
format!("procedure \"{proc_name}\" doesn't have an argument \"{arg_name}\"")
})?;
let (has, text) = match found.1 {
Some(d) => (1, d),
None => (0, String::new()),
};
let written = Value::Str(Arc::new(text));
match index {
Some(index) => {
element_map(vm, place)
.ok_or_else(|| format!("can't set \"{index}\": variable isn't array"))?
.insert(index, written);
}
None => {
if let Some(cell) = crate::runtime::var_cell(vm, place) {
*cell = written;
}
}
}
vm.push(Value::Int(has));
Ok(())
}
ext::SET_SCRIPT => {
let name = to_tcl_string(&vm.pop());
crate::runtime::note_script(&name);
vm.push(Value::Str(Arc::new(name)));
Ok(())
}
ext::ABOUT => {
let text = describe(arg)?;
vm.push(Value::Str(Arc::new(text)));
Ok(())
}
other => Err(format!("unknown info op {other}")),
}
}
fn place_of_operand(operand: &Value) -> Result<Place, String> {
match operand {
Value::Int(raw) => Ok(Place::decode(*raw)),
other => Err(format!("not a variable place: {other:?}")),
}
}
fn is_set(vm: &VM, place: Place, index: Option<&str>) -> bool {
let Some(index) = index else {
return crate::runtime::var_is_set(vm, place);
};
crate::assoc::element_is_set(vm, place, index)
}
fn is_complete(text: &str) -> bool {
match crate::parser::parse(text) {
Ok(_) => true,
Err(e) => !unterminated(&e.msg),
}
}
fn unterminated(msg: &str) -> bool {
msg.starts_with("missing close-brace")
|| msg.starts_with("missing close-bracket")
|| msg.starts_with("missing \"")
}
fn describe(which: u8) -> Result<String, String> {
Ok(match which {
SCRIPT => crate::runtime::current_script(),
EXECUTABLE => std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_default(),
HOSTNAME => hostname(),
SHAREDLIBEXTENSION => {
if cfg!(target_os = "macos") {
".dylib".to_string()
} else if cfg!(target_os = "windows") {
".dll".to_string()
} else {
".so".to_string()
}
}
_ => return Err("no library has been specified for Tcl".to_string()),
})
}
fn hostname() -> String {
let mut buf = [0 as libc::c_char; 256];
let rc = unsafe { libc::gethostname(buf.as_mut_ptr(), buf.len() - 1) };
if rc != 0 {
return String::new();
}
let bytes: Vec<u8> = buf
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}