1use std::rc::Rc;
7
8use crate::Engine;
9use crate::cmd::Cmd;
10use crate::hash::CsId;
11use crate::token::Token;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Place {
16 pub file: String,
18 pub line: u32,
20 pub package: Option<String>,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Kind {
27 Undefined,
29 Macro {
31 protected: bool,
33 long: bool,
35 outer: bool,
37 },
38 Primitive,
40 Other,
42}
43
44#[derive(Clone, Debug)]
46pub struct Inner {
47 pub how: String,
49 pub description: Box<Description>,
51}
52
53#[derive(Clone, Debug)]
55pub struct Description {
56 pub name: String,
58 pub kind: Kind,
60 pub parameters: String,
62 pub meaning: String,
65 pub primitive: Option<&'static str>,
67 pub defined: Option<Place>,
69 pub earlier: Vec<Option<Place>>,
72 pub redefinitions: usize,
74 pub from_format: bool,
77 pub aliases: Vec<String>,
79 pub native: Option<&'static str>,
81 pub similar: Vec<String>,
83 pub inner: Option<Inner>,
85}
86
87const MAX_ALIASES: usize = 8;
89
90impl Engine {
91 pub fn watch_definitions(&mut self, names: &[Vec<u8>]) {
95 self.record_origins();
96 for name in names {
97 for (prefix, suffix) in [
98 (&b""[..], &b""[..]),
99 (b"", b" "),
100 (b"", b" code"),
101 (b"\\", b""),
102 ] {
103 let full = [prefix, name, suffix].concat();
104 let cs = self.hash.lookup_or_insert(&full);
105 if let Some(o) = self.origins.as_mut() {
106 o.history.entry(cs).or_default();
107 }
108 }
109 }
110 }
111
112 fn place(&self, origin: u32) -> Option<Place> {
113 let (file, line) = self.origins.as_ref()?.place(origin)?;
114 Some(Place {
115 file: file.name.clone(),
116 line,
117 package: file.package.clone(),
118 })
119 }
120
121 pub fn describe(&mut self, name: &[u8]) -> Description {
123 self.describe_at(name, 0)
124 }
125
126 fn describe_at(&mut self, name: &[u8], depth: u32) -> Description {
127 let shown = String::from_utf8_lossy(name).into_owned();
128 let cs = self.hash.lookup(name);
129 let meaning = cs.map_or(crate::eqtb::Meaning::UNDEFINED, |cs| {
130 self.eqtb.meaning(cs).clone()
131 });
132 let mut d = Description {
133 name: shown,
134 kind: Kind::Other,
135 parameters: String::new(),
136 meaning: String::new(),
137 primitive: None,
138 defined: None,
139 earlier: Vec::new(),
140 redefinitions: 0,
141 from_format: false,
142 aliases: Vec::new(),
143 native: None,
144 similar: Vec::new(),
145 inner: None,
146 };
147 let Some(cs) = cs.filter(|_| meaning.cmd != Cmd::UndefinedCs) else {
148 d.kind = Kind::Undefined;
149 d.similar = self.similar_defined_names(name);
150 return d;
151 };
152 let saved = (self.cur_cmd, self.cur_chr, self.cur_cs);
154 (self.cur_cmd, self.cur_chr, self.cur_cs) = (meaning.cmd, meaning.chr, cs);
155 let text = self.print_to_string(Engine::print_meaning);
156 (self.cur_cmd, self.cur_chr, self.cur_cs) = saved;
157 let text = String::from_utf8_lossy(&text).into_owned();
158 let history = self
160 .origins
161 .as_ref()
162 .and_then(|o| o.history.get(&cs))
163 .cloned()
164 .unwrap_or_default();
165 let body_origin = meaning.mac.as_ref().map_or(0, |m| m.origin.get());
166 let current = if body_origin != 0 {
167 Some(body_origin)
168 } else {
169 history.last().copied()
170 };
171 d.defined = current.and_then(|o| self.place(o));
172 d.from_format = current.is_none();
173 let mut earlier = history.clone();
174 if let Some(pos) = earlier.iter().rposition(|&o| Some(o) == current) {
175 earlier.truncate(pos);
176 } else {
177 earlier.pop();
178 }
179 d.redefinitions = earlier.len();
180 let mut distinct: Vec<u32> = Vec::new();
181 for o in earlier {
182 if !distinct.contains(&o) {
183 distinct.push(o);
184 }
185 }
186 d.earlier = distinct.into_iter().map(|o| self.place(o)).collect();
187 if let Some(mac) = &meaning.mac {
188 let n = meaning.cmd as u32 - Cmd::Call as u32;
189 d.kind = Kind::Macro {
190 protected: mac.protected,
191 long: n & 1 != 0,
192 outer: n & 2 != 0,
193 };
194 let (params, body) = text.split_once("->").unwrap_or(("", text.as_str()));
195 d.parameters = params
196 .split_once("macro:")
197 .map_or("", |(_, p)| p)
198 .to_owned();
199 d.meaning = body.to_owned();
200 d.native = self
201 .native_of(mac, cs)
202 .map(|i| crate::native::NATIVES[i].name);
203 if depth == 0 {
204 d.inner = self.inner_command(name, mac);
205 }
206 } else {
207 d.primitive = crate::primitives_gen::PRIMITIVES
208 .iter()
209 .find(|p| p.cmd == meaning.cmd && p.chr == meaning.chr)
210 .map(|p| p.name);
211 if d.primitive.is_some() {
212 d.kind = Kind::Primitive;
213 }
214 d.meaning = text;
215 }
216 d.aliases = self.aliases(cs, &meaning);
217 d
218 }
219
220 fn inner_command(&mut self, name: &[u8], mac: &crate::token::Macro) -> Option<Inner> {
225 if mac.tokens.first() != Some(&Token::END_MATCH) {
226 return None;
227 }
228 let body = &mac.tokens[1..];
229 let cs_named = |e: &Engine, t: Option<&Token>, n: &[u8]| {
230 t.is_some_and(|t| t.is_cs() && e.hash.name(t.cs_id()) == n)
231 };
232 let with = |prefix: &[u8], suffix: &[u8]| [prefix, name, suffix].concat();
233 let (how, inner) = if body.len() == 2
234 && cs_named(self, body.first(), b"protect")
235 && cs_named(self, body.get(1), &with(b"", b" "))
236 {
237 (
238 "a robust command (\\DeclareRobustCommand), standing for".to_owned(),
239 with(b"", b" "),
240 )
241 } else if cs_named(self, body.first(), b"@protected@testopt")
242 && cs_named(self, body.get(2), &with(b"\\", b""))
243 {
244 let default = body.get(3..).unwrap_or_default();
245 let default = self.print_to_string(|e| {
246 e.show_token_list(default, 0, default.len(), i64::MAX);
247 });
248 (
249 format!(
250 "a command with an optional argument (\\newcommand), default {}, whose code is",
251 String::from_utf8_lossy(&default)
252 ),
253 with(b"\\", b""),
254 )
255 } else if body
256 .first()
257 .is_some_and(|t| t.is_cs() && self.hash.name(t.cs_id()).starts_with(b"__cmd_start"))
258 && self.hash.lookup(&with(b"", b" code")).is_some()
259 {
260 let spec: Vec<u8> = body
262 .iter()
263 .skip(2)
264 .take_while(|t| t.is_cs() || t.cmd() != Cmd::RightBrace)
265 .filter(|t| !t.is_cs())
266 .filter_map(|t| u8::try_from(t.chr()).ok())
267 .collect();
268 (
269 format!(
270 "a document command (\\NewDocumentCommand) with arguments `{}`, whose code is",
271 String::from_utf8_lossy(&spec)
272 ),
273 with(b"", b" code"),
274 )
275 } else {
276 return None;
277 };
278 let cs = self.hash.lookup(&inner)?;
279 self.eqtb.meaning(cs).mac.as_ref()?;
280 Some(Inner {
281 how,
282 description: Box::new(self.describe_at(&inner, 1)),
283 })
284 }
285
286 fn aliases(&self, cs: CsId, meaning: &crate::eqtb::Meaning) -> Vec<String> {
289 let same = |id: CsId| {
290 let m = self.eqtb.meaning(id);
291 match (&meaning.mac, &m.mac) {
292 (Some(a), Some(b)) => Rc::ptr_eq(a, b),
293 (None, None) => m.cmd == meaning.cmd && m.chr == meaning.chr,
294 _ => false,
295 }
296 };
297 let mut found: Vec<(bool, String)> = self
298 .hash
299 .iter_named()
300 .filter(|&(_, id)| id != cs && same(id))
301 .map(|(n, _)| {
302 let internal = n.iter().any(|&b| matches!(b, b'@' | b'_' | b':'));
303 (internal, String::from_utf8_lossy(n).into_owned())
304 })
305 .collect();
306 found.sort();
307 found
308 .into_iter()
309 .take(MAX_ALIASES)
310 .map(|(_, n)| n)
311 .collect()
312 }
313}