Skip to main content

bcgen_lang/
options.rs

1//! Compilation options and source locations.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use bytecode_lang::Hook;
7use hir_lang::Span;
8
9use crate::intrinsics::Intrinsics;
10
11/// How arrays and maps behave when a value is copied.
12///
13/// # Examples
14///
15/// ```
16/// use bcgen_lang::ArraySemantics;
17///
18/// assert_eq!(ArraySemantics::default(), ArraySemantics::Php);
19/// ```
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum ArraySemantics {
22    /// PHP arrays: values. Assigning, passing, returning, capturing, or
23    /// storing an array copies it (an O(1) copy-on-write `dup`), a nested
24    /// write (`$a['x']['y'] = 1`) separates each intermediate array from any
25    /// copy that shares it, and writing through `null` creates the array
26    /// (auto-vivification).
27    #[default]
28    Php,
29    /// Shared references, as in Python, JavaScript, and Ruby: a copy is the
30    /// same object, nested writes go through, and writing into `null` is an
31    /// error.
32    Reference,
33}
34
35/// How map keys are normalized before a map is indexed.
36///
37/// # Examples
38///
39/// ```
40/// use bcgen_lang::KeyCoercion;
41///
42/// assert_eq!(KeyCoercion::default(), KeyCoercion::Php);
43/// ```
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum KeyCoercion {
46    /// PHP's array-key rules: a string holding a canonical decimal `i64`
47    /// (`"7"`, `"-3"`, not `"07"`, `"-0"`, or `"+1"`) becomes that integer, a
48    /// float is truncated toward zero (saturating at the `i64` range; NaN
49    /// gives `0`), `true`/`false` become `1`/`0`, and `null` becomes `""`.
50    /// Other keys are left as they are.
51    #[default]
52    Php,
53    /// No normalization: keys compare by LSB's rules (`1` and `1.0` differ).
54    None,
55}
56
57/// What the module body (`Module.body`) compiles to.
58///
59/// # Examples
60///
61/// ```
62/// use bcgen_lang::BodyMode;
63///
64/// assert_eq!(BodyMode::default(), BodyMode::Start);
65/// ```
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
67pub enum BodyMode {
68    /// The body is part of the module's start function, which the VM runs
69    /// once before the first call into the module; its value is kept in a
70    /// global that the entry function returns. Scripts and modules.
71    #[default]
72    Start,
73    /// The body is the entry function itself and returns its value on every
74    /// call; the start function only initializes globals. REPL entries and
75    /// tests that run the body more than once.
76    Entry,
77}
78
79/// Compilation options: the dynamic profile's array and key semantics, the
80/// body's role, host linkage, the intrinsic table, and budgets.
81///
82/// The default is the PHP profile Mox needs: value arrays and PHP keys.
83///
84/// # Examples
85///
86/// ```
87/// use bcgen_lang::{ArraySemantics, Options};
88///
89/// let opts = Options::new()
90///     .with_arrays(ArraySemantics::Reference)
91///     .with_file("app.mox")
92///     .with_max_instructions(1_000_000);
93/// assert_eq!(opts.arrays(), ArraySemantics::Reference);
94/// assert_eq!(opts.file(), "app.mox");
95/// ```
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct Options {
98    arrays: ArraySemantics,
99    keys: KeyCoercion,
100    body: BodyMode,
101    file: String,
102    host_module: String,
103    hooks: Vec<(Hook, String, String)>,
104    intrinsics: Intrinsics,
105    max_instructions: u64,
106    ignore_extra: bool,
107    string_offsets: Option<String>,
108    constructor: Option<String>,
109    destructor: Option<String>,
110    clone_method: Option<String>,
111    reference_counting: bool,
112}
113
114impl Default for Options {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl Options {
121    /// The default budget: 64 Mi instructions per module.
122    pub const DEFAULT_MAX_INSTRUCTIONS: u64 = 64 << 20;
123
124    /// The PHP profile: [`ArraySemantics::Php`], [`KeyCoercion::Php`],
125    /// [`BodyMode::Start`], file name `<hir>`, host module `host`, no hooks,
126    /// the built-in intrinsics ([`Intrinsics::core`]), extra arguments
127    /// refused, string offsets read as bytes, and PHP's magic method names
128    /// (`__construct`, `__destruct`, `__clone`).
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use bcgen_lang::{BodyMode, Options};
134    ///
135    /// assert_eq!(Options::new().body(), BodyMode::Start);
136    /// assert_eq!(Options::new().host_module(), "host");
137    /// ```
138    #[must_use]
139    pub fn new() -> Self {
140        Self {
141            arrays: ArraySemantics::Php,
142            keys: KeyCoercion::Php,
143            body: BodyMode::Start,
144            file: String::from("<hir>"),
145            host_module: String::from("host"),
146            hooks: Vec::new(),
147            intrinsics: Intrinsics::core(),
148            max_instructions: Self::DEFAULT_MAX_INSTRUCTIONS,
149            ignore_extra: false,
150            string_offsets: None,
151            constructor: Some(String::from("__construct")),
152            destructor: Some(String::from("__destruct")),
153            clone_method: Some(String::from("__clone")),
154            reference_counting: false,
155        }
156    }
157
158    /// Sets the array semantics.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use bcgen_lang::{ArraySemantics, Options};
164    ///
165    /// let o = Options::new().with_arrays(ArraySemantics::Reference);
166    /// assert_eq!(o.arrays(), ArraySemantics::Reference);
167    /// ```
168    #[must_use]
169    pub fn with_arrays(mut self, arrays: ArraySemantics) -> Self {
170        self.arrays = arrays;
171        self
172    }
173
174    /// Sets the key coercion.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use bcgen_lang::{KeyCoercion, Options};
180    ///
181    /// assert_eq!(Options::new().with_keys(KeyCoercion::None).keys(), KeyCoercion::None);
182    /// ```
183    #[must_use]
184    pub fn with_keys(mut self, keys: KeyCoercion) -> Self {
185        self.keys = keys;
186        self
187    }
188
189    /// Sets what the module body compiles to.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use bcgen_lang::{BodyMode, Options};
195    ///
196    /// assert_eq!(Options::new().with_body(BodyMode::Entry).body(), BodyMode::Entry);
197    /// ```
198    #[must_use]
199    pub fn with_body(mut self, body: BodyMode) -> Self {
200        self.body = body;
201        self
202    }
203
204    /// Sets the file name written into line-table rows when no
205    /// [`Locate`] is given.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use bcgen_lang::Options;
211    ///
212    /// assert_eq!(Options::new().with_file("main.mox").file(), "main.mox");
213    /// ```
214    #[must_use]
215    pub fn with_file(mut self, file: &str) -> Self {
216        self.file = String::from(file);
217        self
218    }
219
220    /// Sets the import module for host symbols that have no `.` in their
221    /// name (`echo` imports as `host`.`echo` by default; `mox.concat` imports
222    /// as `mox`.`concat` whatever this is).
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// use bcgen_lang::Options;
228    ///
229    /// assert_eq!(Options::new().with_host_module("env").host_module(), "env");
230    /// ```
231    #[must_use]
232    pub fn with_host_module(mut self, module: &str) -> Self {
233        self.host_module = String::from(module);
234        self
235    }
236
237    /// Binds an LSB hook (§5.8) to the host function `module`.`name`, with
238    /// the hook's signature (every operand `dyn`). A later binding of the same
239    /// hook replaces an earlier one.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use bcgen_lang::Options;
245    /// use bytecode_lang::Hook;
246    ///
247    /// let o = Options::new().with_hook(Hook::Eq, "mox", "loose_eq");
248    /// assert_eq!(o.hooks().len(), 1);
249    /// ```
250    #[must_use]
251    pub fn with_hook(mut self, hook: Hook, module: &str, name: &str) -> Self {
252        self.hooks.retain(|(h, _, _)| *h != hook);
253        self.hooks
254            .push((hook, String::from(module), String::from(name)));
255        self
256    }
257
258    /// Sets the intrinsic table: how each named intrinsic compiles (an LSB
259    /// sequence, a host function, or a guarded fast path over one). Names
260    /// not in it do not compile
261    /// ([`CompileError::UnknownIntrinsic`](crate::CompileError::UnknownIntrinsic)).
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use bcgen_lang::{Intrinsics, Options};
267    ///
268    /// let o = Options::new().with_intrinsics(Intrinsics::mox());
269    /// assert!(o.intrinsics().get("mox.loose_eq").is_some());
270    /// ```
271    #[must_use]
272    pub fn with_intrinsics(mut self, intrinsics: Intrinsics) -> Self {
273        self.intrinsics = intrinsics;
274        self
275    }
276
277    /// Sets the instruction budget for the whole module; compilation stops
278    /// with [`CompileError::BudgetExceeded`](crate::CompileError) beyond it.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use bcgen_lang::Options;
284    ///
285    /// assert_eq!(Options::new().with_max_instructions(10).max_instructions(), 10);
286    /// ```
287    #[must_use]
288    pub fn with_max_instructions(mut self, limit: u64) -> Self {
289        self.max_instructions = limit;
290        self
291    }
292
293    /// Whether user functions, closures, and methods ignore extra positional
294    /// arguments (PHP: `f(1, 2, 3)` of `function f($a)` drops the extras
295    /// after evaluating them) instead of raising `ArgumentError` (E0114,
296    /// Python's rule and the default). Sets LSB's `ignore_extra` on every
297    /// parameter list the module carries, and direct calls evaluate and
298    /// discard the extras. Named arguments that match no parameter are an
299    /// error either way.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use bcgen_lang::Options;
305    ///
306    /// assert!(Options::new().with_ignore_extra(true).ignore_extra());
307    /// assert!(!Options::new().ignore_extra());
308    /// ```
309    #[must_use]
310    pub fn with_ignore_extra(mut self, ignore: bool) -> Self {
311        self.ignore_extra = ignore;
312        self
313    }
314
315    /// Routes **string-offset reads** (`$s[i]` where `$s` holds a string)
316    /// through the intrinsic `name` of the [intrinsic table](Self::intrinsics),
317    /// called with the string and the (coerced) key; every other indexed read
318    /// stays LSB's `dget_index`. Without it, a string offset reads LSB's
319    /// answer: the byte as an `int` (LSB §5.7). PHP gives a one-character
320    /// string, which [`Fast::StrOffset`](crate::Fast::StrOffset) computes
321    /// inline (`Intrinsics::mox()` maps `mox.str_offset` to it). The entry
322    /// must exist and take two operands, or compilation fails with
323    /// [`CompileError::InvalidOption`](crate::CompileError::InvalidOption).
324    /// String writes (`$s[0] = "x"`) are unchanged: LSB strings are
325    /// immutable, so they go to the `set_index` hook.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use bcgen_lang::{Intrinsics, Options};
331    ///
332    /// let o = Options::new()
333    ///     .with_intrinsics(Intrinsics::mox())
334    ///     .with_string_offsets("mox.str_offset");
335    /// assert_eq!(o.string_offsets(), Some("mox.str_offset"));
336    /// ```
337    #[must_use]
338    pub fn with_string_offsets(mut self, name: &str) -> Self {
339        self.string_offsets = Some(String::from(name));
340        self
341    }
342
343    /// Names the constructor method `new` calls after initializing the
344    /// instance's properties (`None`: classes have no constructor, and `new`
345    /// evaluates and drops its arguments).
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use bcgen_lang::Options;
351    ///
352    /// assert_eq!(Options::new().constructor(), Some("__construct"));
353    /// assert_eq!(Options::new().with_constructor(Some("__init__")).constructor(), Some("__init__"));
354    /// ```
355    #[must_use]
356    pub fn with_constructor(mut self, name: Option<&str>) -> Self {
357        self.constructor = name.map(String::from);
358        self
359    }
360
361    /// Names the destructor method. bcgen-lang does not call it: LSB format
362    /// 2 has no destructor flag, so the host passes
363    /// [`Compiled::destructor`](crate::Compiled::destructor) to bvm-lang's
364    /// `Vm::set_destructor`. A class method of this name must take no
365    /// parameters ([`ClassProblem::BadDestructor`](crate::ClassProblem::BadDestructor)).
366    ///
367    /// # Examples
368    ///
369    /// ```
370    /// use bcgen_lang::Options;
371    ///
372    /// assert_eq!(Options::new().destructor(), Some("__destruct"));
373    /// assert_eq!(Options::new().with_destructor(None).destructor(), None);
374    /// ```
375    #[must_use]
376    pub fn with_destructor(mut self, name: Option<&str>) -> Self {
377        self.destructor = name.map(String::from);
378        self
379    }
380
381    /// Emits code for a VM that counts references (bvm-lang's
382    /// `Memory::Rc`, the language's `[runtime] memory = "rc"`), so objects
383    /// die where PHP frees them: every temporary register and call window
384    /// that may hold a reference is cleared (`load_nil`) when it dies, and a
385    /// variable keeps its register until the function returns (PHP
386    /// variables are function-scoped). Without it, a dead temporary keeps
387    /// its object alive until the register is reused, which delays
388    /// destructors. The cost is one `load_nil` per dying reference
389    /// register; nothing changes under a tracing collector.
390    ///
391    /// # Examples
392    ///
393    /// ```
394    /// use bcgen_lang::Options;
395    ///
396    /// assert!(Options::new().with_reference_counting(true).reference_counting());
397    /// assert!(!Options::new().reference_counting());
398    /// ```
399    #[must_use]
400    pub fn with_reference_counting(mut self, rc: bool) -> Self {
401        self.reference_counting = rc;
402        self
403    }
404
405    /// Names the method a clone ([`ClassOp::Clone`](crate::ClassOp::Clone))
406    /// calls on the new copy (PHP's `__clone`), when the class has one.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use bcgen_lang::Options;
412    ///
413    /// assert_eq!(Options::new().clone_method(), Some("__clone"));
414    /// ```
415    #[must_use]
416    pub fn with_clone_method(mut self, name: Option<&str>) -> Self {
417        self.clone_method = name.map(String::from);
418        self
419    }
420
421    /// The array semantics.
422    ///
423    /// # Examples
424    ///
425    /// ```
426    /// assert_eq!(bcgen_lang::Options::new().arrays(), bcgen_lang::ArraySemantics::Php);
427    /// ```
428    #[must_use]
429    pub fn arrays(&self) -> ArraySemantics {
430        self.arrays
431    }
432
433    /// The key coercion.
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// assert_eq!(bcgen_lang::Options::new().keys(), bcgen_lang::KeyCoercion::Php);
439    /// ```
440    #[must_use]
441    pub fn keys(&self) -> KeyCoercion {
442        self.keys
443    }
444
445    /// What the module body compiles to.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// assert_eq!(bcgen_lang::Options::new().body(), bcgen_lang::BodyMode::Start);
451    /// ```
452    #[must_use]
453    pub fn body(&self) -> BodyMode {
454        self.body
455    }
456
457    /// The default file name for line-table rows.
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// assert_eq!(bcgen_lang::Options::new().file(), "<hir>");
463    /// ```
464    #[must_use]
465    pub fn file(&self) -> &str {
466        &self.file
467    }
468
469    /// The import module for host symbols without a `.`.
470    ///
471    /// # Examples
472    ///
473    /// ```
474    /// assert_eq!(bcgen_lang::Options::new().host_module(), "host");
475    /// ```
476    #[must_use]
477    pub fn host_module(&self) -> &str {
478        &self.host_module
479    }
480
481    /// The hook bindings, as (hook, module, name).
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// assert!(bcgen_lang::Options::new().hooks().is_empty());
487    /// ```
488    #[must_use]
489    pub fn hooks(&self) -> &[(Hook, String, String)] {
490        &self.hooks
491    }
492
493    /// The intrinsic table.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// assert_eq!(bcgen_lang::Options::new().intrinsics(), &bcgen_lang::Intrinsics::core());
499    /// ```
500    #[must_use]
501    pub fn intrinsics(&self) -> &Intrinsics {
502        &self.intrinsics
503    }
504
505    /// The instruction budget.
506    ///
507    /// # Examples
508    ///
509    /// ```
510    /// use bcgen_lang::Options;
511    ///
512    /// assert_eq!(Options::new().max_instructions(), Options::DEFAULT_MAX_INSTRUCTIONS);
513    /// ```
514    #[must_use]
515    pub fn max_instructions(&self) -> u64 {
516        self.max_instructions
517    }
518
519    /// Whether extra positional arguments are ignored (see
520    /// [`with_ignore_extra`](Self::with_ignore_extra)).
521    ///
522    /// # Examples
523    ///
524    /// ```
525    /// assert!(!bcgen_lang::Options::new().ignore_extra());
526    /// ```
527    #[must_use]
528    pub fn ignore_extra(&self) -> bool {
529        self.ignore_extra
530    }
531
532    /// The intrinsic string-offset reads go through, if any (see
533    /// [`with_string_offsets`](Self::with_string_offsets)).
534    ///
535    /// # Examples
536    ///
537    /// ```
538    /// assert_eq!(bcgen_lang::Options::new().string_offsets(), None);
539    /// ```
540    #[must_use]
541    pub fn string_offsets(&self) -> Option<&str> {
542        self.string_offsets.as_deref()
543    }
544
545    /// The constructor method name.
546    ///
547    /// # Examples
548    ///
549    /// See [`with_constructor`](Self::with_constructor).
550    #[must_use]
551    pub fn constructor(&self) -> Option<&str> {
552        self.constructor.as_deref()
553    }
554
555    /// The destructor method name.
556    ///
557    /// # Examples
558    ///
559    /// See [`with_destructor`](Self::with_destructor).
560    #[must_use]
561    pub fn destructor(&self) -> Option<&str> {
562        self.destructor.as_deref()
563    }
564
565    /// Whether code is emitted for a reference-counting VM (see
566    /// [`with_reference_counting`](Self::with_reference_counting)).
567    ///
568    /// # Examples
569    ///
570    /// See [`with_reference_counting`](Self::with_reference_counting).
571    #[must_use]
572    pub fn reference_counting(&self) -> bool {
573        self.reference_counting
574    }
575
576    /// The clone method name.
577    ///
578    /// # Examples
579    ///
580    /// See [`with_clone_method`](Self::with_clone_method).
581    #[must_use]
582    pub fn clone_method(&self) -> Option<&str> {
583        self.clone_method.as_deref()
584    }
585}
586
587/// A source position for a line-table row: file, line, and column, numbered
588/// as the [`Locate`] implementation numbers them.
589///
590/// # Examples
591///
592/// ```
593/// use bcgen_lang::Location;
594///
595/// let at = Location { file: "a.mox", line: 3, column: 7 };
596/// assert_eq!(at.line, 3);
597/// ```
598#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
599pub struct Location<'a> {
600    /// The file name.
601    pub file: &'a str,
602    /// The line.
603    pub line: u32,
604    /// The column.
605    pub column: u32,
606}
607
608/// Maps HIR spans to source positions for LSB line tables.
609///
610/// HIR spans are byte ranges in a session-wide byte space; only the host
611/// (typically a source-lang `SourceMap`) knows the files behind them. Without
612/// a locator, rows carry the [`Options::file`] name, line `0` ("unknown"),
613/// and the span's start byte offset as the column. The exact HIR node of
614/// every instruction is always available from [`Compiled::origin`].
615///
616/// [`Compiled::origin`]: crate::Compiled::origin
617///
618/// # Examples
619///
620/// ```
621/// use bcgen_lang::{Locate, Location};
622/// use hir_lang::Span;
623///
624/// struct OneLine;
625/// impl Locate for OneLine {
626///     fn locate(&self, span: Span) -> Location<'_> {
627///         Location { file: "x", line: 1, column: span.start().to_u32() + 1 }
628///     }
629/// }
630/// assert_eq!(OneLine.locate(Span::new(4, 5)).column, 5);
631/// ```
632pub trait Locate {
633    /// The position of `span`'s start.
634    fn locate(&self, span: Span) -> Location<'_>;
635}
636
637/// A [`Locate`] for one source file: 1-based lines and byte columns from the
638/// file's text, for spans that start at `base` in the session byte space.
639///
640/// # Examples
641///
642/// ```
643/// use bcgen_lang::{LineIndex, Locate};
644/// use hir_lang::Span;
645///
646/// let lines = LineIndex::new("app.mox", 0, "a = 1;\nb = 2;\n");
647/// let at = lines.locate(Span::new(7, 8));
648/// assert_eq!((at.file, at.line, at.column), ("app.mox", 2, 1));
649/// ```
650#[derive(Clone, Debug, PartialEq, Eq)]
651pub struct LineIndex {
652    file: String,
653    base: u32,
654    starts: Vec<u32>,
655}
656
657impl LineIndex {
658    /// Indexes `text`, whose first byte is at offset `base` of the session
659    /// byte space.
660    ///
661    /// # Examples
662    ///
663    /// ```
664    /// use bcgen_lang::LineIndex;
665    ///
666    /// assert_eq!(LineIndex::new("f", 100, "x\ny").lines(), 2);
667    /// ```
668    #[must_use]
669    pub fn new(file: &str, base: u32, text: &str) -> Self {
670        let mut starts = Vec::with_capacity(text.len() / 32 + 1);
671        starts.push(0);
672        for (i, b) in text.bytes().enumerate() {
673            if b == b'\n' {
674                starts.push(u32::try_from(i + 1).unwrap_or(u32::MAX));
675            }
676        }
677        Self {
678            file: String::from(file),
679            base,
680            starts,
681        }
682    }
683
684    /// The number of lines.
685    ///
686    /// # Examples
687    ///
688    /// ```
689    /// assert_eq!(bcgen_lang::LineIndex::new("f", 0, "").lines(), 1);
690    /// ```
691    #[must_use]
692    pub fn lines(&self) -> usize {
693        self.starts.len()
694    }
695}
696
697impl Locate for LineIndex {
698    fn locate(&self, span: Span) -> Location<'_> {
699        let offset = span.start().to_u32().saturating_sub(self.base);
700        // The last line start at or before the offset; offsets past the end
701        // land on the last line.
702        let line = self.starts.partition_point(|&s| s <= offset).max(1);
703        let start = self.starts.get(line - 1).copied().unwrap_or(0);
704        Location {
705            file: &self.file,
706            line: u32::try_from(line).unwrap_or(u32::MAX),
707            column: offset - start + 1,
708        }
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::{LineIndex, Locate, Options};
715    use hir_lang::Span;
716
717    #[test]
718    fn line_index_counts_from_the_base() {
719        let l = LineIndex::new("f", 10, "ab\ncd\n\nx");
720        let at = |o: u32| {
721            let p = l.locate(Span::new(o, o));
722            (p.line, p.column)
723        };
724        assert_eq!(at(10), (1, 1));
725        assert_eq!(at(12), (1, 3));
726        assert_eq!(at(13), (2, 1));
727        assert_eq!(at(16), (3, 1));
728        assert_eq!(at(17), (4, 1));
729        // Before the base clamps to the start; past the end stays on the last line.
730        assert_eq!(at(0), (1, 1));
731        assert_eq!(at(1_000), (4, 984));
732    }
733
734    #[test]
735    fn a_later_hook_binding_replaces_an_earlier_one() {
736        let o = Options::new()
737            .with_hook(bytecode_lang::Hook::Eq, "a", "b")
738            .with_hook(bytecode_lang::Hook::Eq, "c", "d");
739        assert_eq!(o.hooks().len(), 1);
740        assert_eq!(o.hooks()[0].1, "c");
741    }
742}