Skip to main content

retex_engine/
describe.rs

1//! What a control sequence is, for `retex explain`: its meaning, where it
2//! was defined (with [`crate::profile::Origins`] recorded), the other
3//! names with the same meaning, the command behind a LaTeX robust
4//! command, and whether retex runs it natively.
5
6use std::rc::Rc;
7
8use crate::Engine;
9use crate::cmd::Cmd;
10use crate::hash::CsId;
11use crate::token::Token;
12
13/// Where a definition was made.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Place {
16    /// The file, as TeX prints its name.
17    pub file: String,
18    /// The line.
19    pub line: u32,
20    /// The package of the file; `None` for the document's own files.
21    pub package: Option<String>,
22}
23
24/// What a control sequence is.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Kind {
27    /// Not defined.
28    Undefined,
29    /// A macro.
30    Macro {
31        /// `\protected`.
32        protected: bool,
33        /// `\long`.
34        long: bool,
35        /// `\outer`.
36        outer: bool,
37    },
38    /// A TeX primitive, or another name for one.
39    Primitive,
40    /// Anything else: a register, a character, a font, ….
41    Other,
42}
43
44/// The command a macro stands for.
45#[derive(Clone, Debug)]
46pub struct Inner {
47    /// How the macro relates to it.
48    pub how: String,
49    /// The command.
50    pub description: Box<Description>,
51}
52
53/// A description of a control sequence.
54#[derive(Clone, Debug)]
55pub struct Description {
56    /// The name, without the escape character.
57    pub name: String,
58    /// What it is.
59    pub kind: Kind,
60    /// For macros: the parameter text, as `\show` prints it.
61    pub parameters: String,
62    /// The meaning, as `\meaning` gives it (for macros, the body after
63    /// `->`).
64    pub meaning: String,
65    /// For primitives: the primitive's own name.
66    pub primitive: Option<&'static str>,
67    /// Where the current meaning was defined, if known.
68    pub defined: Option<Place>,
69    /// Where earlier meanings were defined during the run, each place
70    /// once, in order (`None`: unknown).
71    pub earlier: Vec<Option<Place>>,
72    /// The number of earlier definitions.
73    pub redefinitions: usize,
74    /// Whether the meaning comes from the format (none of the run's
75    /// definitions made it).
76    pub from_format: bool,
77    /// Other names with the same meaning.
78    pub aliases: Vec<String>,
79    /// The name of retex's native implementation, if it has one.
80    pub native: Option<&'static str>,
81    /// For undefined names: defined names close to it.
82    pub similar: Vec<String>,
83    /// The command a robust command or a document command stands for.
84    pub inner: Option<Inner>,
85}
86
87/// The number of other names shown.
88const MAX_ALIASES: usize = 8;
89
90impl Engine {
91    /// Records the definitions of `names` from now on (see
92    /// [`describe`](Self::describe)), with those of the commands a robust
93    /// or document command `\x` stands for (`\x␣`, `\\x`, `\x code`).
94    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    /// Describes control sequence `name` (without the escape character).
122    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        // The meaning, as `\meaning` gives it.
153        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        // Definitions during the run, and the origin of the macro's body.
159        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    /// The command behind the macro `\name`: a LaTeX robust command is
221    /// `\protect\name␣`; a command with an optional argument is
222    /// `\@protected@testopt\name\\name{default}`; a document command's
223    /// code is `\name code`.
224    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            // `\__cmd_start:nNNnnn {som}…`: the argument specification.
261            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    /// Other names with the same meaning, names without `@`, `_` or `:`
287    /// first.
288    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}