retex-engine 1.3.0

The TeX typesetting engine for retex
Documentation
//! What a control sequence is, for `retex explain`: its meaning, where it
//! was defined (with [`crate::profile::Origins`] recorded), the other
//! names with the same meaning, the command behind a LaTeX robust
//! command, and whether retex runs it natively.

use std::rc::Rc;

use crate::Engine;
use crate::cmd::Cmd;
use crate::hash::CsId;
use crate::token::Token;

/// Where a definition was made.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Place {
    /// The file, as TeX prints its name.
    pub file: String,
    /// The line.
    pub line: u32,
    /// The package of the file; `None` for the document's own files.
    pub package: Option<String>,
}

/// What a control sequence is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
    /// Not defined.
    Undefined,
    /// A macro.
    Macro {
        /// `\protected`.
        protected: bool,
        /// `\long`.
        long: bool,
        /// `\outer`.
        outer: bool,
    },
    /// A TeX primitive, or another name for one.
    Primitive,
    /// Anything else: a register, a character, a font, ….
    Other,
}

/// The command a macro stands for.
#[derive(Clone, Debug)]
pub struct Inner {
    /// How the macro relates to it.
    pub how: String,
    /// The command.
    pub description: Box<Description>,
}

/// A description of a control sequence.
#[derive(Clone, Debug)]
pub struct Description {
    /// The name, without the escape character.
    pub name: String,
    /// What it is.
    pub kind: Kind,
    /// For macros: the parameter text, as `\show` prints it.
    pub parameters: String,
    /// The meaning, as `\meaning` gives it (for macros, the body after
    /// `->`).
    pub meaning: String,
    /// For primitives: the primitive's own name.
    pub primitive: Option<&'static str>,
    /// Where the current meaning was defined, if known.
    pub defined: Option<Place>,
    /// Where earlier meanings were defined during the run, each place
    /// once, in order (`None`: unknown).
    pub earlier: Vec<Option<Place>>,
    /// The number of earlier definitions.
    pub redefinitions: usize,
    /// Whether the meaning comes from the format (none of the run's
    /// definitions made it).
    pub from_format: bool,
    /// Other names with the same meaning.
    pub aliases: Vec<String>,
    /// The name of retex's native implementation, if it has one.
    pub native: Option<&'static str>,
    /// For undefined names: defined names close to it.
    pub similar: Vec<String>,
    /// The command a robust command or a document command stands for.
    pub inner: Option<Inner>,
}

/// The number of other names shown.
const MAX_ALIASES: usize = 8;

impl Engine {
    /// Records the definitions of `names` from now on (see
    /// [`describe`](Self::describe)), with those of the commands a robust
    /// or document command `\x` stands for (`\x␣`, `\\x`, `\x code`).
    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(),
        })
    }

    /// Describes control sequence `name` (without the escape character).
    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;
        };
        // The meaning, as `\meaning` gives it.
        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();
        // Definitions during the run, and the origin of the macro's body.
        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
    }

    /// The command behind the macro `\name`: a LaTeX robust command is
    /// `\protect\name␣`; a command with an optional argument is
    /// `\@protected@testopt\name\\name{default}`; a document command's
    /// code is `\name code`.
    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()
        {
            // `\__cmd_start:nNNnnn {som}…`: the argument specification.
            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)),
        })
    }

    /// Other names with the same meaning, names without `@`, `_` or `:`
    /// first.
    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()
    }
}