texlang_stdlib/
lib.rs

1//! Texlang standard library of primitives.
2//!
3//! This module contains implementations of TeX primitives for Texcraft.
4
5extern crate texcraft_stdext;
6extern crate texlang;
7
8use std::collections::HashMap;
9
10use texlang::command;
11use texlang::traits::*;
12use texlang::vm;
13use texlang::vm::implement_has_component;
14
15pub mod alias;
16pub mod alloc;
17pub mod catcode;
18pub mod conditional;
19pub mod def;
20pub mod expansion;
21pub mod io;
22pub mod job;
23pub mod math;
24pub mod prefix;
25pub mod registers;
26pub mod repl;
27pub mod script;
28pub mod sleep;
29pub mod testing;
30pub mod texcraft;
31pub mod the;
32pub mod time;
33pub mod tracingmacros;
34
35/// A state struct that is compatible with every primitive in the Texlang standard library.
36#[derive(Default)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct StdLibState {
39    alloc: alloc::Component,
40    catcode: catcode::Component,
41    conditional: conditional::Component,
42    pub job: job::Component,
43    prefix: prefix::Component,
44    registers: registers::Component<i32, 32768>,
45    repl: repl::Component,
46    script: script::Component,
47    time: time::Component,
48    tracing_macros: tracingmacros::Component,
49}
50
51impl TexlangState for StdLibState {
52    #[inline]
53    fn cat_code(&self, c: char) -> texlang::token::CatCode {
54        catcode::cat_code(self, c)
55    }
56
57    #[inline]
58    fn post_macro_expansion_hook(
59        token: texlang::token::Token,
60        input: &vm::ExpansionInput<Self>,
61        tex_macro: &texlang::texmacro::Macro,
62        arguments: &[&[texlang::token::Token]],
63        reversed_expansion: &[texlang::token::Token],
64    ) {
65        tracingmacros::hook(token, input, tex_macro, arguments, reversed_expansion)
66    }
67
68    #[inline]
69    fn expansion_override_hook(
70        token: texlang::token::Token,
71        input: &mut vm::ExpansionInput<Self>,
72        tag: Option<texlang::command::Tag>,
73    ) -> command::Result<Option<texlang::token::Token>> {
74        expansion::noexpand_hook(token, input, tag)
75    }
76
77    #[inline]
78    fn variable_assignment_scope_hook(
79        state: &mut Self,
80    ) -> texcraft_stdext::collections::groupingmap::Scope {
81        prefix::variable_assignment_scope_hook(state)
82    }
83}
84
85impl StdLibState {
86    pub fn all_initial_built_ins(
87    ) -> HashMap<&'static str, texlang::command::BuiltIn<StdLibState>> {
88        HashMap::from([
89            ("advance", math::get_advance()),
90            //
91            ("catcode", catcode::get_catcode()),
92            ("count", registers::get_count()),
93            ("countdef", registers::get_countdef()),
94            //
95            ("day", time::get_day()),
96            ("def", def::get_def()),
97            ("divide", math::get_divide()),
98            ("dumpFormat", job::get_dumpformat()),
99            ("dumpValidate", job::get_dumpvalidate()),
100            //
101            ("else", conditional::get_else()),
102            ("expandafter", expansion::get_expandafter_optimized()),
103            //
104            ("fi", conditional::get_fi()),
105            //
106            ("gdef", def::get_gdef()),
107            ("global", prefix::get_global()),
108            ("globaldefs", prefix::get_globaldefs()),
109            //
110            ("ifcase", conditional::get_if_case()),
111            ("iffalse", conditional::get_if_false()),
112            ("ifnum", conditional::get_if_num()),
113            ("ifodd", conditional::get_if_odd()),
114            ("iftrue", conditional::get_if_true()),
115            ("input", io::get_input()),
116            //
117            ("jobname", job::get_jobname()),
118            //
119            ("let", alias::get_let()),
120            ("long", prefix::get_long()),
121            //
122            ("month", time::get_month()),
123            ("multiply", math::get_multiply()),
124            //
125            ("newInt", alloc::get_newint()),
126            (
127                "newInt_getter_provider_\u{0}",
128                alloc::get_newint_getter_provider(),
129            ),
130            ("newIntArray", alloc::get_newintarray()),
131            (
132                "newIntArray_getter_provider_\u{0}",
133                alloc::get_newintarray_getter_provider(),
134            ),
135            ("noexpand", expansion::get_noexpand()),
136            //
137            ("or", conditional::get_or()),
138            ("outer", prefix::get_outer()),
139            //
140            ("relax", expansion::get_relax()),
141            //
142            ("sleep", sleep::get_sleep()),
143            //
144            ("the", the::get_the()),
145            ("time", time::get_time()),
146            ("tracingmacros", tracingmacros::get_tracingmacros()),
147            //
148            ("year", time::get_year()),
149        ])
150    }
151
152    pub fn new() -> Box<vm::VM<StdLibState>> {
153        vm::VM::<StdLibState>::new(StdLibState::all_initial_built_ins())
154    }
155}
156
157implement_has_component![
158    StdLibState,
159    (alloc::Component, alloc),
160    (catcode::Component, catcode),
161    (conditional::Component, conditional),
162    (job::Component, job),
163    (prefix::Component, prefix),
164    (registers::Component<i32, 32768>, registers),
165    (repl::Component, repl),
166    (script::Component, script),
167    (time::Component, time),
168    (tracingmacros::Component, tracing_macros),
169];
170
171pub struct ErrorCase {
172    pub description: &'static str,
173    pub source_code: &'static str,
174}
175
176impl ErrorCase {
177    /// Returns a vector of TeX snippets that exercise all error paths in Texlang
178    pub fn all_error_cases() -> Vec<ErrorCase> {
179        let mut cases = vec![];
180        for (description, source_code) in vec![
181            ("end of input after \\global", r"\global"),
182            ("can't be prefixed by \\global", r"\global \sleep"),
183            ("can't be prefixed by \\global (character)", r"\global a"),
184            ("can't be prefixed by \\long", r"\long \let \a = \def"),
185            ("can't be prefixed by \\outer", r"\outer \let \a = \def"),
186            ("bad rhs in assignment", r"\year = X"),
187            ("invalid variable (undefined)", r"\advance \undefined by 4"),
188            (
189                "invalid variable (not a variable command)",
190                r"\advance \def by 4",
191            ),
192            ("invalid variable (character token)", r"\advance a by 4"),
193            ("invalid variable (eof)", r"\advance"),
194            ("invalid relation", r"\ifnum 3 z 4"),
195            ("malformed by keyword", r"\advance \year bg"),
196            ("undefined control sequence", r"\elephant"),
197            ("invalid character", "\u{7F}"),
198            ("empty control sequence", r"\"),
199            ("invalid end of group", r"}"),
200            ("invalid start of number", r"\count X"),
201            ("invalid start of number (eof)", r"\count"),
202            ("invalid start of number (not a variable)", r"\count \def"),
203            (
204                "case negative number to positive (from constant)",
205                r"\count -1",
206            ),
207            (
208                "cast negative number to positive (from variable)",
209                r"\count 0 = 1 \count - \count 0",
210            ),
211            (
212                "read positive number from negative variable value",
213                r"\count 0 = -1 \count \count 0",
214            ),
215            ("invalid character", r"\count `\def"),
216            ("invalid character (eof)", r"\count `"),
217            ("invalid octal digit", r"\count '9"),
218            ("invalid octal digit (eof)", r"\count '"),
219            ("invalid hexadecimal digit", "\\count \"Z"),
220            ("invalid hexadecimal digit (eof)", "\\count \""),
221            (
222                "decimal number too big (radix)",
223                r"\count 1000000000000000000000",
224            ),
225            (
226                "decimal number too big (sum)",
227                r"\count 18446744073709551617",
228            ),
229            ("octal number too big", r"\count '7777777777777777777777"),
230            (
231                "hexadecimal number too big",
232                "\\count \"AAAAAAAAAAAAAAAAAAAAAA",
233            ),
234            ("number with letter catcode", r"\catcode `1 = 11 \count 1"),
235            /*
236            ("", r""),
237            ("", r""),
238            ("", r""),
239            ("", r""),
240            ("", r""),
241            ("", r""),
242            ("", r""),
243            ("", r""),
244            ("", r""),
245            ("", r""),
246            ("", r""),
247            ("", r""),
248            ("", r""),
249            ("", r""), */
250            ("category code out of bounds", r"\catcode 0 = 17"),
251            ("invalid command target", r"\let a = \year"),
252            ("invalid command target (eof)", r"\let"),
253        ] {
254            cases.push(ErrorCase {
255                description,
256                source_code,
257            })
258        }
259        cases
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use testing::*;
267    use texlang::command;
268
269    type State = StdLibState;
270
271    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
272        StdLibState::all_initial_built_ins()
273    }
274
275    test_suite![
276        expansion_equality_tests((
277            overwrite_else,
278            r"\def\else{}\ifodd 2 \else should be skipped \fi",
279            r""
280        )),
281        serde_tests((serde_sanity, r"\def\HW{Hello World} ", r"\HW"),),
282    ];
283
284    #[test]
285    fn all_error_cases() {
286        let options = vec![
287            TestOption::InitialCommands(StdLibState::all_initial_built_ins),
288            TestOption::AllowUndefinedCommands(false),
289        ];
290        for case in ErrorCase::all_error_cases() {
291            println!("CASE {}", case.description);
292            run_failure_test::<StdLibState>(case.source_code, &options)
293        }
294    }
295}