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}