retex-engine 1.3.0

The TeX typesetting engine for retex
Documentation
//! pgf's `\pgfmathsetlength` and `\pgfmathaddtolength`
//! (`pgfmathcalc.code.tex`), when the expression does not start with `+`:
//!
//! ```tex
//! \def\pgfmathsetlength#1#2{%
//!   \expandafter\pgfmath@onquick#2\pgfmath@%
//!   {…quick: a plain TeX assignment…}%
//!   {%
//!     \pgfmathparse{#2}%
//!     \ifpgfmathmathunitsdeclared%
//!       #1\pgfmathresult mu\relax%
//!     \else%
//!       #1\pgfmathresult pt\relax%
//!     \fi%
//!   }%
//!   \ignorespaces%
//! }
//! ```
//!
//! `\pgfmath@onquick` expands the first token of `#2` once, lets
//! `\pgfmath@next` be the token that follows (`\futurelet`), and then lets
//! it be `\pgfmath@secondoftwo` unless that token is `\ifx`-equal to `+`.
//! The native implementation makes the same local `\let`, runs the native
//! `\pgfmathparse` ([`super::pgfmath::parse_tokens`]), which leaves
//! `\ifpgfmathmathunitsdeclared` false, assigns (or advances) the dimension
//! register `#1` locally with the value TeX scans from `<result>pt`, and
//! puts `\ignorespaces` back (the `\relax` after the unit does nothing).
//!
//! It declines (nothing changed) when `#1` is not a dimension register,
//! for the quick case, a first token whose expansion the native
//! `\pgfmathparse` would not handle, `#2` containing `\pgfmath@`, a result that TeX would not
//! scan as a dimension or that overflows, `\globaldefs`, a pending
//! `\afterassignment` (which the `\futurelet` would trigger), other
//! definitions of the macros involved, and whenever the native
//! `\pgfmathparse` declines.

use std::cell::RefCell;

use super::Native;
use super::deps::Deps;
use super::latex::{ifx, token_meaning};
use super::pgfdim;
use crate::Engine;
use crate::cmd::Cmd;
use crate::eqtb::{Loc, Meaning, Val};
use crate::primitives_gen::assign_int as ip;
use crate::scan::REG_OFFSET;
use crate::token::{Token, TokenList};

/// `\pgfmathsetlength` of pgf 3.1.11a.
pub(super) const SETLENGTH: Native = Native {
    name: "pgfmathsetlength",
    fingerprints: &[(41, 0xdfa5_e94d_031f_863c)],
    executes: true,
    run: run_setlength,
};

/// `\pgfmathaddtolength` of pgf 3.1.11a.
pub(super) const ADDTOLENGTH: Native = Native {
    name: "pgfmathaddtolength",
    fingerprints: &[(44, 0x1b8d_0332_32b4_64e5)],
    executes: true,
    run: run_addtolength,
};

const NAMES: &[&str] = &[
    "pgfmath@onquick",
    "pgfmath@@onquick",
    "pgfmath@firstoftwo",
    "pgfmath@secondoftwo",
    "pgfmathparse",
    "ifpgfmathmathunitsdeclared",
    "iffalse",
    "ignorespaces",
    "relax",
    "futurelet",
    "expandafter",
    "ifx",
    "let",
    "else",
    "fi",
    "advance",
];

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

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

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

fn run_setlength(e: &mut Engine, args: &[TokenList]) -> bool {
    run(e, args, false)
}

fn run_addtolength(e: &mut Engine, args: &[TokenList]) -> bool {
    run(e, args, true)
}

/// The token `\futurelet` sees after `\expandafter` expanded the first
/// token of `arg` once, if that is one of the simple cases (see
/// [`super::pgfmath::first_after_expansion`]: also macros with
/// arguments, as `\pgfkeysvalueof{...}`).
fn quick_token(e: &Engine, arg: &[Token], stop: Token) -> Option<Token> {
    super::pgfmath::first_after_expansion(e, arg, stop)
}

fn run(e: &mut Engine, args: &[TokenList], advance: bool) -> bool {
    if e.eqtb.int(ip::GLOBALDEFS) != 0 || e.after_token.0 != 0 {
        return false;
    }
    let [reg] = &args[0][..] else {
        return why("register");
    };
    if !reg.is_cs() {
        return why("register");
    }
    let rm = e.eqtb.meaning(reg.cs_id());
    if rm.cmd != Cmd::AssignDimen || rm.chr < REG_OFFSET {
        return why("register");
    }
    let n = rm.chr - REG_OFFSET;
    if !DEPS.with(|d| d.borrow_mut().check(e)) {
        return why("dependencies");
    }
    let lookup = |n: &[u8]| e.hash.lookup(n);
    let (Some(stop), Some(next), Some(second), Some(ignore)) = (
        lookup(b"pgfmath@"),
        lookup(b"pgfmath@next"),
        lookup(b"pgfmath@secondoftwo"),
        lookup(b"ignorespaces"),
    ) else {
        return false;
    };
    let stop = Token::cs(stop);
    if args[1].contains(&stop) {
        return why("stop token");
    }
    let Some(t) = quick_token(e, &args[1], stop) else {
        return why("first token");
    };
    let plus = Meaning::prim(Cmd::OtherChar, u32::from(b'+'));
    if ifx(&token_meaning(e, t), &plus) {
        return why("quick");
    }
    // The value TeX scans from `<result>pt`, checked before anything is
    // assigned.
    let value = |e: &Engine, result: &[Token]| -> Option<i32> {
        let v = scanned(result)?;
        if advance {
            pgfdim::adv(e.eqtb.dimen[n as usize].val, v).ok()
        } else {
            Some(v)
        }
    };
    let Some(result) =
        super::pgfmath::parse_tokens(e, args[1].to_vec(), &|e, r| value(e, r).is_some())
    else {
        return why("parse");
    };
    let v = value(e, &result).expect("accepted");
    let m = e.eqtb.meaning(second).clone();
    e.eq_define(Loc::Cs(next), Val::Meaning(m));
    e.eq_define(Loc::Dimen(n), Val::Int(i64::from(v)));
    e.native_insert(vec![Token::cs(ignore)]);
    if super::debug() {
        eprintln!("[native] pgfmathsetlength native");
    }
    true
}

/// `<result>pt` as TeX scans it: only characters of category 12 make a
/// number.
fn scanned(result: &[Token]) -> Option<i32> {
    let mut bytes = Vec::with_capacity(result.len());
    for t in result {
        if t.is_cs() || t.cmd_code() != Cmd::OtherChar as u32 || t.chr() > 0x7f {
            return None;
        }
        bytes.push(t.chr() as u8);
    }
    pgfdim::scan_pt(&bytes).ok()
}