use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::compiler::{ext, CompileError, Compiler};
use crate::parser::{SubstFlags, SubstParse, SubstPart, Word};
use crate::runtime::{
in_frame, levels, read_global, run_source, to_tcl_string, Shared, TclError, TCL_BREAK,
TCL_CONTINUE, TCL_ERROR,
};
pub(crate) fn compile(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let count = u8::try_from(args.len() + 2)
.map_err(|_| c.err("too many arguments for \"subst\"".to_string()))?;
let declared = c.declared_globals().unwrap_or_default();
c.push_str(&declared);
let here = c.ns.current.clone();
c.push_str(&here);
for arg in args {
c.word(arg)?;
}
c.emit(Op::Extended(ext::SUBST, count), 1 - count as i32);
Ok(())
}
const USAGE: &str = "wrong # args: should be \
\"subst ?-nobackslashes? ?-nocommands? ?-novariables? string\"";
fn options(words: &[String]) -> Result<(SubstFlags, &String), TclError> {
let Some((text, opts)) = words.split_last() else {
return Err(TclError::plain(USAGE));
};
let mut flags = SubstFlags::default();
for word in opts {
match resolve(word) {
Some("-nobackslashes") => flags.backslashes = false,
Some("-nocommands") => flags.commands = false,
Some("-novariables") => flags.variables = false,
_ => {
return Err(TclError::plain(format!(
"bad option \"{word}\": must be -nobackslashes, -nocommands, or -novariables"
)))
}
}
}
Ok((flags, text))
}
fn resolve(word: &str) -> Option<&'static str> {
const NAMES: [&str; 3] = ["-nobackslashes", "-nocommands", "-novariables"];
if word.is_empty() {
return None;
}
let hits: Vec<&'static str> = NAMES
.iter()
.copied()
.filter(|name| name.starts_with(word))
.collect();
match hits.as_slice() {
[only] => Some(only),
_ => None,
}
}
pub(crate) fn subst_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let mut args: Vec<String> = (0..argc).map(|_| to_tcl_string(&vm.pop())).collect();
args.reverse();
let declared = args.remove(0);
let here = args.remove(0);
let (flags, text) = options(&args)?;
let parse = crate::parser::subst_parts(text, flags);
let up = levels(vm).first().copied().unwrap_or(0);
let ns = Namespace::of(&here);
let value = in_frame(interp, vm, up, &declared, |interp| {
substitute(interp, &ns, &parse)
})?;
vm.push(Value::Str(Arc::new(value)));
Ok(())
}
struct Namespace {
prefix: Option<String>,
}
impl Namespace {
fn of(current: &str) -> Namespace {
let key = crate::cmd_namespace::store_key(current);
Namespace {
prefix: (!key.is_empty()).then(|| format!("{key}::")),
}
}
fn keys(&self, name: &str) -> Vec<String> {
let key = crate::cmd_namespace::store_key(name);
if name.starts_with("::") {
return vec![crate::cmd_namespace::chunk_key(name)];
}
if key.contains("::") {
return vec![key.to_string()];
}
match &self.prefix {
Some(prefix) => vec![format!("{prefix}{key}"), key.to_string()],
None => vec![key.to_string()],
}
}
}
fn substitute(interp: &Shared, ns: &Namespace, parse: &SubstParse) -> Result<String, TclError> {
let mut out = String::new();
for part in &parse.parts {
let guarded = match part {
SubstPart::Script(_) => true,
SubstPart::Elem { index, .. } => {
index.iter().any(|p| matches!(p, SubstPart::Script(_)))
}
_ => false,
};
let piece = one(interp, ns, part);
match piece {
Ok(text) => out.push_str(&text),
Err(e) if !guarded => return Err(e),
Err(e) => match e.visible_code() {
TCL_ERROR => return Err(e),
TCL_BREAK => return Ok(out),
TCL_CONTINUE => {}
_ => out.push_str(&e.msg),
},
}
}
match &parse.error {
Some(e) => Err(TclError::plain(e.msg.clone())),
None => Ok(out),
}
}
fn one(interp: &Shared, ns: &Namespace, part: &SubstPart) -> Result<String, TclError> {
match part {
SubstPart::Lit(text) => Ok(text.clone()),
SubstPart::Var(name) => read(interp, ns, name, None),
SubstPart::Elem { name, index } => {
let index = eval_parts(interp, ns, index)?;
read(interp, ns, name, Some(&index))
}
SubstPart::Script(script) => run(interp, script),
}
}
fn eval_parts(interp: &Shared, ns: &Namespace, parts: &[SubstPart]) -> Result<String, TclError> {
let mut out = String::new();
for part in parts {
out.push_str(&one(interp, ns, part)?);
}
Ok(out)
}
fn run(interp: &Shared, src: &str) -> Result<String, TclError> {
run_source(interp, src).map(|v| to_tcl_string(&v))
}
fn read(
interp: &Shared,
ns: &Namespace,
name: &str,
index: Option<&str>,
) -> Result<String, TclError> {
let spelled = match index {
Some(i) => format!("{name}({i})"),
None => name.to_string(),
};
let value = ns
.keys(name)
.into_iter()
.find_map(|key| read_global(interp, &key));
match (value, index) {
(None, _) | (Some(Value::Undef), _) => Err(TclError::plain(format!(
"can't read \"{spelled}\": no such variable"
))),
(Some(Value::Hash(map)), Some(index)) => match map.get(index) {
Some(v) => Ok(to_tcl_string(v)),
None => Err(TclError::plain(format!(
"can't read \"{spelled}\": no such element in array"
))),
},
(Some(_), Some(_)) => Err(TclError::plain(format!(
"can't read \"{spelled}\": variable isn't array"
))),
(Some(Value::Hash(_)), None) => Err(TclError::plain(format!(
"can't read \"{spelled}\": variable is array"
))),
(Some(v), None) => Ok(to_tcl_string(&v)),
}
}