use std::collections::{BTreeMap, BTreeSet, HashMap};
use fusevm::{Op, Value};
use crate::compiler::{CompileError, Compiler};
use crate::parser::{Part, Script, Word};
use crate::runtime::{to_tcl_string, TclError};
pub mod ext {
use crate::compiler::ext::NS_BASE;
pub const NS: u16 = NS_BASE;
pub const RENAME_GUARD: u16 = NS_BASE + 1;
}
pub fn is_op(id: u16) -> bool {
id == ext::NS || id == ext::RENAME_GUARD
}
pub fn qualifiers(name: &str) -> &str {
let b = name.as_bytes();
let mut p = b.len();
while p > 0 {
p -= 1;
if b[p] == b':' && p > 0 && b[p - 1] == b':' {
if p < 2 {
return "";
}
p -= 2;
while b[p] == b':' {
if p == 0 {
return "";
}
p -= 1;
}
return &name[..p + 1];
}
}
""
}
pub fn tail(name: &str) -> &str {
let b = name.as_bytes();
let mut p = b.len();
while p > 1 {
p -= 1;
if b[p] == b':' && b[p - 1] == b':' {
return &name[p + 1..];
}
}
name
}
fn components(name: &str) -> (bool, Vec<&str>) {
let absolute = name.starts_with("::");
let mut parts = Vec::new();
let mut rest = name;
while !rest.is_empty() {
match rest.find("::") {
Some(0) => {
rest = rest.trim_start_matches(':');
}
Some(i) => {
parts.push(&rest[..i]);
rest = &rest[i..];
}
None => {
parts.push(rest);
break;
}
}
}
(absolute, parts)
}
pub fn resolve(current: &str, name: &str) -> String {
let (absolute, parts) = components(name);
let mut out = String::new();
if !absolute {
out.push_str(current.trim_end_matches(':'));
}
for part in parts {
out.push_str("::");
out.push_str(part);
}
if out.is_empty() {
out.push_str("::");
}
out
}
pub fn parent_of(fqn: &str) -> String {
if fqn == "::" {
return String::new();
}
let q = qualifiers(fqn);
if q.is_empty() {
"::".to_string()
} else {
q.to_string()
}
}
pub fn store_key(fqn: &str) -> &str {
fqn.strip_prefix("::").unwrap_or(fqn)
}
pub(crate) fn chunk_key(fqn: &str) -> String {
format!("::{}", store_key(fqn))
}
pub(crate) fn is_namespaced(name: &str) -> bool {
name.contains("::")
}
pub struct NsCtx {
pub current: String,
pub links: HashMap<(String, String), Link>,
pub renames: BTreeSet<String>,
pub exports: HashMap<String, Vec<String>>,
pub imports: HashMap<String, String>,
}
#[derive(Clone, PartialEq, Eq)]
pub enum Link {
Global,
Variable(String),
}
impl Default for NsCtx {
fn default() -> Self {
NsCtx {
current: "::".to_string(),
links: HashMap::new(),
renames: BTreeSet::new(),
exports: HashMap::new(),
imports: HashMap::new(),
}
}
}
impl NsCtx {
pub fn at_global(&self) -> bool {
self.current == "::"
}
}
pub(crate) fn global_key(c: &Compiler, name: &str) -> String {
if name.starts_with('\u{0}') {
return name.to_string();
}
if c.scope.is_some() {
return match c.ns.links.get(&(c.ns.current.clone(), name.to_string())) {
Some(Link::Variable(fqn)) => store_key(fqn).to_string(),
Some(Link::Global) => name.to_string(),
None => resolve_var(c, name),
};
}
resolve_var(c, name)
}
fn resolve_var(c: &Compiler, name: &str) -> String {
if c.projected && name.starts_with("::") {
return chunk_key(&resolve(&c.ns.current, name));
}
if name.contains("::") {
return store_key(&resolve(&c.ns.current, name)).to_string();
}
if c.ns.at_global() {
return name.to_string();
}
store_key(&resolve(&c.ns.current, name)).to_string()
}
#[derive(Clone, PartialEq, Eq)]
pub struct Entry {
pub origin: String,
}
#[derive(Default)]
pub struct Registry {
namespaces: BTreeSet<String>,
commands: BTreeMap<String, Entry>,
exports: BTreeMap<String, Vec<String>>,
ensembles: BTreeSet<String>,
gone: BTreeSet<String>,
}
impl Registry {
pub fn ensure(&mut self, fqn: &str) {
self.namespaces.insert("::".to_string());
let mut here = String::from("::");
for part in components(fqn).1 {
if here == "::" {
here = format!("::{part}");
} else {
here = format!("{here}::{part}");
}
self.namespaces.insert(here.clone());
}
}
pub fn exists(&self, fqn: &str) -> bool {
fqn == "::" || self.namespaces.contains(fqn)
}
pub fn define(&mut self, fqn: &str) {
let ns = parent_of(fqn);
if !ns.is_empty() {
self.ensure(&ns);
}
self.commands.insert(
fqn.to_string(),
Entry {
origin: fqn.to_string(),
},
);
}
pub fn command(&self, fqn: &str) -> Option<&Entry> {
self.commands.get(fqn)
}
fn commands_in(&self, ns: &str) -> Vec<String> {
self.commands
.keys()
.filter(|name| parent_of(name) == ns)
.cloned()
.collect()
}
}
fn refuse_dynamic(c: &Compiler, what: &str) -> CompileError {
c.err(format!(
"{what} is not supported yet: this frontend resolves namespaces while compiling, \
so the name has to be written out"
))
}
pub const SUBCOMMANDS: &[&str] = &[
"children",
"code",
"current",
"delete",
"ensemble",
"eval",
"exists",
"export",
"forget",
"import",
"inscope",
"origin",
"parent",
"path",
"qualifiers",
"tail",
"unknown",
"upvar",
"which",
];
fn bad_subcommand(name: &str) -> String {
let mut list = String::new();
for (i, s) in SUBCOMMANDS.iter().enumerate() {
if i > 0 {
list.push_str(", ");
}
if i + 1 == SUBCOMMANDS.len() {
list.push_str("or ");
}
list.push_str(s);
}
format!("unknown or ambiguous subcommand \"{name}\": must be {list}")
}
fn subcommand_of(name: &str) -> Option<&'static str> {
if let Some(exact) = SUBCOMMANDS.iter().find(|s| **s == name) {
return Some(exact);
}
let mut hits = SUBCOMMANDS.iter().filter(|s| s.starts_with(name));
match (hits.next(), hits.next()) {
(Some(only), None) if !name.is_empty() => Some(only),
_ => None,
}
}
impl Compiler {
pub(crate) fn cmd_namespace(&mut self, args: &[Word]) -> Result<(), CompileError> {
let Some(first) = args.first() else {
return self.error("wrong # args: should be \"namespace subcommand ?arg ...?\"");
};
let sub = self.literal_of(first, "namespace subcommand")?.to_string();
let Some(sub) = subcommand_of(&sub) else {
return self.error(bad_subcommand(&sub));
};
let rest = &args[1..];
match sub {
"eval" => self.ns_eval(rest),
"current" => {
if !rest.is_empty() {
return self.error("wrong # args: should be \"namespace current\"");
}
let here = self.ns.current.clone();
self.push_str(&here);
Ok(())
}
"qualifiers" | "tail" => self.ns_name_op(sub, rest),
"code" => self.ns_runtime(sub, rest, 1..=1, "namespace code arg"),
"exists" => self.ns_runtime(sub, rest, 1..=1, "namespace exists name"),
"parent" => self.ns_runtime(sub, rest, 0..=1, "namespace parent ?name?"),
"children" => self.ns_runtime(sub, rest, 0..=2, "namespace children ?name? ?pattern?"),
"delete" => {
self.ns_runtime(sub, rest, 0..=usize::MAX, "namespace delete ?name name...?")
}
"export" => {
self.ns_note_export(rest);
self.ns_runtime(
sub,
rest,
0..=usize::MAX,
"namespace export ?-clear? ?pattern pattern...?",
)
}
"import" => {
self.ns_note_import(rest)?;
self.ns_runtime(
sub,
rest,
0..=usize::MAX,
"namespace import ?-force? ?pattern pattern...?",
)
}
"forget" => {
self.ns_note_forget(rest);
self.ns_runtime(
sub,
rest,
0..=usize::MAX,
"namespace forget ?pattern pattern...?",
)
}
"origin" => self.ns_runtime(sub, rest, 1..=1, "namespace origin name"),
"which" => self.ns_runtime(
sub,
rest,
1..=2,
"namespace which ?-command? ?-variable? name",
),
"ensemble" => self.ns_runtime(
sub,
rest,
1..=usize::MAX,
"namespace ensemble subcommand ?arg ...?",
),
"inscope" => self.ns_runtime(
sub,
rest,
2..=usize::MAX,
"namespace inscope ns script ?arg...?",
),
"path" | "unknown" | "upvar" => {
Err(refuse_dynamic(self, &format!("\"namespace {sub}\"")))
}
_ => unreachable!("every subcommand above is one of SUBCOMMANDS"),
}
}
fn ns_name_op(&mut self, sub: &str, rest: &[Word]) -> Result<(), CompileError> {
let [arg] = rest else {
return self.error(format!(
"wrong # args: should be \"namespace {sub} string\""
));
};
if let Some(text) = arg.as_literal() {
let answer = if sub == "tail" {
tail(text)
} else {
qualifiers(text)
};
self.push_str(answer);
return Ok(());
}
self.ns_runtime(sub, rest, 1..=1, &format!("namespace {sub} string"))
}
fn ns_runtime(
&mut self,
sub: &str,
rest: &[Word],
arity: std::ops::RangeInclusive<usize>,
usage: &str,
) -> Result<(), CompileError> {
if !arity.contains(&rest.len()) {
return self.error(format!("wrong # args: should be \"{usage}\""));
}
let count = u8::try_from(rest.len() + 2)
.map_err(|_| self.err(format!("too many arguments for \"namespace {sub}\"")))?;
self.push_str(sub);
let here = self.ns.current.clone();
self.push_str(&here);
for w in rest {
self.word(w)?;
}
self.emit(Op::Extended(ext::NS, count), 1 - count as i32);
Ok(())
}
fn ns_eval(&mut self, rest: &[Word]) -> Result<(), CompileError> {
let [name_w, body @ ..] = rest else {
return self.error("wrong # args: should be \"namespace eval name arg ?arg...?\"");
};
if body.is_empty() {
return self.error("wrong # args: should be \"namespace eval name arg ?arg...?\"");
}
let Some(name) = name_w.as_literal() else {
return Err(refuse_dynamic(self, "a computed \"namespace eval\" name"));
};
if self.scope.is_some() {
return self.error(
"\"namespace eval\" inside a procedure is not supported yet: an unqualified \
name in its body would take a frame slot rather than the namespace's variable",
);
}
let target = resolve(&self.ns.current, name);
let mut text = String::new();
for (i, w) in body.iter().enumerate() {
let Some(piece) = w.as_literal() else {
return Err(refuse_dynamic(self, "a computed \"namespace eval\" body"));
};
if i > 0 {
text.push(' ');
}
text.push_str(piece);
}
let script = crate::parser::parse(&text).map_err(|e| self.deferrable_err(e.msg))?;
prescan(&mut self.procs, &script, &target);
prescan_script(&mut self.procs, &script, &target);
self.push_str("\u{0}create");
let here = self.ns.current.clone();
self.push_str(&here);
self.push_str(&target);
self.emit(Op::Extended(ext::NS, 3), -2);
self.emit(Op::Pop, -1);
let outer = std::mem::replace(&mut self.ns.current, target);
self.body_depth += 1;
let result = self.script_value(&script);
self.body_depth -= 1;
self.ns.current = outer;
result
}
pub(crate) fn cmd_variable(&mut self, args: &[Word]) -> Result<(), CompileError> {
let here = self.ns.current.clone();
let mut i = 0;
while i < args.len() {
let name = self.var_name_of(&args[i])?;
if name.contains("::") {
return self.error(format!(
"bad variable name \"{name}\": can't create a local variable with a \
namespace separator"
));
}
let fqn = resolve(&here, &name);
if self.scope.is_some() {
self.ns
.links
.insert((here.clone(), name.clone()), Link::Variable(fqn.clone()));
if let Some(scope) = self.scope.as_mut() {
if scope.locals.contains_key(&name) {
return self.error(format!("variable \"{name}\" already exists"));
}
scope.globals.insert(name.clone());
}
}
if let Some(value) = args.get(i + 1) {
self.scalar_set_guard(&name);
self.word(value)?;
self.emit_set_var(&name);
i += 2;
} else {
i += 1;
}
}
self.push_empty();
Ok(())
}
pub(crate) fn ns_global(&mut self, args: &[Word]) -> Result<(), CompileError> {
if self.scope.is_some() && !self.ns.at_global() {
let here = self.ns.current.clone();
for w in args {
let Ok(name) = self.var_name_of(w) else {
continue;
};
self.ns.links.insert((here.clone(), name), Link::Global);
}
}
self.cmd_global(args)
}
pub(crate) fn ns_proc(&mut self, args: &[Word]) -> Result<(), CompileError> {
let qualified;
let args = if self.ns.at_global() {
args
} else {
let [name_w, rest @ ..] = args else {
return self.cmd_proc(args);
};
let Some(name) = name_w.as_literal() else {
return self.cmd_proc(args);
};
let fqn = resolve(&self.ns.current, name);
qualified = std::iter::once(literal_word(store_key(&fqn)))
.chain(rest.iter().cloned())
.collect::<Vec<Word>>();
&qualified
};
let outer_links = self.ns.links.clone();
let compiled = self.cmd_proc(args);
self.ns.links = outer_links;
compiled?;
if let Some(name) = args.first().and_then(|w| w.as_literal()) {
let fqn = resolve("::", name);
self.emit(Op::Pop, -1);
self.push_str("\u{0}define");
let here = self.ns.current.clone();
self.push_str(&here);
self.push_str(&fqn);
self.emit(Op::Extended(ext::NS, 3), -2);
}
Ok(())
}
pub(crate) fn cmd_rename(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [old, new] = args else {
return self.error("wrong # args: should be \"rename oldName newName\"");
};
if let Some(name) = old.as_literal() {
self.ns.renames.insert(name.to_string());
self.ns.renames.insert(tail(name).to_string());
let from = store_key(&resolve(&self.ns.current, name)).to_string();
if let Some(to) = new.as_literal().filter(|t| !t.is_empty()) {
if self.procs.contains_key(&from) {
let to = store_key(&resolve(&self.ns.current, to)).to_string();
self.ns.imports.insert(to, from);
}
}
}
self.ns_runtime(
"\u{0}rename",
&[old.clone(), new.clone()],
2..=2,
"rename oldName newName",
)
}
fn ns_note_export(&mut self, rest: &[Word]) {
let here = self.ns.current.clone();
let list = self.ns.exports.entry(here).or_default();
for w in rest {
if let Some(pattern) = w.as_literal() {
if pattern != "-clear" && !list.contains(&pattern.to_string()) {
list.push(pattern.to_string());
}
}
}
}
fn ns_note_import(&mut self, rest: &[Word]) -> Result<(), CompileError> {
let here = self.ns.current.clone();
for w in rest {
let Some(pattern) = w.as_literal() else {
return Err(refuse_dynamic(
self,
"a computed \"namespace import\" pattern",
));
};
if pattern == "-force" {
continue;
}
let fqn = resolve(&here, pattern);
let from = parent_of(&fqn);
let pat = tail(&fqn).to_string();
let exported = self.ns.exports.get(&from).cloned().unwrap_or_default();
let found: Vec<String> = self
.procs
.keys()
.filter(|key| parent_of(&format!("::{key}")) == from)
.filter(|key| crate::assoc::string_match(tail(key), &pat))
.filter(|key| {
exported
.iter()
.any(|e| crate::assoc::string_match(tail(key), e))
})
.cloned()
.collect();
let force = rest.iter().any(|w| w.as_literal() == Some("-force"));
for origin in found {
let local = store_key(&resolve(&here, tail(&origin))).to_string();
if local == origin {
continue;
}
let held = self.ns.imports.get(&local).cloned();
if !force
&& held.as_deref() != Some(origin.as_str())
&& (held.is_some() || self.procs.contains_key(&local))
{
let msg = format!("can't import command \"{}\": already exists", tail(&origin));
return Err(self.deferrable_err(msg));
}
self.ns.imports.insert(local, origin);
}
}
Ok(())
}
fn ns_note_forget(&mut self, rest: &[Word]) {
let here = self.ns.current.clone();
for w in rest {
let Some(pattern) = w.as_literal() else {
continue;
};
let fqn = resolve(&here, pattern);
let from = parent_of(&fqn);
let pat = tail(&fqn).to_string();
self.ns.imports.retain(|local, origin| {
parent_of(&format!("::{local}")) != here
|| parent_of(&format!("::{origin}")) != from
|| !crate::assoc::string_match(tail(origin), &pat)
});
}
}
pub(crate) fn ns_resolves(&self, name: &str) -> Option<String> {
let scoped = store_key(&resolve(&self.ns.current, name)).to_string();
if let Some(origin) = self.ns.imports.get(&scoped) {
return Some(origin.clone());
}
if name.contains("::") {
return self.procs.contains_key(&scoped).then_some(scoped);
}
if !self.ns.at_global() && self.procs.contains_key(&scoped) {
return Some(scoped);
}
if self.ns.renames.contains(name) && self.procs.contains_key(name) {
return Some(name.to_string());
}
None
}
pub(crate) fn ns_call(
&mut self,
written: &str,
key: &str,
args: &[Word],
) -> Result<(), CompileError> {
if self.ns.renames.contains(written) || self.ns.renames.contains(tail(written)) {
self.ns_guard(written);
}
self.call_proc(key, args)
}
fn ns_guard(&mut self, name: &str) {
let fqn = resolve(&self.ns.current, name);
self.push_str(&fqn);
self.emit(Op::Extended(ext::RENAME_GUARD, 1), 0);
self.emit(Op::Pop, -1);
}
}
pub(crate) fn call(c: &mut Compiler, name: &str, args: &[Word]) -> Result<(), CompileError> {
let key = c.ns_resolves(name).expect("the dispatch arm asked first");
c.ns_call(name, &key, args)
}
fn literal_word(text: &str) -> Word {
Word {
parts: if text.is_empty() {
Vec::new()
} else {
vec![Part::Lit(text.to_string())]
},
expand: false,
braced: true,
quoted: false,
}
}
pub fn prescan(procs: &mut HashMap<String, crate::procs::Signature>, script: &Script, ns: &str) {
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;
};
let fqn = store_key(&resolve(ns, name)).to_string();
if let Ok(sig) = crate::procs::parse_signature(&fqn, spec) {
procs.insert(fqn, sig);
}
}
}
pub fn prescan_script(
procs: &mut HashMap<String, crate::procs::Signature>,
script: &Script,
ns: &str,
) {
walk(procs, script, ns);
}
fn walk(procs: &mut HashMap<String, crate::procs::Signature>, script: &Script, ns: &str) {
for cmd in &script.commands {
let [head, sub, name, body @ ..] = cmd.words.as_slice() else {
continue;
};
if head.as_literal() != Some("namespace") {
continue;
}
if sub.as_literal().and_then(subcommand_of) != Some("eval") {
continue;
}
let (Some(name), true) = (name.as_literal(), !body.is_empty()) else {
continue;
};
let target = resolve(ns, name);
let mut text = String::new();
for (i, w) in body.iter().enumerate() {
let Some(piece) = w.as_literal() else {
return;
};
if i > 0 {
text.push(' ');
}
text.push_str(piece);
}
let Ok(inner) = crate::parser::parse(&text) else {
continue;
};
prescan(procs, &inner, &target);
walk(procs, &inner, &target);
}
}
pub(crate) fn extension(
interp: &crate::runtime::Shared,
vm: &mut fusevm::VM,
id: u16,
argc: u8,
) -> Result<(), TclError> {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
if id == ext::RENAME_GUARD {
let name = to_tcl_string(&values[0]);
let gone = {
let state = interp.lock().expect("interpreter lock");
state.ns.command(&name).is_none() && state.ns.renamed_away(&name)
};
if gone {
return Err(TclError::plain(format!(
"invalid command name \"{}\"",
tail(&name)
)));
}
vm.push(values.into_iter().next().expect("the guard takes one name"));
return Ok(());
}
let sub = to_tcl_string(&values[0]);
let here = to_tcl_string(&values[1]);
let args: Vec<String> = values[2..].iter().map(to_tcl_string).collect();
let result =
crate::runtime::with_written_back(interp, vm, |interp| run(interp, &sub, &here, &args))?;
vm.push(Value::Str(std::sync::Arc::new(result)));
Ok(())
}
impl Registry {
fn renamed_away(&self, fqn: &str) -> bool {
self.gone.contains(fqn)
}
}
fn rename(
state: &mut crate::runtime::State,
here: &str,
args: &[String],
) -> Result<String, TclError> {
let old = resolve(here, &args[0]);
let Some(entry) = state.ns.commands.remove(&old) else {
return Err(TclError::plain(format!(
"can't rename \"{}\": command doesn't exist",
args[0]
)));
};
let defined = state.commands.remove(store_key(&old));
state.ns.gone.insert(old);
if args[1].is_empty() {
return Ok(String::new());
}
let new = resolve(here, &args[1]);
if state.ns.commands.contains_key(&new) {
return Err(TclError::plain(format!(
"can't rename to \"{}\": command already exists",
args[1]
)));
}
let ns = parent_of(&new);
if !ns.is_empty() && !state.ns.exists(&ns) {
return Err(TclError::plain(format!(
"can't rename to \"{}\": unknown namespace",
args[1]
)));
}
state.ns.gone.remove(&new);
if let Some(defined) = defined {
state.commands.insert(store_key(&new).to_string(), defined);
}
state.ns.commands.insert(new, entry);
Ok(String::new())
}
fn run(
interp: &crate::runtime::Shared,
sub: &str,
here: &str,
args: &[String],
) -> Result<String, TclError> {
if sub == "inscope" {
return inscope(interp, here, args);
}
let mut state = interp.lock().expect("interpreter lock");
state.ns.ensure(here);
if sub == "which" {
return which(&state, here, args);
}
if sub == "\u{0}rename" {
return rename(&mut state, here, args);
}
let reg = &mut state.ns;
match sub {
"\u{0}create" => {
reg.ensure(&args[0]);
Ok(String::new())
}
"\u{0}define" => {
reg.define(&args[0]);
Ok(String::new())
}
"qualifiers" => Ok(qualifiers(&args[0]).to_string()),
"tail" => Ok(tail(&args[0]).to_string()),
"exists" => {
let fqn = resolve(here, &args[0]);
Ok(u8::from(reg.exists(&fqn)).to_string())
}
"parent" => {
let fqn = match args.first() {
Some(name) => resolve(here, name),
None => here.to_string(),
};
if !reg.exists(&fqn) {
return Err(TclError::plain(format!("namespace \"{fqn}\" not found")));
}
Ok(parent_of(&fqn))
}
"children" => {
let fqn = match args.first() {
Some(name) => resolve(here, name),
None => here.to_string(),
};
if !reg.exists(&fqn) {
return Err(TclError::plain(format!("namespace \"{fqn}\" not found")));
}
let pattern = args.get(1).map(|p| {
if p.contains("::") {
resolve(here, p)
} else if fqn == "::" {
format!("::{p}")
} else {
format!("{fqn}::{p}")
}
});
let kids: Vec<String> = reg
.namespaces
.iter()
.filter(|ns| parent_of(ns) == fqn)
.filter(|ns| match &pattern {
Some(p) => crate::assoc::string_match(ns, p),
None => true,
})
.cloned()
.collect();
Ok(crate::list::join(&kids))
}
"delete" => {
for name in args {
let fqn = resolve(here, name);
if !reg.exists(&fqn) {
return Err(TclError::plain(format!(
"unknown namespace \"{fqn}\" in namespace delete command"
)));
}
let prefix = format!("{}::", fqn.trim_end_matches(':'));
reg.namespaces
.retain(|ns| *ns != fqn && !ns.starts_with(&prefix));
let doomed: Vec<String> = reg
.commands
.keys()
.filter(|c| parent_of(c) == fqn || c.starts_with(&prefix))
.cloned()
.collect();
for c in doomed {
reg.commands.remove(&c);
reg.gone.insert(c);
}
reg.exports.remove(&fqn);
reg.ensembles.remove(&fqn);
}
Ok(String::new())
}
"code" => {
let script = &args[0];
if script.starts_with("::namespace inscope ")
|| script.starts_with("namespace inscope ")
{
return Ok(script.clone());
}
Ok(format!(
"::namespace inscope {here} {}",
crate::list::join(std::slice::from_ref(script))
))
}
"export" => {
let ns = here.to_string();
let mut patterns = args.to_vec();
let clear = patterns.first().map(String::as_str) == Some("-clear");
if clear {
patterns.remove(0);
reg.exports.remove(&ns);
}
if patterns.is_empty() && !clear {
return Ok(crate::list::join(
reg.exports.get(&ns).map(Vec::as_slice).unwrap_or(&[]),
));
}
let list = reg.exports.entry(ns).or_default();
for p in patterns {
if p.contains("::") {
return Err(TclError::plain(format!(
"invalid export pattern \"{p}\": pattern can't specify a namespace"
)));
}
if !list.contains(&p) {
list.push(p);
}
}
Ok(String::new())
}
"import" => {
let mut patterns = args.to_vec();
let force = patterns.first().map(String::as_str) == Some("-force");
if force {
patterns.remove(0);
}
for p in patterns {
let fqn = resolve(here, &p);
let from = parent_of(&fqn);
let pat = tail(&fqn).to_string();
if !reg.exists(&from) {
return Err(TclError::plain(format!(
"unknown namespace in import pattern \"{p}\""
)));
}
let exported = reg.exports.get(&from).cloned().unwrap_or_default();
let matches: Vec<String> = reg
.commands_in(&from)
.into_iter()
.filter(|c| crate::assoc::string_match(tail(c), &pat))
.filter(|c| {
exported
.iter()
.any(|e| crate::assoc::string_match(tail(c), e))
})
.collect();
for origin in matches {
let local = if here == "::" {
format!("::{}", tail(&origin))
} else {
format!("{here}::{}", tail(&origin))
};
let root = reg
.commands
.get(&origin)
.map(|e| e.origin.clone())
.unwrap_or_else(|| origin.clone());
if !force {
if let Some(held) = reg.commands.get(&local) {
if held.origin != root {
return Err(TclError::plain(format!(
"can't import command \"{}\": already exists",
tail(&origin)
)));
}
}
}
reg.gone.remove(&local);
reg.commands.insert(local, Entry { origin: root });
}
}
Ok(String::new())
}
"forget" => {
for p in args {
let fqn = resolve(here, p);
let from = parent_of(&fqn);
let pat = tail(&fqn).to_string();
let sources: Vec<String> = reg
.commands_in(&from)
.into_iter()
.filter(|c| crate::assoc::string_match(tail(c), &pat))
.collect();
let doomed: Vec<String> = reg
.commands_in(here)
.into_iter()
.filter(|local| {
!sources.contains(local)
&& reg
.commands
.get(local)
.is_some_and(|e| sources.contains(&e.origin))
})
.collect();
for c in doomed {
reg.commands.remove(&c);
reg.gone.insert(c);
}
}
Ok(String::new())
}
"origin" => {
let Some(found) = which_command(reg, here, &args[0]) else {
return Err(TclError::plain(format!(
"invalid command name \"{}\"",
args[0]
)));
};
Ok(reg
.commands
.get(&found)
.map(|e| e.origin.clone())
.unwrap_or(found))
}
"ensemble" => ensemble(reg, here, args),
other => Err(TclError::plain(bad_subcommand(other))),
}
}
fn which(state: &crate::runtime::State, here: &str, args: &[String]) -> Result<String, TclError> {
let (kind, name) = match args {
[name] => ("-command", name.as_str()),
[flag, name] => (flag.as_str(), name.as_str()),
_ => unreachable!("the arity was checked while compiling"),
};
match kind {
"-command" => Ok(which_command(&state.ns, here, name).unwrap_or_default()),
"-variable" => {
let scoped = resolve(here, name);
if state.globals.contains_key(store_key(&scoped)) {
return Ok(scoped);
}
if !name.starts_with("::") {
let global = resolve("::", name);
if state.globals.contains_key(store_key(&global)) {
return Ok(global);
}
}
Ok(String::new())
}
other => Err(TclError::plain(format!(
"bad option \"{other}\": must be -command or -variable"
))),
}
}
fn inscope(
interp: &crate::runtime::Shared,
here: &str,
args: &[String],
) -> Result<String, TclError> {
let ns = resolve(here, &args[0]);
if !interp.lock().expect("interpreter lock").ns.exists(&ns) {
return Err(TclError::plain(format!(
"unknown namespace \"{ns}\" in inscope namespace command"
)));
}
let mut script = args[1].clone();
for extra in &args[2..] {
script.push(' ');
script.push_str(&crate::list::join(std::slice::from_ref(extra)));
}
let source = format!(
"namespace eval {} {{\n{script}\n}}",
crate::list::join(std::slice::from_ref(&ns))
);
crate::runtime::run_source(interp, &source).map(|v| to_tcl_string(&v))
}
fn which_command(reg: &Registry, here: &str, name: &str) -> Option<String> {
let scoped = resolve(here, name);
if reg.commands.contains_key(&scoped) {
return Some(scoped);
}
if !name.starts_with("::") {
let global = resolve("::", name);
if reg.commands.contains_key(&global) {
return Some(global);
}
}
let bare = tail(&scoped);
if parent_of(&scoped) == "::" && crate::names::commands().contains(&bare) {
return Some(scoped);
}
None
}
fn ensemble(reg: &mut Registry, here: &str, args: &[String]) -> Result<String, TclError> {
const OPTIONS: &str =
"must be -command, -map, -parameters, -prefixes, -subcommands, or -unknown";
match args[0].as_str() {
"create" => {
let mut ns = here.to_string();
let mut i = 1;
while i < args.len() {
let opt = &args[i];
if !matches!(
opt.as_str(),
"-command" | "-map" | "-parameters" | "-prefixes" | "-subcommands" | "-unknown"
) {
return Err(TclError::plain(format!("bad option \"{opt}\": {OPTIONS}")));
}
if opt == "-command" {
ns = resolve(here, args.get(i + 1).map(String::as_str).unwrap_or(""));
}
i += 2;
}
reg.ensembles.insert(here.to_string());
reg.define(&ns);
Ok(ns)
}
"exists" => {
let fqn = resolve(here, args.get(1).map(String::as_str).unwrap_or(""));
Ok(u8::from(reg.ensembles.contains(&fqn)).to_string())
}
"configure" => {
let fqn = resolve(here, args.get(1).map(String::as_str).unwrap_or(""));
if !reg.ensembles.contains(&fqn) {
return Err(TclError::plain(format!("unknown command \"{fqn}\"")));
}
let exports = reg.exports.get(&fqn).cloned().unwrap_or_default();
Ok(format!(
"-map {{}} -namespace {fqn} -parameters {{}} -prefixes 1 -subcommands {{{}}} \
-unknown {{}}",
crate::list::join(&exports)
))
}
other => Err(TclError::plain(format!(
"unknown or ambiguous subcommand \"{other}\": must be configure, create, or exists"
))),
}
}