texlang_stdlib/
prefix.rs

1//! The `\global`, `\long` and `\outer` prefix commands
2//!
3//! ## Global
4//!
5//! The `\global` command makes the subsequent assignment global;
6//!     i.e., not scoped to the current group.
7//! This command checks that the subsequent command is allowed to be prefixed
8//!     by global and then sets a `\global` bit.
9//! All commands that can be prefixed then consume this bit by calling
10//!     the hook [`TexlangState::variable_assignment_scope_hook`].
11//!
12//! In order for this to work correctly it is essential that *all* code
13//!   paths within the command call the hook -
14//!   even if the result is not used!
15//! For example `\gdef` always creates a macro in the global scope, but it still needs to
16//!   call the hook.
17//! Otherwise the global bit will still be set, and the next assignment may be
18//!     incorrectly performed in the global scope.
19//!
20//! This behavior should be verified with unit tests.
21//! This module provides
22//!   an [`assert_global_is_false`](get_assert_global_is_false) execution command
23//!   to make this easy - the command just raises an error if the global bit is still true.
24//!
25//! ## Outer and long
26//!
27//! The `\long` and `\outer` commands are designed to place restrictions on macros
28//!    to avoid performance problems.
29//! These restrictions are described in the TeXBook.
30//! Texlang does not currently impose these restrictions.
31//! However, Texlang does enforce the rule that these prefix commands can only come before
32//!  `\def`, `\gdef`, `\edef` and `\xdef`.
33//!
34
35use crate::alias;
36use crate::def;
37use crate::math;
38use crate::registers;
39use std::collections::HashSet;
40use texcraft_stdext::collections::groupingmap;
41use texlang::prelude as txl;
42use texlang::traits::*;
43use texlang::*;
44
45/// Component for the prefix commands.
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct Component {
48    scope: groupingmap::Scope,
49    global_defs_value: i32,
50    #[cfg_attr(feature = "serde", serde(skip))]
51    tags: Tags,
52}
53
54impl Default for Component {
55    fn default() -> Self {
56        Component {
57            scope: groupingmap::Scope::Local,
58            global_defs_value: 0,
59            tags: Default::default(),
60        }
61    }
62}
63
64struct Tags {
65    can_be_prefixed_with_any: HashSet<command::Tag>,
66    can_be_prefixed_with_global: HashSet<command::Tag>,
67    global_tag: command::Tag,
68    long_tag: command::Tag,
69    outer_tag: command::Tag,
70}
71
72impl Default for Tags {
73    fn default() -> Self {
74        Self {
75            can_be_prefixed_with_any: vec![def::def_tag()].into_iter().collect(),
76            can_be_prefixed_with_global: vec![
77                alias::let_tag(),
78                math::variable_op_tag(),
79                registers::countdef_tag(),
80            ]
81            .into_iter()
82            .collect(),
83            global_tag: GLOBAL_TAG.get(),
84            long_tag: LONG_TAG.get(),
85            outer_tag: OUTER_TAG.get(),
86        }
87    }
88}
89
90impl Component {
91    /// Register a command that can be prefixed with `\global`.
92    pub fn register_globally_prefixable_command(&mut self, tag: command::Tag) {
93        self.tags.can_be_prefixed_with_global.insert(tag);
94    }
95
96    /// Read the value of the global flag and reset the flag to false.
97    #[inline]
98    fn read_and_reset_global(&mut self) -> groupingmap::Scope {
99        match self.global_defs_value.cmp(&0) {
100            std::cmp::Ordering::Less => groupingmap::Scope::Local,
101            std::cmp::Ordering::Equal => {
102                std::mem::replace(&mut self.scope, groupingmap::Scope::Local)
103            }
104            std::cmp::Ordering::Greater => groupingmap::Scope::Global,
105        }
106    }
107
108    fn set_scope(&mut self, scope: groupingmap::Scope) {
109        self.scope = if self.global_defs_value == 0 {
110            scope
111        } else {
112            // If either globaldefs override is enabled we skip setting the scope here so that
113            // we can avoid setting it in the (hotter) variable assignment path.
114            groupingmap::Scope::Local
115        }
116    }
117}
118
119#[derive(Default, Clone, Copy)]
120struct Prefix {
121    global: Option<token::Token>,
122    long: Option<token::Token>,
123    outer: Option<token::Token>,
124}
125
126impl Prefix {
127    fn get_one(&self) -> (token::Token, Kind) {
128        if let Some(global_token) = self.global {
129            (global_token, Kind::Global)
130        } else if let Some(long_token) = self.long {
131            (long_token, Kind::Long)
132        } else if let Some(outer_token) = self.outer {
133            (outer_token, Kind::Outer)
134        } else {
135            panic!("")
136        }
137    }
138}
139
140/// Get the `\globaldefs` command.
141pub fn get_globaldefs<S: HasComponent<Component>>() -> command::BuiltIn<S> {
142    command::BuiltIn::new_variable(variable::Command::new_singleton(
143        |s, _| &s.component().global_defs_value,
144        |s, _| &mut s.component_mut().global_defs_value,
145    ))
146}
147
148/// Get the `\global` command.
149pub fn get_global<S: HasComponent<Component>>() -> command::BuiltIn<S> {
150    command::BuiltIn::new_execution(global_primitive_fn).with_tag(GLOBAL_TAG.get())
151}
152
153static GLOBAL_TAG: command::StaticTag = command::StaticTag::new();
154
155/// Implementation of the variable assignment scope hook.
156#[inline]
157pub fn variable_assignment_scope_hook<S: HasComponent<Component>>(
158    state: &mut S,
159) -> groupingmap::Scope {
160    state.component_mut().read_and_reset_global()
161}
162
163/// Get the `\long` command.
164pub fn get_long<S: HasComponent<Component>>() -> command::BuiltIn<S> {
165    command::BuiltIn::new_execution(long_primitive_fn).with_tag(LONG_TAG.get())
166}
167
168static LONG_TAG: command::StaticTag = command::StaticTag::new();
169
170/// Get the `\outer` command.
171pub fn get_outer<S: HasComponent<Component>>() -> command::BuiltIn<S> {
172    command::BuiltIn::new_execution(outer_primitive_fn).with_tag(OUTER_TAG.get())
173}
174
175static OUTER_TAG: command::StaticTag = command::StaticTag::new();
176
177fn global_primitive_fn<S: HasComponent<Component>>(
178    global_token: token::Token,
179    input: &mut vm::ExecutionInput<S>,
180) -> txl::Result<()> {
181    process_prefixes(
182        Prefix {
183            global: Some(global_token),
184            long: None,
185            outer: None,
186        },
187        input,
188    )
189}
190
191fn long_primitive_fn<S: HasComponent<Component>>(
192    long_token: token::Token,
193    input: &mut vm::ExecutionInput<S>,
194) -> txl::Result<()> {
195    process_prefixes(
196        Prefix {
197            global: None,
198            long: Some(long_token),
199            outer: None,
200        },
201        input,
202    )
203}
204
205fn outer_primitive_fn<S: HasComponent<Component>>(
206    outer_token: token::Token,
207    input: &mut vm::ExecutionInput<S>,
208) -> txl::Result<()> {
209    process_prefixes(
210        Prefix {
211            global: None,
212            long: None,
213            outer: Some(outer_token),
214        },
215        input,
216    )
217}
218
219fn process_prefixes<S: HasComponent<Component>>(
220    mut prefix: Prefix,
221    input: &mut vm::ExecutionInput<S>,
222) -> txl::Result<()> {
223    complete_prefix(&mut prefix, input)?;
224    let t = input.next_or_err(PrefixEndOfInputError {})?;
225    input.back(t);
226    match t.value() {
227        token::Value::CommandRef(command_ref) => {
228            // First check it it's a command that can be prefixed by global only.
229            if let Some(command::Command::Variable(_) | command::Command::Font(_)) =
230                input.commands_map_mut().get_command(&command_ref)
231            {
232                assert_only_global_prefix(input, t, prefix)?;
233                if prefix.global.is_some() {
234                    input
235                        .state_mut()
236                        .component_mut()
237                        .set_scope(groupingmap::Scope::Global);
238                }
239                return Ok(());
240            }
241            // Next check if it's a command that can be prefixed by any of the prefix command.
242            let component = input.state().component();
243            let tag = input.commands_map().get_tag(&command_ref);
244            if let Some(tag) = tag {
245                if component.tags.can_be_prefixed_with_any.contains(&tag) {
246                    if prefix.global.is_some() {
247                        input
248                            .state_mut()
249                            .component_mut()
250                            .set_scope(groupingmap::Scope::Global);
251                    }
252                    return Ok(());
253                }
254                // Next check if it's a command that can be prefixed by global only. In this case we check
255                // that no other prefixes are present.
256                if component.tags.can_be_prefixed_with_global.contains(&tag) {
257                    assert_only_global_prefix(input, t, prefix)?;
258                    if prefix.global.is_some() {
259                        input
260                            .state_mut()
261                            .component_mut()
262                            .set_scope(groupingmap::Scope::Global);
263                    }
264                    return Ok(());
265                }
266            }
267            // If we make it to here, this is not a valid target for the prefix command.
268            let (prefix_token, kind) = prefix.get_one();
269            Err(input.fatal_error(Error {
270                kind,
271                got: t,
272                prefix: prefix_token,
273                prefix_kind: kind,
274            }))
275        }
276        _ => {
277            let (prefix_token, kind) = prefix.get_one();
278            Err(input.fatal_error(Error {
279                kind,
280                got: t,
281                prefix: prefix_token,
282                prefix_kind: kind,
283            }))
284        }
285    }
286}
287
288#[derive(Debug)]
289struct PrefixEndOfInputError;
290
291impl error::EndOfInputError for PrefixEndOfInputError {
292    fn doing(&self) -> String {
293        "scanning a command to prefix".into()
294    }
295    fn notes(&self) -> Vec<error::display::Note> {
296        vec![
297            r"prefix commands (\global, \long, \outer) must be followed by a command to prefix"
298                .into(),
299        ]
300    }
301}
302
303fn complete_prefix<S: HasComponent<Component>>(
304    prefix: &mut Prefix,
305    input: &mut vm::ExecutionInput<S>,
306) -> txl::Result<()> {
307    // BUG: spaces and \relax are allowed after prefixes per TeX source sections 1211 and 404.
308    let found_prefix = match input.next()? {
309        None => false,
310        Some(t) => match t.value() {
311            token::Value::CommandRef(command_ref) => {
312                let tag = input.commands_map().get_tag(&command_ref);
313                if tag == Some(input.state().component().tags.global_tag) {
314                    prefix.global = Some(t);
315                    true
316                } else if tag == Some(input.state().component().tags.outer_tag) {
317                    prefix.outer = Some(t);
318                    true
319                } else if tag == Some(input.state().component().tags.long_tag) {
320                    prefix.long = Some(t);
321                    true
322                } else {
323                    input.back(t);
324                    false
325                }
326            }
327            _ => {
328                input.back(t);
329                false
330            }
331        },
332    };
333    if !found_prefix {
334        return Ok(());
335    }
336    complete_prefix(prefix, input)
337}
338
339fn assert_only_global_prefix<S: TexlangState>(
340    input: &mut vm::ExecutionInput<S>,
341    token: token::Token,
342    prefix: Prefix,
343) -> txl::Result<()> {
344    if let Some(outer_token) = prefix.outer {
345        Err(input.fatal_error(Error {
346            kind: Kind::Outer,
347            got: token,
348            prefix: outer_token,
349            prefix_kind: Kind::Outer,
350        }))
351    } else if let Some(long_token) = prefix.long {
352        Err(input.fatal_error(Error {
353            kind: Kind::Long,
354            got: token,
355            prefix: long_token,
356            prefix_kind: Kind::Long,
357        }))
358    } else {
359        Ok(())
360    }
361}
362
363#[derive(Debug, Clone, Copy)]
364enum Kind {
365    Global,
366    Long,
367    Outer,
368}
369
370impl Kind {
371    fn control_sequence(&self) -> &'static str {
372        match self {
373            Kind::Global => r"\global",
374            Kind::Long => r"\long",
375            Kind::Outer => r"\outer",
376        }
377    }
378}
379
380#[derive(Debug)]
381struct Error {
382    kind: Kind,
383    got: token::Token,
384    prefix: token::Token,
385    prefix_kind: Kind,
386}
387
388impl error::TexError for Error {
389    fn kind(&self) -> error::Kind {
390        error::Kind::Token(self.got)
391    }
392
393    fn title(&self) -> String {
394        match self.got.value() {
395            token::Value::CommandRef(_) => {
396                format![
397                    "this command cannot be prefixed by {}",
398                    self.prefix_kind.control_sequence()
399                ]
400            }
401            _ => format![
402                "character tokens cannot be prefixed by {}",
403                self.prefix_kind.control_sequence()
404            ],
405        }
406    }
407
408    fn source_annotation(&self) -> String {
409        format![
410            "cannot by prefixed by {}",
411            self.prefix_kind.control_sequence()
412        ]
413    }
414
415    fn notes(&self) -> Vec<error::display::Note> {
416        let guidance = match self.kind {
417            Kind::Global => {
418                r"see the documentation for \global for the list of commands it can be used with"
419            }
420            Kind::Long => {
421                r"the \long prefix can only be used with \def, \gdef, \edef and \xdef (or their aliases)"
422            }
423            Kind::Outer => {
424                r"the \outer prefix can only be used with \def, \gdef, \edef and \xdef (or their aliases)"
425            }
426        };
427        vec![
428            guidance.into(),
429            error::display::Note::SourceCodeTrace("the prefix appeared here:".into(), self.prefix),
430        ]
431    }
432}
433
434/// Get an execution command that checks that the global flag is off.
435///
436/// This command is used for unit testing Texlang.
437/// It tests that functions that can be prefixed with `\global`
438/// are following the convention described in the module docs.
439/// To use it, create a test for the following TeX snippet:
440/// ```tex
441/// \global \command <input to command> \assertGlobalIsFalse
442/// ```
443pub fn get_assert_global_is_false<S: HasComponent<Component>>() -> command::BuiltIn<S> {
444    fn noop_execution_cmd_fn<S: HasComponent<Component>>(
445        token: token::Token,
446        input: &mut vm::ExecutionInput<S>,
447    ) -> txl::Result<()> {
448        match input.state_mut().component_mut().read_and_reset_global() {
449            groupingmap::Scope::Global => Err(input.fatal_error(error::SimpleTokenError::new(
450                token,
451                "assertion failed: global is true",
452            ))),
453            groupingmap::Scope::Local => Ok(()),
454        }
455    }
456    command::BuiltIn::new_execution(noop_execution_cmd_fn)
457}
458
459#[cfg(test)]
460mod test {
461    use super::*;
462    use crate::the;
463    use std::collections::HashMap;
464    use texlang::vm::implement_has_component;
465    use texlang_testing::*;
466
467    #[derive(Default)]
468    struct State {
469        prefix: Component,
470        testing: TestingComponent,
471    }
472
473    impl TexlangState for State {
474        fn variable_assignment_scope_hook(state: &mut Self) -> groupingmap::Scope {
475            variable_assignment_scope_hook(state)
476        }
477    }
478    impl the::TheCompatible for State {}
479
480    implement_has_component![State {
481        prefix: Component,
482        testing: TestingComponent,
483    }];
484
485    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
486        HashMap::from([
487            ("global", get_global()),
488            ("globaldefs", get_globaldefs()),
489            ("long", get_long()),
490            ("outer", get_outer()),
491            ("i", TestingComponent::get_integer()),
492            ("the", the::get_the()),
493            ("def", def::get_def()),
494            ("advance", math::get_advance()),
495            (
496                "noOpExpansion",
497                command::BuiltIn::new_expansion(|_, _| Ok(())),
498            ),
499            (
500                "noOpExecution",
501                command::BuiltIn::new_execution(|_, _| Ok(())),
502            ),
503        ])
504    }
505
506    test_suite![
507        expansion_equality_tests(
508            (non_global, r"\i=5{\i=8}\the\i", "5"),
509            (non_global_2, r"\i=5\i=6{\i=8}\the\i", "6"),
510            (non_global_3, r"\i=5{\i=6{\i=8 \the\i}\the\i}\the\i", "865"),
511            (global, r"\i=5{\global\i=8}\the\i", "8"),
512            (global_squared, r"\i=5{\global\global\i=8}\the\i", "8"),
513            (long, r"\long\def\A{Hello}\A", "Hello"),
514            (outer, r"\outer\def\A{Hello}\A", "Hello"),
515            (
516                many_prefixes,
517                r"\long\outer\global\long\global\outer\def\A{Hello}\A",
518                "Hello"
519            ),
520            (global_defs_1, r"\i=5{\globaldefs=1 \i=8}\the\i", "8"),
521            (global_defs_2, r"\i=5{\globaldefs=-1\global\i=8}\the\i", "5"),
522        ),
523        fatal_error_tests(
524            (global_end_of_input, r"\global"),
525            (global_with_character, r"\global a"),
526            (global_with_undefined_command, r"\global \undefinedCommand"),
527            (
528                global_with_no_op_expansion_command,
529                r"\global \noOpExpansion"
530            ),
531            (
532                global_with_no_op_execution_command,
533                r"\global \noOpExecution"
534            ),
535            (long_prefix_when_global_allowed, r"\long\advance\i 0"),
536            (outer_prefix_when_global_allowed, r"\outer\advance\i 0"),
537        ),
538    ];
539}