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