use std::rc::Rc;
use crate::Engine;
use crate::cmd::Cmd;
use crate::hash::CsId;
use crate::token::Token;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Place {
pub file: String,
pub line: u32,
pub package: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Undefined,
Macro {
protected: bool,
long: bool,
outer: bool,
},
Primitive,
Other,
}
#[derive(Clone, Debug)]
pub struct Inner {
pub how: String,
pub description: Box<Description>,
}
#[derive(Clone, Debug)]
pub struct Description {
pub name: String,
pub kind: Kind,
pub parameters: String,
pub meaning: String,
pub primitive: Option<&'static str>,
pub defined: Option<Place>,
pub earlier: Vec<Option<Place>>,
pub redefinitions: usize,
pub from_format: bool,
pub aliases: Vec<String>,
pub native: Option<&'static str>,
pub similar: Vec<String>,
pub inner: Option<Inner>,
}
const MAX_ALIASES: usize = 8;
impl Engine {
pub fn watch_definitions(&mut self, names: &[Vec<u8>]) {
self.record_origins();
for name in names {
for (prefix, suffix) in [
(&b""[..], &b""[..]),
(b"", b" "),
(b"", b" code"),
(b"\\", b""),
] {
let full = [prefix, name, suffix].concat();
let cs = self.hash.lookup_or_insert(&full);
if let Some(o) = self.origins.as_mut() {
o.history.entry(cs).or_default();
}
}
}
}
fn place(&self, origin: u32) -> Option<Place> {
let (file, line) = self.origins.as_ref()?.place(origin)?;
Some(Place {
file: file.name.clone(),
line,
package: file.package.clone(),
})
}
pub fn describe(&mut self, name: &[u8]) -> Description {
self.describe_at(name, 0)
}
fn describe_at(&mut self, name: &[u8], depth: u32) -> Description {
let shown = String::from_utf8_lossy(name).into_owned();
let cs = self.hash.lookup(name);
let meaning = cs.map_or(crate::eqtb::Meaning::UNDEFINED, |cs| {
self.eqtb.meaning(cs).clone()
});
let mut d = Description {
name: shown,
kind: Kind::Other,
parameters: String::new(),
meaning: String::new(),
primitive: None,
defined: None,
earlier: Vec::new(),
redefinitions: 0,
from_format: false,
aliases: Vec::new(),
native: None,
similar: Vec::new(),
inner: None,
};
let Some(cs) = cs.filter(|_| meaning.cmd != Cmd::UndefinedCs) else {
d.kind = Kind::Undefined;
d.similar = self.similar_defined_names(name);
return d;
};
let saved = (self.cur_cmd, self.cur_chr, self.cur_cs);
(self.cur_cmd, self.cur_chr, self.cur_cs) = (meaning.cmd, meaning.chr, cs);
let text = self.print_to_string(Engine::print_meaning);
(self.cur_cmd, self.cur_chr, self.cur_cs) = saved;
let text = String::from_utf8_lossy(&text).into_owned();
let history = self
.origins
.as_ref()
.and_then(|o| o.history.get(&cs))
.cloned()
.unwrap_or_default();
let body_origin = meaning.mac.as_ref().map_or(0, |m| m.origin.get());
let current = if body_origin != 0 {
Some(body_origin)
} else {
history.last().copied()
};
d.defined = current.and_then(|o| self.place(o));
d.from_format = current.is_none();
let mut earlier = history.clone();
if let Some(pos) = earlier.iter().rposition(|&o| Some(o) == current) {
earlier.truncate(pos);
} else {
earlier.pop();
}
d.redefinitions = earlier.len();
let mut distinct: Vec<u32> = Vec::new();
for o in earlier {
if !distinct.contains(&o) {
distinct.push(o);
}
}
d.earlier = distinct.into_iter().map(|o| self.place(o)).collect();
if let Some(mac) = &meaning.mac {
let n = meaning.cmd as u32 - Cmd::Call as u32;
d.kind = Kind::Macro {
protected: mac.protected,
long: n & 1 != 0,
outer: n & 2 != 0,
};
let (params, body) = text.split_once("->").unwrap_or(("", text.as_str()));
d.parameters = params
.split_once("macro:")
.map_or("", |(_, p)| p)
.to_owned();
d.meaning = body.to_owned();
d.native = self
.native_of(mac, cs)
.map(|i| crate::native::NATIVES[i].name);
if depth == 0 {
d.inner = self.inner_command(name, mac);
}
} else {
d.primitive = crate::primitives_gen::PRIMITIVES
.iter()
.find(|p| p.cmd == meaning.cmd && p.chr == meaning.chr)
.map(|p| p.name);
if d.primitive.is_some() {
d.kind = Kind::Primitive;
}
d.meaning = text;
}
d.aliases = self.aliases(cs, &meaning);
d
}
fn inner_command(&mut self, name: &[u8], mac: &crate::token::Macro) -> Option<Inner> {
if mac.tokens.first() != Some(&Token::END_MATCH) {
return None;
}
let body = &mac.tokens[1..];
let cs_named = |e: &Engine, t: Option<&Token>, n: &[u8]| {
t.is_some_and(|t| t.is_cs() && e.hash.name(t.cs_id()) == n)
};
let with = |prefix: &[u8], suffix: &[u8]| [prefix, name, suffix].concat();
let (how, inner) = if body.len() == 2
&& cs_named(self, body.first(), b"protect")
&& cs_named(self, body.get(1), &with(b"", b" "))
{
(
"a robust command (\\DeclareRobustCommand), standing for".to_owned(),
with(b"", b" "),
)
} else if cs_named(self, body.first(), b"@protected@testopt")
&& cs_named(self, body.get(2), &with(b"\\", b""))
{
let default = body.get(3..).unwrap_or_default();
let default = self.print_to_string(|e| {
e.show_token_list(default, 0, default.len(), i64::MAX);
});
(
format!(
"a command with an optional argument (\\newcommand), default {}, whose code is",
String::from_utf8_lossy(&default)
),
with(b"\\", b""),
)
} else if body
.first()
.is_some_and(|t| t.is_cs() && self.hash.name(t.cs_id()).starts_with(b"__cmd_start"))
&& self.hash.lookup(&with(b"", b" code")).is_some()
{
let spec: Vec<u8> = body
.iter()
.skip(2)
.take_while(|t| t.is_cs() || t.cmd() != Cmd::RightBrace)
.filter(|t| !t.is_cs())
.filter_map(|t| u8::try_from(t.chr()).ok())
.collect();
(
format!(
"a document command (\\NewDocumentCommand) with arguments `{}`, whose code is",
String::from_utf8_lossy(&spec)
),
with(b"", b" code"),
)
} else {
return None;
};
let cs = self.hash.lookup(&inner)?;
self.eqtb.meaning(cs).mac.as_ref()?;
Some(Inner {
how,
description: Box::new(self.describe_at(&inner, 1)),
})
}
fn aliases(&self, cs: CsId, meaning: &crate::eqtb::Meaning) -> Vec<String> {
let same = |id: CsId| {
let m = self.eqtb.meaning(id);
match (&meaning.mac, &m.mac) {
(Some(a), Some(b)) => Rc::ptr_eq(a, b),
(None, None) => m.cmd == meaning.cmd && m.chr == meaning.chr,
_ => false,
}
};
let mut found: Vec<(bool, String)> = self
.hash
.iter_named()
.filter(|&(_, id)| id != cs && same(id))
.map(|(n, _)| {
let internal = n.iter().any(|&b| matches!(b, b'@' | b'_' | b':'));
(internal, String::from_utf8_lossy(n).into_owned())
})
.collect();
found.sort();
found
.into_iter()
.take(MAX_ALIASES)
.map(|(_, n)| n)
.collect()
}
}