retex-engine 1.2.0

The TeX typesetting engine for retex
Documentation
//! LaTeX kernel macros used everywhere.
//!
//! `\@ifnextchar#1#2#3` (also `\kernel@ifnextchar`, a copy):
//!
//! ```tex
//! \long\def\@ifnextchar#1#2#3{%
//!   \let\reserved@d=#1%
//!   \def\reserved@a{#2}%
//!   \def\reserved@b{#3}%
//!   \futurelet\@let@token\@ifnch}
//! ```
//!
//! `\@ifnch` skips space tokens (`\@xifnch` consumes each and looks
//! again), then lets `\reserved@c` be `\reserved@a` if the next token is
//! `\ifx`-equal to `#1`, else `\reserved@b`, and runs it. The native
//! implementation makes the same five local assignments, consumes the
//! spaces, and puts the chosen branch back into the input. It declines
//! for a `#1` that is not one token (or is a space), branches with macro
//! parameter characters, a next token that is a control sequence `\let`
//! to a space (which `\@xifnch` would not match) or `\outer`, with
//! `\globaldefs` or a pending `\afterassignment`, when the next token is
//! not in a token list on top of the input stack, and when `\@ifnch` or
//! `\@xifnch` have other definitions.

use std::cell::RefCell;
use std::rc::Rc;

use super::Native;
use super::deps::Deps;
use crate::Engine;
use crate::cmd::Cmd;
use crate::eqtb::{Loc, Meaning, Val};
use crate::primitives_gen::assign_int as ip;
use crate::token::{Macro, Token, TokenList};

/// `\@ifnextchar`.
pub(super) const IFNEXTCHAR: Native = Native {
    name: "@ifnextchar",
    fingerprints: &[(21, 0x8d84_79e2_4ee9_3867)],
    executes: true,
    run: run_ifnextchar,
};

const NAMES: &[&str] = &["@ifnch", "@xifnch", "@sptoken"];

const KNOWN: &[u64] = &[0x1af6_b74e_04ff_e52b];

thread_local! {
    static DEPS: RefCell<Deps> = RefCell::new(Deps::new("ifnextchar", NAMES, KNOWN));
}

fn why(reason: &str) -> bool {
    if super::debug() {
        eprintln!("[native] @ifnextchar declines ({reason})");
    }
    false
}

/// `\ifx` of two meanings.
pub fn ifx(a: &Meaning, b: &Meaning) -> bool {
    if a.cmd != b.cmd {
        return false;
    }
    if !a.cmd.is_call() {
        return a.chr == b.chr;
    }
    match (&a.mac, &b.mac) {
        (Some(x), Some(y)) => {
            Rc::ptr_eq(x, y) || (x.protected == y.protected && x.tokens == y.tokens)
        }
        _ => false,
    }
}

/// The meaning `\let` or `\futurelet` gives for token `t`.
pub fn token_meaning(e: &Engine, t: Token) -> Meaning {
    if t.is_cs() {
        e.eqtb.meaning(t.cs_id()).clone()
    } else {
        Meaning::prim(t.cmd(), t.chr())
    }
}

fn macro_meaning(body: &[Token]) -> Meaning {
    let mut t = Vec::with_capacity(body.len() + 1);
    t.push(Token::END_MATCH);
    t.extend_from_slice(body);
    Meaning {
        cmd: Cmd::Call,
        chr: 0,
        mac: Some(Rc::new(Macro::new(false, Rc::from(t)))),
    }
}

fn is_param(t: &Token) -> bool {
    !t.is_cs() && (t.cmd_code() == Cmd::MacParam as u32 || t.cmd_code() == Cmd::CarRet as u32)
}

fn run_ifnextchar(e: &mut Engine, args: &[TokenList]) -> bool {
    if e.eqtb.int(ip::GLOBALDEFS) != 0 || e.after_token.0 != 0 {
        return false;
    }
    let [c] = &args[0][..] else {
        return why("not one token");
    };
    if !c.is_cs() && c.cmd_code() == Cmd::Spacer as u32 {
        return why("space");
    }
    if args[1].iter().any(is_param) || args[2].iter().any(is_param) {
        return why("parameter characters");
    }
    if !DEPS.with(|d| d.borrow_mut().check(e)) {
        return why("dependencies");
    }
    let lookup = |n: &[u8]| e.hash.lookup(n);
    let (Some(ra), Some(rb), Some(rc), Some(rd), Some(lt), Some(sp)) = (
        lookup(b"reserved@a"),
        lookup(b"reserved@b"),
        lookup(b"reserved@c"),
        lookup(b"reserved@d"),
        lookup(b"@let@token"),
        lookup(b"@sptoken"),
    ) else {
        return false;
    };
    let Some(mut f) = e.native_following_subst() else {
        return why("input");
    };
    let space = e.eqtb.meaning(sp).clone();
    let mut i = 0;
    let next = loop {
        let Some(t) = f.get(e, i) else {
            return why("input");
        };
        let m = token_meaning(e, t);
        if ifx(&m, &space) {
            if t.is_cs() || t.chr() != u32::from(b' ') {
                return why("implicit or unusual space");
            }
            i += 1;
            continue;
        }
        if matches!(m.cmd, Cmd::OuterCall | Cmd::LongOuterCall) {
            return why("outer");
        }
        break (t, m);
    };
    if f.count(e, i).is_none() {
        return why("spaces inside a parameter");
    }
    let target = token_meaning(e, *c);
    let take_a = ifx(&next.1, &target);
    // Commit.
    e.native_consume(&f, i);
    e.eq_define(Loc::Cs(rd), Val::Meaning(target));
    let am = macro_meaning(&args[1]);
    let bm = macro_meaning(&args[2]);
    let cm = if take_a { am.clone() } else { bm.clone() };
    e.eq_define(Loc::Cs(ra), Val::Meaning(am));
    e.eq_define(Loc::Cs(rb), Val::Meaning(bm));
    e.eq_define(Loc::Cs(lt), Val::Meaning(next.1));
    e.eq_define(Loc::Cs(rc), Val::Meaning(cm));
    let branch = if take_a { &args[1] } else { &args[2] };
    e.native_insert(branch.to_vec());
    if super::debug() {
        eprintln!("[native] @ifnextchar native");
    }
    true
}