Skip to main content

bcgen_lang/
intrinsics.rs

1//! The intrinsic table (LexerSketch decision D27): how each named intrinsic
2//! (`Intrinsic::Named` in HIR) compiles.
3//!
4//! A language's lowering turns operators whose semantics are not OPS
5//! operations (PHP's loose `==`, `.`, `%`, `**`, casts) into named
6//! intrinsics. The embedder registers each name with one of three
7//! lowerings: an LSB instruction sequence with LSB's own semantics, a call
8//! to a host function the embedder provides to the VM, or a **guarded fast
9//! path**: the operands are tested at run time, the LSB sequence runs where
10//! its result is exactly the language's, and the host function runs
11//! everywhere else. Fast paths never invoke an LSB hook, so their results do
12//! not depend on which hooks a module binds.
13//!
14//! [`Intrinsics::mox`] maps every intrinsic of the Mox sketch; `docs/API.md`
15//! states each mapping and the exact semantics its host function must have.
16
17use alloc::collections::BTreeMap;
18use alloc::string::String;
19use core::fmt;
20
21/// How many operands an intrinsic takes: from [`min`](Arity::min) to
22/// [`max`](Arity::max), both inclusive (at most 255, an import's limit).
23///
24/// # Examples
25///
26/// ```
27/// use bcgen_lang::Arity;
28///
29/// assert!(Arity::exact(2).accepts(2));
30/// assert!(!Arity::exact(2).accepts(3));
31/// assert!(Arity::at_least(1).accepts(255));
32/// assert_eq!(Arity::at_least(1).to_string(), "1 to 255");
33/// ```
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub struct Arity {
36    min: u8,
37    max: u8,
38}
39
40impl Arity {
41    /// Exactly `n` operands.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// assert_eq!(bcgen_lang::Arity::exact(1).max(), 1);
47    /// ```
48    #[must_use]
49    pub const fn exact(n: u8) -> Self {
50        Arity { min: n, max: n }
51    }
52
53    /// From `min` to `max` operands (a `max` below `min` is raised to it).
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use bcgen_lang::Arity;
59    ///
60    /// assert_eq!(Arity::range(2, 1), Arity::exact(2));
61    /// ```
62    #[must_use]
63    pub const fn range(min: u8, max: u8) -> Self {
64        Arity {
65            min,
66            max: if max < min { min } else { max },
67        }
68    }
69
70    /// `n` operands or more, up to 255.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// assert_eq!(bcgen_lang::Arity::at_least(0).max(), 255);
76    /// ```
77    #[must_use]
78    pub const fn at_least(n: u8) -> Self {
79        Arity::range(n, u8::MAX)
80    }
81
82    /// The fewest operands.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// assert_eq!(bcgen_lang::Arity::exact(3).min(), 3);
88    /// ```
89    #[must_use]
90    pub const fn min(self) -> u8 {
91        self.min
92    }
93
94    /// The most operands.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// assert_eq!(bcgen_lang::Arity::exact(3).max(), 3);
100    /// ```
101    #[must_use]
102    pub const fn max(self) -> u8 {
103        self.max
104    }
105
106    /// Whether `n` operands are accepted.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// assert!(!bcgen_lang::Arity::range(1, 2).accepts(0));
112    /// ```
113    #[must_use]
114    pub const fn accepts(self, n: usize) -> bool {
115        n >= self.min as usize && n <= self.max as usize
116    }
117}
118
119impl fmt::Display for Arity {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        if self.min == self.max {
122            write!(f, "{}", self.min)
123        } else {
124            write!(f, "{} to {}", self.min, self.max)
125        }
126    }
127}
128
129/// An LSB instruction sequence with LSB's own semantics, hooks included
130/// (LSB §5.6–§5.10). These are the 0.2 built-in intrinsics.
131///
132/// # Examples
133///
134/// ```
135/// use bcgen_lang::{Arity, LsbOp};
136///
137/// assert_eq!(LsbOp::Concat.arity(), Arity::exact(2));
138/// ```
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
140#[non_exhaustive]
141pub enum LsbOp {
142    /// `dconcat`: two strings concatenated; other kinds go to the `concat`
143    /// hook, else `TypeError`.
144    Concat,
145    /// `dlen`, as a dynamic `int`: string bytes or collection entries; else
146    /// the `len` hook.
147    Len,
148    /// `dtruthy`, as a dynamic `bool` (the `truthy` hook for non-scalars).
149    Truthy,
150    /// `dnot`, as a dynamic `bool`: the negated truthiness.
151    Not,
152    /// `type_of`, as a dynamic `int`: the LSB kind code (LSB §2.2).
153    TypeOf,
154    /// `diter_new` (after a copy-on-write `dup` under PHP arrays, so the
155    /// loop iterates a snapshot): an iterator over a collection or
156    /// coroutine.
157    Iter,
158    /// One `iter_next`: `nil` when the iterator is done, else the pair
159    /// `[key, value]` as a new array.
160    IterNext,
161    /// `resume coro, value`: the value the coroutine (operand 1) yields,
162    /// awaits, or returns next, after resuming it with operand 2 (PHP
163    /// `Generator::send`, Python `send`).
164    Resume,
165    /// `resume_throw coro, error`: as [`Resume`](LsbOp::Resume), raising
166    /// operand 2 at the suspension point (PHP `Generator::throw`).
167    ResumeThrow,
168    /// `coro_close coro, signal`: closes the coroutine, running its pending
169    /// `finally` blocks (LSB §5.13 rule 11).
170    CoroClose,
171    /// `coro_key`: the key of the value most recently yielded (PHP
172    /// `Generator::key`).
173    CoroKey,
174    /// `coro_result`: a returned coroutine's return value (PHP
175    /// `Generator::getReturn`).
176    CoroResult,
177    /// `coro_status`, as a dynamic `int` (0 created, 1 running, 2 yielded,
178    /// 3 awaiting, 4 returned, 5 failed).
179    CoroStatus,
180    /// `err_code`, as a dynamic `int`: the code of a caught runtime error
181    /// value (`200` for `NoMatch`), else 0.
182    ErrCode,
183    /// `err_payload`: the payload of a caught runtime error value (the
184    /// unmatched value of a `NoMatch`), else nil.
185    ErrPayload,
186}
187
188impl LsbOp {
189    /// The operands the op takes.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// assert_eq!(bcgen_lang::LsbOp::Len.arity().max(), 1);
195    /// ```
196    #[must_use]
197    pub const fn arity(self) -> Arity {
198        match self {
199            Self::Concat | Self::Resume | Self::ResumeThrow | Self::CoroClose => Arity::exact(2),
200            Self::Len
201            | Self::Truthy
202            | Self::Not
203            | Self::TypeOf
204            | Self::Iter
205            | Self::IterNext
206            | Self::CoroKey
207            | Self::CoroResult
208            | Self::CoroStatus
209            | Self::ErrCode
210            | Self::ErrPayload => Arity::exact(1),
211        }
212    }
213}
214
215/// A guarded fast path: the operand kinds where an LSB sequence computes
216/// exactly the intrinsic's result, tested at run time; outside that domain
217/// the intrinsic's host function is called with the same operands.
218///
219/// Each variant states its **domain** (where the inline code runs) and what
220/// it computes there. The domains are chosen so that the inline result is
221/// PHP 8's (Mox's) result; a host function bound to the same intrinsic must
222/// agree with the fast path on its domain, and is free everywhere else.
223/// Kinds are LSB dynamic kinds (§2.2): `nil`, `bool`, `int` (i64), `float`
224/// (f64), `str` (bytes), `map`.
225///
226/// # Examples
227///
228/// ```
229/// use bcgen_lang::{Arity, Fast};
230///
231/// assert_eq!(Fast::LooseEq.arity(), Arity::exact(2));
232/// assert_eq!(Fast::ConcatN.arity(), Arity::at_least(0));
233/// ```
234#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
235#[non_exhaustive]
236pub enum Fast {
237    /// Domain: both `str`. Result: the byte concatenation.
238    Concat,
239    /// Domain: every operand `str` (any count, none included). Result: the
240    /// byte concatenation in order (`""` for none), one `str_concat_n`.
241    ConcatN,
242    /// Domain: both `int`. Result: the truncated remainder (the sign follows
243    /// the dividend; `MIN % -1` is 0); a zero divisor raises `DivByZero`
244    /// (E0002) at the intrinsic.
245    IntRem,
246    /// Domain: both numbers (`int` or `float`). Result: `dpow` with
247    /// `overflow = promote`: an exact `int` power when it fits, else the
248    /// nearest `float`; a negative integer exponent gives the `float`
249    /// power; floats by the family's `ls_pow`.
250    NumPow,
251    /// Domain: both `int`. Result: `shl` with `shift = saturate` (an amount
252    /// of 64 or more gives 0; a negative amount raises `ShiftOutOfRange`,
253    /// E0003).
254    IntShl,
255    /// Domain: both `int`. Result: arithmetic `shr` with `shift = saturate`
256    /// (64 or more gives 0, or -1 for a negative value; a negative amount
257    /// raises `ShiftOutOfRange`).
258    IntShr,
259    /// Domain: both in `nil`, `bool`, `int`, `float`. Result: the exclusive
260    /// or of their truthiness (`nil`, `false`, `0`, `±0.0` are false; NaN is
261    /// true), a `bool`.
262    ScalarXor,
263    /// Domain: the same kind in `nil`, `bool`, `int`, `float` (equality by
264    /// value; IEEE for floats, so NaN equals nothing and `-0.0 == 0.0`), or
265    /// two `str` with equal bytes (`true`). A `bool`.
266    LooseEq,
267    /// The negation of [`LooseEq`](Fast::LooseEq), on the same domain.
268    LooseNe,
269    /// Domain: both in `nil`, `bool`, `int`, `float`, `str`. Result: `true`
270    /// exactly when the kinds are equal and the values are (floats by IEEE,
271    /// strings by bytes). A `bool`.
272    StrictEq,
273    /// The negation of [`StrictEq`](Fast::StrictEq), on the same domain.
274    StrictNe,
275    /// Domain: two `int`s or two `float`s. Result: `a < b` (false with NaN).
276    Lt,
277    /// Domain: two `int`s or two `float`s. Result: `a <= b`.
278    Le,
279    /// Domain: two `int`s or two `float`s. Result: `a > b`.
280    Gt,
281    /// Domain: two `int`s or two `float`s. Result: `a >= b`.
282    Ge,
283    /// Domain: two `int`s. Result: -1, 0, or 1 as an `int`.
284    IntSpaceship,
285    /// Domain: an `int` or a `float`. Result: the operand.
286    NumIdentity,
287    /// Domain: an `int`. Result: its bitwise complement.
288    IntBitNot,
289    /// Domain: `int` (itself), `bool` (0 or 1), `nil` (0). An `int`.
290    ToInt,
291    /// Domain: `float` (itself), `int` (the nearest `float`, ties to even),
292    /// `bool` (0.0 or 1.0), `nil` (0.0). A `float`.
293    ToFloat,
294    /// Domain: `str`. Result: the operand.
295    ToStr,
296    /// Domain: `nil`, `bool`, `int`, `float`. Result: the truthiness, a
297    /// `bool`.
298    ToBool,
299    /// Domain: `map`. Result: the operand.
300    ToArray,
301}
302
303impl Fast {
304    /// The operands the fast path takes.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// assert_eq!(bcgen_lang::Fast::ToInt.arity().min(), 1);
310    /// ```
311    #[must_use]
312    pub const fn arity(self) -> Arity {
313        match self {
314            Self::ConcatN => Arity::at_least(0),
315            Self::NumIdentity
316            | Self::IntBitNot
317            | Self::ToInt
318            | Self::ToFloat
319            | Self::ToStr
320            | Self::ToBool
321            | Self::ToArray => Arity::exact(1),
322            _ => Arity::exact(2),
323        }
324    }
325}
326
327/// How one named intrinsic compiles.
328///
329/// # Examples
330///
331/// ```
332/// use bcgen_lang::{Fast, Lowering};
333///
334/// let l = Lowering::Fast { fast: Fast::Concat, module: "mox".into(), name: "concat".into() };
335/// assert_eq!(l.host(), Some(("mox", "concat")));
336/// ```
337#[derive(Clone, Debug, PartialEq, Eq, Hash)]
338#[non_exhaustive]
339pub enum Lowering {
340    /// An LSB sequence, with LSB's semantics (hooks included).
341    Lsb(LsbOp),
342    /// A call to the host function `module`.`name` (an LSB import taking the
343    /// operands as `dyn` and returning a `dyn`), always.
344    Host {
345        /// The import module.
346        module: String,
347        /// The function name.
348        name: String,
349    },
350    /// The fast path inline on its domain, the host function `module`.`name`
351    /// everywhere else.
352    Fast {
353        /// The guarded fast path.
354        fast: Fast,
355        /// The import module of the slow path.
356        module: String,
357        /// The function name of the slow path.
358        name: String,
359    },
360}
361
362impl Lowering {
363    /// The host function this lowering may call, as (module, name).
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use bcgen_lang::{Lowering, LsbOp};
369    ///
370    /// assert_eq!(Lowering::Lsb(LsbOp::Len).host(), None);
371    /// ```
372    #[must_use]
373    pub fn host(&self) -> Option<(&str, &str)> {
374        match self {
375            Self::Lsb(_) => None,
376            Self::Host { module, name } | Self::Fast { module, name, .. } => {
377                Some((module.as_str(), name.as_str()))
378            }
379        }
380    }
381}
382
383/// One entry of an [`Intrinsics`] table: the operands it takes and how it
384/// compiles.
385///
386/// # Examples
387///
388/// ```
389/// use bcgen_lang::{Arity, Intrinsics};
390///
391/// let t = Intrinsics::mox();
392/// let def = t.get("mox.concat_n").unwrap();
393/// assert_eq!(def.arity(), Arity::at_least(0));
394/// ```
395#[derive(Clone, Debug, PartialEq, Eq, Hash)]
396pub struct IntrinsicDef {
397    arity: Arity,
398    lowering: Lowering,
399}
400
401impl IntrinsicDef {
402    /// The operands it takes; a call with another count is
403    /// [`CompileError::IntrinsicArity`](crate::CompileError::IntrinsicArity).
404    ///
405    /// # Examples
406    ///
407    /// See [`IntrinsicDef`].
408    #[must_use]
409    pub fn arity(&self) -> Arity {
410        self.arity
411    }
412
413    /// How it compiles.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// use bcgen_lang::{Intrinsics, Lowering, LsbOp};
419    ///
420    /// let t = Intrinsics::core();
421    /// assert_eq!(t.get("len").unwrap().lowering(), &Lowering::Lsb(LsbOp::Len));
422    /// ```
423    #[must_use]
424    pub fn lowering(&self) -> &Lowering {
425        &self.lowering
426    }
427}
428
429/// The intrinsic table: named intrinsic → arity and lowering. A name that is
430/// not in the table is [`CompileError::UnknownIntrinsic`](crate::CompileError::UnknownIntrinsic).
431///
432/// [`Options::new`](crate::Options::new) starts from [`Intrinsics::core`];
433/// Mox uses [`Intrinsics::mox`].
434///
435/// # Examples
436///
437/// ```
438/// use bcgen_lang::{Arity, Fast, Intrinsics, LsbOp, Options};
439///
440/// let table = Intrinsics::new()
441///     .with_lsb("concat", LsbOp::Concat)
442///     .with_host("lang.format", Arity::exact(1), "lang", "format")
443///     .with_fast("lang.eq", Fast::StrictEq, "lang", "eq");
444/// assert_eq!(table.len(), 3);
445/// let opts = Options::new().with_intrinsics(table);
446/// assert!(opts.intrinsics().get("lang.eq").is_some());
447/// ```
448#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
449pub struct Intrinsics {
450    table: BTreeMap<String, IntrinsicDef>,
451}
452
453impl Intrinsics {
454    /// An empty table.
455    ///
456    /// # Examples
457    ///
458    /// ```
459    /// assert!(bcgen_lang::Intrinsics::new().is_empty());
460    /// ```
461    #[must_use]
462    pub fn new() -> Self {
463        Intrinsics::default()
464    }
465
466    /// The built-in LSB intrinsics (see [`LsbOp`]): `concat`, `len`,
467    /// `truthy`, `lnot`, `type_of`, `iter`, `iter_next`, the coroutine
468    /// controls `resume`, `resume_throw`, `coro_close`, `coro_key`,
469    /// `coro_result`, `coro_status` (what a runtime's generator object
470    /// lowers to), and `err_code`, `err_payload` (what a runtime's exception
471    /// mapping reads from a caught error value).
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// assert_eq!(bcgen_lang::Intrinsics::core().len(), 15);
477    /// ```
478    #[must_use]
479    pub fn core() -> Self {
480        Intrinsics::new()
481            .with_lsb("concat", LsbOp::Concat)
482            .with_lsb("len", LsbOp::Len)
483            .with_lsb("truthy", LsbOp::Truthy)
484            .with_lsb("lnot", LsbOp::Not)
485            .with_lsb("type_of", LsbOp::TypeOf)
486            .with_lsb("iter", LsbOp::Iter)
487            .with_lsb("iter_next", LsbOp::IterNext)
488            .with_lsb("resume", LsbOp::Resume)
489            .with_lsb("resume_throw", LsbOp::ResumeThrow)
490            .with_lsb("coro_close", LsbOp::CoroClose)
491            .with_lsb("coro_key", LsbOp::CoroKey)
492            .with_lsb("coro_result", LsbOp::CoroResult)
493            .with_lsb("coro_status", LsbOp::CoroStatus)
494            .with_lsb("err_code", LsbOp::ErrCode)
495            .with_lsb("err_payload", LsbOp::ErrPayload)
496    }
497
498    /// [`core`](Intrinsics::core) plus every intrinsic of the Mox sketch
499    /// (`_lexersketch/sketches/mox.lsf`), each a [`Fast`] path over the host
500    /// function `mox`.`<name>` where one is exact, else the host function
501    /// alone (`mox.instanceof`, `mox.cast_object`, `mox.clone`). The table
502    /// in `docs/API.md` gives every mapping and the semantics its host
503    /// function must implement.
504    ///
505    /// # Examples
506    ///
507    /// ```
508    /// use bcgen_lang::{Fast, Intrinsics, Lowering};
509    ///
510    /// let t = Intrinsics::mox();
511    /// assert!(matches!(
512    ///     t.get("mox.loose_eq").unwrap().lowering(),
513    ///     Lowering::Fast { fast: Fast::LooseEq, .. }
514    /// ));
515    /// assert!(matches!(t.get("mox.clone").unwrap().lowering(), Lowering::Host { .. }));
516    /// ```
517    #[must_use]
518    pub fn mox() -> Self {
519        const FAST: [(&str, Fast); 23] = [
520            ("concat", Fast::Concat),
521            ("concat_n", Fast::ConcatN),
522            ("mod", Fast::IntRem),
523            ("pow", Fast::NumPow),
524            ("shl", Fast::IntShl),
525            ("shr", Fast::IntShr),
526            ("xor", Fast::ScalarXor),
527            ("loose_eq", Fast::LooseEq),
528            ("loose_ne", Fast::LooseNe),
529            ("identical", Fast::StrictEq),
530            ("not_identical", Fast::StrictNe),
531            ("lt", Fast::Lt),
532            ("le", Fast::Le),
533            ("gt", Fast::Gt),
534            ("ge", Fast::Ge),
535            ("spaceship", Fast::IntSpaceship),
536            ("to_number", Fast::NumIdentity),
537            ("bit_not", Fast::IntBitNot),
538            ("cast_int", Fast::ToInt),
539            ("cast_float", Fast::ToFloat),
540            ("cast_string", Fast::ToStr),
541            ("cast_bool", Fast::ToBool),
542            ("cast_array", Fast::ToArray),
543        ];
544        const HOST: [(&str, u8); 3] = [("instanceof", 2), ("cast_object", 1), ("clone", 1)];
545        let mut t = Intrinsics::core();
546        for (name, fast) in FAST {
547            let mut full = String::from("mox.");
548            full.push_str(name);
549            t = t.with_fast(&full, fast, "mox", name);
550        }
551        for (name, n) in HOST {
552            let mut full = String::from("mox.");
553            full.push_str(name);
554            t = t.with_host(&full, Arity::exact(n), "mox", name);
555        }
556        t
557    }
558
559    /// Registers `name` as the LSB sequence `op` (its arity is the op's),
560    /// replacing any earlier entry of that name.
561    ///
562    /// # Examples
563    ///
564    /// ```
565    /// use bcgen_lang::{Intrinsics, LsbOp};
566    ///
567    /// assert!(Intrinsics::new().with_lsb("strlen", LsbOp::Len).get("strlen").is_some());
568    /// ```
569    #[must_use]
570    pub fn with_lsb(mut self, name: &str, op: LsbOp) -> Self {
571        let def = IntrinsicDef {
572            arity: op.arity(),
573            lowering: Lowering::Lsb(op),
574        };
575        let _previous = self.table.insert(String::from(name), def);
576        self
577    }
578
579    /// Registers `name` as a call to the host function `module`.`func`
580    /// taking `arity` operands, replacing any earlier entry.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use bcgen_lang::{Arity, Intrinsics};
586    ///
587    /// let t = Intrinsics::new().with_host("php.clone", Arity::exact(1), "php", "clone");
588    /// assert_eq!(t.get("php.clone").unwrap().lowering().host(), Some(("php", "clone")));
589    /// ```
590    #[must_use]
591    pub fn with_host(mut self, name: &str, arity: Arity, module: &str, func: &str) -> Self {
592        let def = IntrinsicDef {
593            arity,
594            lowering: Lowering::Host {
595                module: String::from(module),
596                name: String::from(func),
597            },
598        };
599        let _previous = self.table.insert(String::from(name), def);
600        self
601    }
602
603    /// Registers `name` as the guarded fast path `fast` (its arity is the
604    /// fast path's) over the host function `module`.`func`, replacing any
605    /// earlier entry.
606    ///
607    /// # Examples
608    ///
609    /// ```
610    /// use bcgen_lang::{Fast, Intrinsics};
611    ///
612    /// let t = Intrinsics::new().with_fast("cmp", Fast::IntSpaceship, "rt", "cmp");
613    /// assert_eq!(t.len(), 1);
614    /// ```
615    #[must_use]
616    pub fn with_fast(mut self, name: &str, fast: Fast, module: &str, func: &str) -> Self {
617        let def = IntrinsicDef {
618            arity: fast.arity(),
619            lowering: Lowering::Fast {
620                fast,
621                module: String::from(module),
622                name: String::from(func),
623            },
624        };
625        let _previous = self.table.insert(String::from(name), def);
626        self
627    }
628
629    /// The entry for `name`.
630    ///
631    /// # Examples
632    ///
633    /// ```
634    /// assert!(bcgen_lang::Intrinsics::core().get("mox.concat").is_none());
635    /// ```
636    #[must_use]
637    pub fn get(&self, name: &str) -> Option<&IntrinsicDef> {
638        self.table.get(name)
639    }
640
641    /// The number of entries.
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// assert_eq!(bcgen_lang::Intrinsics::mox().len(), 41);
647    /// ```
648    #[must_use]
649    pub fn len(&self) -> usize {
650        self.table.len()
651    }
652
653    /// Whether the table is empty.
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// assert!(!bcgen_lang::Intrinsics::core().is_empty());
659    /// ```
660    #[must_use]
661    pub fn is_empty(&self) -> bool {
662        self.table.is_empty()
663    }
664
665    /// Every entry, in name order.
666    ///
667    /// # Examples
668    ///
669    /// ```
670    /// let core = bcgen_lang::Intrinsics::core();
671    /// let names: Vec<&str> = core.iter().map(|(n, _)| n).collect();
672    /// assert_eq!(names[0], "concat");
673    /// ```
674    pub fn iter(&self) -> impl Iterator<Item = (&str, &IntrinsicDef)> {
675        self.table.iter().map(|(n, d)| (n.as_str(), d))
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use alloc::string::ToString;
682
683    use super::{Arity, Fast, Intrinsics, Lowering};
684
685    #[test]
686    fn the_mox_preset_covers_every_sketch_intrinsic() {
687        let t = Intrinsics::mox();
688        for name in [
689            "concat",
690            "concat_n",
691            "mod",
692            "pow",
693            "shl",
694            "shr",
695            "xor",
696            "loose_eq",
697            "loose_ne",
698            "identical",
699            "not_identical",
700            "spaceship",
701            "lt",
702            "le",
703            "gt",
704            "ge",
705            "to_number",
706            "bit_not",
707            "instanceof",
708            "cast_int",
709            "cast_float",
710            "cast_string",
711            "cast_bool",
712            "cast_array",
713            "cast_object",
714            "clone",
715        ] {
716            let full = alloc::format!("mox.{name}");
717            let def = t.get(&full).unwrap_or_else(|| panic!("{full} missing"));
718            assert_eq!(def.lowering().host(), Some(("mox", name)));
719        }
720    }
721
722    #[test]
723    fn later_entries_replace_earlier_ones() {
724        let t = Intrinsics::new()
725            .with_host("x", Arity::exact(1), "a", "b")
726            .with_fast("x", Fast::Concat, "c", "d");
727        assert_eq!(t.len(), 1);
728        assert!(matches!(
729            t.get("x").map(super::IntrinsicDef::lowering),
730            Some(Lowering::Fast { .. })
731        ));
732        assert_eq!(
733            t.get("x").map(super::IntrinsicDef::arity),
734            Some(Arity::exact(2))
735        );
736    }
737
738    #[test]
739    fn arity_display_and_bounds() {
740        assert_eq!(Arity::exact(2).to_string(), "2");
741        assert_eq!(Arity::range(0, 3).to_string(), "0 to 3");
742        assert!(Arity::at_least(0).accepts(0));
743        assert!(!Arity::at_least(0).accepts(256));
744    }
745}