Skip to main content

mathtex_engine/
profile.rs

1use alloc::borrow::Cow;
2use alloc::vec::Vec;
3
4/// Opaque identifier string for an engine profile.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct ProfileId(pub &'static str);
7
8/// Identifies which TeX engine variant is active.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum EngineKind {
12    /// Plain TeX baseline engine.
13    Tex,
14    /// eTeX extended engine.
15    Etex,
16    /// XeTeX Unicode and font extensions.
17    Xetex,
18}
19
20/// Trait implemented by each engine variant to declare its capabilities and defaults.
21pub trait EngineProfile {
22    /// Returns the unique identifier for this profile.
23    fn id(&self) -> ProfileId;
24
25    /// Returns which TeX engine variant this profile represents.
26    fn kind(&self) -> EngineKind;
27
28    /// Returns the list of primitives available for this profile.
29    fn primitives(&self) -> &[PrimitiveSpec];
30
31    /// Returns default category code settings for this profile.
32    fn catcode_defaults(&self) -> CatcodeDefaults;
33
34    /// Returns default math code settings for this profile.
35    fn mathcode_defaults(&self) -> MathcodeDefaults;
36
37    /// Returns default register counts for this profile.
38    fn register_defaults(&self) -> RegisterDefaults;
39
40    /// Returns font capability settings for this profile.
41    fn font_semantics(&self) -> FontSemantics;
42
43    /// Returns which extension families are active for this profile.
44    fn extension_policy(&self) -> ExtensionPolicy;
45}
46
47/// Owned copy of profile settings so shared TeX code can check the active profile at runtime.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct EngineSemantics {
50    /// Profile identifier for this semantics snapshot.
51    pub profile: ProfileId,
52    /// Engine variant reported by the profile.
53    pub kind: EngineKind,
54    /// Primitives declared by the profile.
55    pub primitives: Vec<PrimitiveSpec>,
56    /// Category code defaults for this profile.
57    pub catcodes: CatcodeDefaults,
58    /// Math code defaults for this profile.
59    pub mathcodes: MathcodeDefaults,
60    /// Register count defaults for this profile.
61    pub registers: RegisterDefaults,
62    /// Font capability settings for this profile.
63    pub fonts: FontSemantics,
64    /// Extension families active for this profile.
65    pub extensions: ExtensionPolicy,
66    /// Computed patch flags derived from the extension policy.
67    pub patches: EnginePatches,
68}
69
70impl EngineSemantics {
71    /// Constructs an `EngineSemantics` snapshot from any `EngineProfile` implementor.
72    #[must_use]
73    pub fn from_profile<P>(profile: &P) -> Self
74    where
75        P: EngineProfile,
76    {
77        let extensions = profile.extension_policy();
78        Self {
79            profile: profile.id(),
80            kind: profile.kind(),
81            primitives: profile.primitives().to_vec(),
82            catcodes: profile.catcode_defaults(),
83            mathcodes: profile.mathcode_defaults(),
84            registers: profile.register_defaults(),
85            fonts: profile.font_semantics(),
86            extensions,
87            patches: EnginePatches::from_extension_policy(extensions),
88        }
89    }
90
91    /// Returns the slice of primitives registered for this profile.
92    #[must_use]
93    pub fn primitives(&self) -> &[PrimitiveSpec] {
94        &self.primitives
95    }
96
97    /// Finds a `PrimitiveSpec` by control sequence name, or `None` if not registered.
98    #[must_use]
99    pub fn primitive(&self, name: &str) -> Option<&PrimitiveSpec> {
100        self.primitives
101            .iter()
102            .find(|primitive| primitive.name == name)
103    }
104
105    /// Returns `true` if a primitive with the given name is registered for this profile.
106    #[must_use]
107    pub fn has_primitive(&self, name: &str) -> bool {
108        self.primitive(name).is_some()
109    }
110
111    /// Returns `true` if the given engine patch is active.
112    #[must_use]
113    pub const fn has_patch(&self, patch: EnginePatch) -> bool {
114        self.patches.contains(patch)
115    }
116
117    /// Returns `true` if the XeTeX patch is active.
118    #[must_use]
119    pub const fn is_xetex(&self) -> bool {
120        self.has_patch(EnginePatch::Xetex)
121    }
122}
123
124/// Translation target for Web2C/C2Rust code with profile differences applied as patches.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub struct TexCore {
127    profile: ProfileId,
128    kind: EngineKind,
129    primitives: &'static [PrimitiveSpec],
130    catcodes: CatcodeDefaults,
131    mathcodes: MathcodeDefaults,
132    registers: RegisterDefaults,
133    fonts: FontSemantics,
134    extensions: ExtensionPolicy,
135    patches: EnginePatches,
136}
137
138impl TexCore {
139    /// Constructs a plain TeX `TexCore` with no patches applied.
140    #[must_use]
141    pub const fn tex(profile: ProfileId) -> Self {
142        Self {
143            profile,
144            kind: EngineKind::Tex,
145            primitives: TEX_CORE_PRIMITIVES,
146            catcodes: CatcodeDefaults {
147                unicode_scalars: false,
148            },
149            mathcodes: MathcodeDefaults {
150                unicode_math: false,
151            },
152            registers: RegisterDefaults::tex(),
153            fonts: FontSemantics::tex(),
154            extensions: ExtensionPolicy::tex(),
155            patches: EnginePatches::empty(),
156        }
157    }
158
159    /// Returns a new `TexCore` with the given patch applied, updating all dependent fields.
160    #[must_use]
161    pub const fn with_patch(mut self, patch: EnginePatch) -> Self {
162        self.patches = self.patches.with(patch);
163        match patch {
164            EnginePatch::Etex => {
165                self.kind = EngineKind::Etex;
166                self.primitives = ETEX_PROFILE_PRIMITIVES;
167                self.registers = RegisterDefaults::extended();
168                self.extensions = ExtensionPolicy {
169                    etex: true,
170                    ..self.extensions
171                };
172            }
173            EnginePatch::Xetex => {
174                self.kind = EngineKind::Xetex;
175                self.primitives = XETEX_PROFILE_PRIMITIVES;
176                self.catcodes = CatcodeDefaults {
177                    unicode_scalars: true,
178                };
179                self.mathcodes = MathcodeDefaults { unicode_math: true };
180                self.registers = RegisterDefaults::extended();
181                self.fonts = FontSemantics {
182                    unicode_fonts: true,
183                    shaped_text: true,
184                    unicode_math_fonts: true,
185                    host_native_fonts: false,
186                };
187                self.extensions = ExtensionPolicy {
188                    etex: true,
189                    xetex: true,
190                };
191            }
192        }
193        self
194    }
195
196    /// Returns the profile identifier.
197    #[must_use]
198    pub const fn profile(&self) -> ProfileId {
199        self.profile
200    }
201
202    /// Returns the engine kind.
203    #[must_use]
204    pub const fn kind(&self) -> EngineKind {
205        self.kind
206    }
207
208    /// Returns the primitive list for this core.
209    #[must_use]
210    pub const fn primitives(&self) -> &'static [PrimitiveSpec] {
211        self.primitives
212    }
213
214    /// Returns the category code defaults.
215    #[must_use]
216    pub const fn catcode_defaults(&self) -> CatcodeDefaults {
217        self.catcodes
218    }
219
220    /// Returns the math code defaults.
221    #[must_use]
222    pub const fn mathcode_defaults(&self) -> MathcodeDefaults {
223        self.mathcodes
224    }
225
226    /// Returns the register count defaults.
227    #[must_use]
228    pub const fn register_defaults(&self) -> RegisterDefaults {
229        self.registers
230    }
231
232    /// Returns the font semantics.
233    #[must_use]
234    pub const fn font_semantics(&self) -> FontSemantics {
235        self.fonts
236    }
237
238    /// Returns the extension policy.
239    #[must_use]
240    pub const fn extension_policy(&self) -> ExtensionPolicy {
241        self.extensions
242    }
243
244    /// Returns the active patch flags.
245    #[must_use]
246    pub const fn patches(&self) -> EnginePatches {
247        self.patches
248    }
249
250    /// Returns `true` if the given patch is active on this core.
251    #[must_use]
252    pub const fn has_patch(&self, patch: EnginePatch) -> bool {
253        self.patches.contains(patch)
254    }
255}
256
257/// An individual capability patch that can be layered onto a `TexCore`.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259#[non_exhaustive]
260pub enum EnginePatch {
261    /// eTeX register and primitive extensions.
262    Etex,
263    /// XeTeX Unicode and font extensions, which also imply eTeX behavior.
264    Xetex,
265}
266
267/// Bit-set recording which engine patches are active on a core.
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub struct EnginePatches {
270    etex: bool,
271    xetex: bool,
272}
273
274impl EnginePatches {
275    /// Returns an empty patch set with no patches active.
276    #[must_use]
277    pub const fn empty() -> Self {
278        Self {
279            etex: false,
280            xetex: false,
281        }
282    }
283
284    /// Returns a new `EnginePatches` with the given patch added.
285    #[must_use]
286    pub const fn with(mut self, patch: EnginePatch) -> Self {
287        match patch {
288            EnginePatch::Etex => {
289                self.etex = true;
290            }
291            EnginePatch::Xetex => {
292                self.etex = true;
293                self.xetex = true;
294            }
295        }
296        self
297    }
298
299    /// Derives an `EnginePatches` value from an `ExtensionPolicy`.
300    #[must_use]
301    pub const fn from_extension_policy(policy: ExtensionPolicy) -> Self {
302        Self {
303            etex: policy.etex || policy.xetex,
304            xetex: policy.xetex,
305        }
306    }
307
308    /// Returns `true` if the given patch is present in this set.
309    #[must_use]
310    pub const fn contains(&self, patch: EnginePatch) -> bool {
311        match patch {
312            EnginePatch::Etex => self.etex,
313            EnginePatch::Xetex => self.xetex,
314        }
315    }
316}
317
318/// Primitive descriptor; behavior is in the engine core.
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub struct PrimitiveSpec {
321    /// Control sequence name without the leading backslash.
322    pub name: Cow<'static, str>,
323    /// Opcode used for dispatch in the translated engine.
324    pub opcode: PrimitiveOpcode,
325    /// Coarse category of this primitive's runtime behavior.
326    pub kind: PrimitiveKind,
327}
328
329/// Integer opcode used to dispatch a primitive in the translated engine.
330#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
331pub struct PrimitiveOpcode(pub u32);
332
333/// Coarse category of a primitive's runtime behavior.
334#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
335#[non_exhaustive]
336pub enum PrimitiveKind {
337    /// Primitive that expands during tokenization.
338    Expandable,
339    /// Primitive that performs a variable or register assignment.
340    Assignment,
341    /// Math mode primitive.
342    Math,
343    /// Box and list construction.
344    Layout,
345    /// Resource loading such as \\input.
346    Resource,
347    /// XeTeX extension primitive.
348    Extension,
349    #[default]
350    /// Catch-all for primitives that do not fit any other category.
351    Other,
352}
353
354/// Default category code configuration for a profile.
355#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
356pub struct CatcodeDefaults {
357    /// Whether scalar Unicode code points above U+00FF are valid character tokens.
358    pub unicode_scalars: bool,
359}
360
361/// Default math code configuration for a profile.
362#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
363pub struct MathcodeDefaults {
364    /// Whether Unicode math code points are used instead of classic 15-bit codes.
365    pub unicode_math: bool,
366}
367
368/// Default register counts for count, dimension, skip, and token registers.
369#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub struct RegisterDefaults {
371    /// Number of count registers available.
372    pub count_registers: u16,
373    /// Number of dimension registers available.
374    pub dimension_registers: u16,
375    /// Number of skip registers available.
376    pub skip_registers: u16,
377    /// Number of token list registers available.
378    pub token_registers: u16,
379}
380
381impl Default for RegisterDefaults {
382    fn default() -> Self {
383        Self::tex()
384    }
385}
386
387impl RegisterDefaults {
388    /// 256 registers each, matching classic TeX limits.
389    #[must_use]
390    pub const fn tex() -> Self {
391        Self {
392            count_registers: 256,
393            dimension_registers: 256,
394            skip_registers: 256,
395            token_registers: 256,
396        }
397    }
398
399    /// 32768 registers each, matching eTeX and XeTeX limits.
400    #[must_use]
401    pub const fn extended() -> Self {
402        Self {
403            count_registers: 32_768,
404            dimension_registers: 32_768,
405            skip_registers: 32_768,
406            token_registers: 32_768,
407        }
408    }
409}
410
411/// Font loading and shaping capabilities for a profile.
412#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
413pub struct FontSemantics {
414    /// Whether Unicode-indexed fonts are supported.
415    pub unicode_fonts: bool,
416    /// Whether shaped text output via a text shaper is enabled.
417    pub shaped_text: bool,
418    /// Whether Unicode math fonts are supported.
419    pub unicode_math_fonts: bool,
420    /// Engine may call host platform font assembly hooks.
421    pub host_native_fonts: bool,
422}
423
424impl FontSemantics {
425    /// Returns plain TeX font semantics with all capabilities disabled.
426    #[must_use]
427    pub const fn tex() -> Self {
428        Self {
429            unicode_fonts: false,
430            shaped_text: false,
431            unicode_math_fonts: false,
432            host_native_fonts: false,
433        }
434    }
435}
436
437/// Which TeX extension families the engine activates.
438#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
439pub struct ExtensionPolicy {
440    /// Whether eTeX extensions are active.
441    pub etex: bool,
442    /// Whether XeTeX extensions are active.
443    pub xetex: bool,
444}
445
446impl ExtensionPolicy {
447    /// Plain TeX: no extension families.
448    #[must_use]
449    pub const fn tex() -> Self {
450        Self {
451            etex: false,
452            xetex: false,
453        }
454    }
455}
456
457const TEX_CORE_PRIMITIVES: &[PrimitiveSpec] = &[
458    PrimitiveSpec {
459        name: Cow::Borrowed("relax"),
460        opcode: PrimitiveOpcode(0),
461        kind: PrimitiveKind::Expandable,
462    },
463    PrimitiveSpec {
464        name: Cow::Borrowed("input"),
465        opcode: PrimitiveOpcode(1),
466        kind: PrimitiveKind::Resource,
467    },
468    PrimitiveSpec {
469        name: Cow::Borrowed("hbox"),
470        opcode: PrimitiveOpcode(2),
471        kind: PrimitiveKind::Layout,
472    },
473    PrimitiveSpec {
474        name: Cow::Borrowed("vbox"),
475        opcode: PrimitiveOpcode(3),
476        kind: PrimitiveKind::Layout,
477    },
478];
479
480const ETEX_PROFILE_PRIMITIVES: &[PrimitiveSpec] = &[
481    PrimitiveSpec {
482        name: Cow::Borrowed("relax"),
483        opcode: PrimitiveOpcode(0),
484        kind: PrimitiveKind::Expandable,
485    },
486    PrimitiveSpec {
487        name: Cow::Borrowed("input"),
488        opcode: PrimitiveOpcode(1),
489        kind: PrimitiveKind::Resource,
490    },
491    PrimitiveSpec {
492        name: Cow::Borrowed("hbox"),
493        opcode: PrimitiveOpcode(2),
494        kind: PrimitiveKind::Layout,
495    },
496    PrimitiveSpec {
497        name: Cow::Borrowed("vbox"),
498        opcode: PrimitiveOpcode(3),
499        kind: PrimitiveKind::Layout,
500    },
501    PrimitiveSpec {
502        name: Cow::Borrowed("expanded"),
503        opcode: PrimitiveOpcode(100),
504        kind: PrimitiveKind::Expandable,
505    },
506];
507
508const XETEX_PROFILE_PRIMITIVES: &[PrimitiveSpec] = &[
509    PrimitiveSpec {
510        name: Cow::Borrowed("relax"),
511        opcode: PrimitiveOpcode(0),
512        kind: PrimitiveKind::Expandable,
513    },
514    PrimitiveSpec {
515        name: Cow::Borrowed("input"),
516        opcode: PrimitiveOpcode(1),
517        kind: PrimitiveKind::Resource,
518    },
519    PrimitiveSpec {
520        name: Cow::Borrowed("hbox"),
521        opcode: PrimitiveOpcode(2),
522        kind: PrimitiveKind::Layout,
523    },
524    PrimitiveSpec {
525        name: Cow::Borrowed("vbox"),
526        opcode: PrimitiveOpcode(3),
527        kind: PrimitiveKind::Layout,
528    },
529    PrimitiveSpec {
530        name: Cow::Borrowed("expanded"),
531        opcode: PrimitiveOpcode(100),
532        kind: PrimitiveKind::Expandable,
533    },
534    PrimitiveSpec {
535        name: Cow::Borrowed("XeTeXrevision"),
536        opcode: PrimitiveOpcode(200),
537        kind: PrimitiveKind::Extension,
538    },
539    PrimitiveSpec {
540        name: Cow::Borrowed("font"),
541        opcode: PrimitiveOpcode(201),
542        kind: PrimitiveKind::Assignment,
543    },
544];
545
546/// Plain TeX compatible baseline profile.
547#[derive(Clone, Copy, Debug, Default)]
548pub struct TexProfile;
549
550impl EngineProfile for TexProfile {
551    fn id(&self) -> ProfileId {
552        self.core().profile()
553    }
554
555    fn kind(&self) -> EngineKind {
556        self.core().kind()
557    }
558
559    fn primitives(&self) -> &[PrimitiveSpec] {
560        self.core().primitives()
561    }
562
563    fn catcode_defaults(&self) -> CatcodeDefaults {
564        self.core().catcode_defaults()
565    }
566
567    fn mathcode_defaults(&self) -> MathcodeDefaults {
568        self.core().mathcode_defaults()
569    }
570
571    fn register_defaults(&self) -> RegisterDefaults {
572        self.core().register_defaults()
573    }
574
575    fn font_semantics(&self) -> FontSemantics {
576        self.core().font_semantics()
577    }
578
579    fn extension_policy(&self) -> ExtensionPolicy {
580        self.core().extension_policy()
581    }
582}
583
584impl TexProfile {
585    /// Returns the `TexCore` for this profile.
586    #[must_use]
587    pub const fn core(&self) -> TexCore {
588        TexCore::tex(ProfileId("tex"))
589    }
590}
591
592/// eTeX profile used by modern LaTeX formats.
593#[derive(Clone, Copy, Debug, Default)]
594pub struct EtexProfile;
595
596impl EngineProfile for EtexProfile {
597    fn id(&self) -> ProfileId {
598        self.core().profile()
599    }
600
601    fn kind(&self) -> EngineKind {
602        self.core().kind()
603    }
604
605    fn primitives(&self) -> &[PrimitiveSpec] {
606        self.core().primitives()
607    }
608
609    fn catcode_defaults(&self) -> CatcodeDefaults {
610        self.core().catcode_defaults()
611    }
612
613    fn mathcode_defaults(&self) -> MathcodeDefaults {
614        self.core().mathcode_defaults()
615    }
616
617    fn register_defaults(&self) -> RegisterDefaults {
618        self.core().register_defaults()
619    }
620
621    fn font_semantics(&self) -> FontSemantics {
622        self.core().font_semantics()
623    }
624
625    fn extension_policy(&self) -> ExtensionPolicy {
626        self.core().extension_policy()
627    }
628}
629
630impl EtexProfile {
631    /// Returns the `TexCore` for this profile with the eTeX patch applied.
632    #[must_use]
633    pub const fn core(&self) -> TexCore {
634        TexCore::tex(ProfileId("etex")).with_patch(EnginePatch::Etex)
635    }
636}
637
638/// XeTeX profile placeholder.
639#[derive(Clone, Copy, Debug, Default)]
640pub struct XetexProfile;
641
642impl EngineProfile for XetexProfile {
643    fn id(&self) -> ProfileId {
644        self.core().profile()
645    }
646
647    fn kind(&self) -> EngineKind {
648        self.core().kind()
649    }
650
651    fn primitives(&self) -> &[PrimitiveSpec] {
652        self.core().primitives()
653    }
654
655    fn catcode_defaults(&self) -> CatcodeDefaults {
656        self.core().catcode_defaults()
657    }
658
659    fn mathcode_defaults(&self) -> MathcodeDefaults {
660        self.core().mathcode_defaults()
661    }
662
663    fn register_defaults(&self) -> RegisterDefaults {
664        self.core().register_defaults()
665    }
666
667    fn font_semantics(&self) -> FontSemantics {
668        self.core().font_semantics()
669    }
670
671    fn extension_policy(&self) -> ExtensionPolicy {
672        self.core().extension_policy()
673    }
674}
675
676impl XetexProfile {
677    /// Returns the `TexCore` for this profile with eTeX and XeTeX patches applied.
678    #[must_use]
679    pub const fn core(&self) -> TexCore {
680        TexCore::tex(ProfileId("xetex"))
681            .with_patch(EnginePatch::Etex)
682            .with_patch(EnginePatch::Xetex)
683    }
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn xetex_profile_declares_unicode_font_and_extension_semantics() {
692        let profile = XetexProfile;
693
694        assert_eq!(profile.kind(), EngineKind::Xetex);
695        assert!(profile.font_semantics().unicode_fonts);
696        assert!(profile.font_semantics().shaped_text);
697        assert!(profile.font_semantics().unicode_math_fonts);
698        assert!(!profile.font_semantics().host_native_fonts);
699        assert!(profile.extension_policy().etex);
700        assert!(profile.extension_policy().xetex);
701        assert!(profile.register_defaults().count_registers > 256);
702    }
703
704    #[test]
705    fn runtime_semantics_resolve_profile_gated_primitives() {
706        let tex = EngineSemantics::from_profile(&TexProfile);
707        let xetex = EngineSemantics::from_profile(&XetexProfile);
708
709        assert!(tex.has_primitive("input"));
710        assert!(xetex.has_primitive("input"));
711        assert!(!tex.has_primitive("XeTeXrevision"));
712        assert!(xetex.has_primitive("XeTeXrevision"));
713        assert_eq!(
714            xetex
715                .primitive("XeTeXrevision")
716                .expect("xetex primitive")
717                .kind,
718            PrimitiveKind::Extension
719        );
720    }
721
722    #[test]
723    fn tex_and_xetex_profiles_share_the_same_core_before_patches() {
724        let tex = TexProfile.core();
725        let xetex = XetexProfile.core();
726
727        assert_eq!(tex.profile(), ProfileId("tex"));
728        assert_eq!(xetex.profile(), ProfileId("xetex"));
729        assert!(!tex.has_patch(EnginePatch::Etex));
730        assert!(!tex.has_patch(EnginePatch::Xetex));
731        assert!(xetex.has_patch(EnginePatch::Etex));
732        assert!(xetex.has_patch(EnginePatch::Xetex));
733        assert!(xetex.primitives().len() > tex.primitives().len());
734        for primitive in tex.primitives() {
735            assert!(
736                xetex
737                    .primitives()
738                    .iter()
739                    .any(|candidate| candidate.name == primitive.name
740                        && candidate.opcode == primitive.opcode
741                        && candidate.kind == primitive.kind),
742                "xetex should retain TeX core primitive {primitive:?}"
743            );
744        }
745    }
746
747    #[test]
748    fn xetex_patch_adds_unicode_and_font_semantics_conditionally() {
749        let core = TexCore::tex(ProfileId("custom-xetex")).with_patch(EnginePatch::Xetex);
750
751        assert_eq!(core.kind(), EngineKind::Xetex);
752        assert!(core.catcode_defaults().unicode_scalars);
753        assert!(core.mathcode_defaults().unicode_math);
754        assert!(core.font_semantics().unicode_fonts);
755        assert!(core.font_semantics().shaped_text);
756        assert!(!core.font_semantics().host_native_fonts);
757        assert!(core.extension_policy().etex);
758        assert!(core.extension_policy().xetex);
759        assert!(core
760            .primitives()
761            .iter()
762            .any(|primitive| primitive.name == "XeTeXrevision"));
763    }
764}