use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::compiler::{CompileError, Compiler, Place};
use crate::parser::Word;
use crate::assoc::{element_map, place_of};
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;
}
const COMMANDS: u8 = 0;
const PROCS: u8 = 1;
const GLOBALS: u8 = 2;
const VARS: u8 = 3;
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 self.error("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"),
"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" => self.info_names(rest, VARS, "info vars ?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\"");
};
match self.target_of(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_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 {
"body" => "a procedure's body is compiled into the enclosing chunk and its source text is not kept",
"locals" | "level" | "frame" => "it reports on the running call frame, which this frontend does not expose yet",
"class" | "object" => "TclOO is not implemented",
"constant" | "consts" => "constant variables are not implemented",
"functions" => "math functions 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.into_iter().map(|(n, _)| n).collect();
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
.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) if *raw < 0 => Ok(Place::Slot((-raw - 1) as u16)),
Value::Int(raw) => Ok(Place::Global(*raw as u16)),
other => Err(format!("not a variable place: {other:?}")),
}
}
fn is_set(vm: &VM, place: Place, index: Option<&str>) -> bool {
let held = match place {
Place::Global(idx) => vm.globals.get(idx as usize),
Place::Slot(slot) => vm.frames.last().and_then(|f| f.slots.get(slot as usize)),
};
match (held, index) {
(None | Some(Value::Undef), _) => false,
(Some(Value::Hash(map)), Some(key)) => map.contains_key(key),
(Some(_), Some(_)) => false,
(Some(_), None) => true,
}
}
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()
}