qcode/value/insn/intrinsic.rs
1//! Pure intrinsic functions: named, side-effect-free operations such as `rol`
2//! and `ror`.
3//!
4//! An intrinsic is represented by a single [`Mnemonic::Intrinsic`] variant
5//! carrying an [`IntrinsicId`] plus its operands — there is no dedicated
6//! mnemonic per intrinsic and no control-flow call. Semantics live in an
7//! [`Intrinsic`] definition looked up from a process-global registry.
8//!
9//! # Purity
10//!
11//! `Mnemonic::Intrinsic` is *categorically pure*: it has no memory effects and
12//! no observable side effects. Passes treat the variant itself as the purity
13//! contract (DCE may drop an unused intrinsic; GVN/CSE may dedup one). Anything
14//! impure (syscalls, `rdtsc`, …) stays a [`PCodeOp`](super::PCodeOp).
15//!
16//! # Registry
17//!
18//! Built-in intrinsics self-register with [`inventory`] via
19//! [`register_intrinsic!`], mirroring the pass registry. The id-indexed table
20//! and the name→id map are built once, lazily, from `inventory::iter`. An
21//! [`IntrinsicId`] is a runtime handle (cheap to copy/match/hash) but
22//! *serializes by name*, so the on-disk form is stable regardless of link
23//! order and an unknown name is a clean deserialize error.
24
25use rustc_hash::FxHashMap as HashMap;
26use std::sync::OnceLock;
27
28use super::binop::IntBinop;
29use super::mnemonic::{Args, MnemonicKind};
30use crate::{
31 types::{TypeId, TypeManager},
32 value::{BodyView, InstructionId, LocalValueId, QCodeView, ValueId, ValueRef},
33};
34use smallvec::SmallVec;
35
36/// A stable-by-name handle into the intrinsic registry.
37///
38/// Cheap to copy, match, and hash in hot pass code; serialized as the
39/// intrinsic's name so the id is never written to disk.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct IntrinsicId(usize);
42
43impl IntrinsicId {
44 /// Resolve an intrinsic by name, or `None` if no intrinsic is registered
45 /// under `name`.
46 pub fn from_name(name: &str) -> Option<Self> {
47 registry().by_name.get(name).copied()
48 }
49
50 /// The registered name of this intrinsic (e.g. `"rol"`).
51 pub fn name(self) -> &'static str {
52 self.desc().name()
53 }
54
55 /// The [`Intrinsic`] definition this id resolves to.
56 pub fn desc(self) -> &'static dyn Intrinsic {
57 registry().descs[self.0]
58 }
59}
60
61impl serde::Serialize for IntrinsicId {
62 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
63 s.serialize_str(self.name())
64 }
65}
66
67impl<'de> serde::Deserialize<'de> for IntrinsicId {
68 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
69 let name = <std::borrow::Cow<'de, str>>::deserialize(d)?;
70 IntrinsicId::from_name(&name)
71 .ok_or_else(|| serde::de::Error::custom(format!("unknown intrinsic `{name}`")))
72 }
73}
74
75/// The IR shape an intrinsic's idiom is rooted at, used to gate recognition so
76/// only relevant recognizers run per instruction.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum RootOp {
79 /// Rooted at an integer binop (e.g. `rol`/`ror` root at `IntBinop::Or`).
80 IntBinop(IntBinop),
81}
82
83/// The definition of one *kind* of intrinsic — its name, arity, result typing,
84/// shared evaluator, and optional recognition / simplification behaviour.
85///
86/// An [`IntrinsicId`] is a by-name handle resolving to a single `&'static dyn
87/// Intrinsic`; the [`IntrinsicApp`] mnemonic is an *application* of that
88/// definition to operands. Built-in definitions are unit structs registered via
89/// [`register_intrinsic!`](crate::register_intrinsic).
90pub trait Intrinsic: Sync {
91 /// Textual name, e.g. `"rol"`. Unique across the registry.
92 fn name(&self) -> &'static str;
93
94 /// Number of operands the intrinsic takes.
95 fn arity(&self) -> usize;
96
97 /// The result type for an application to operands of types `args`. A sized
98 /// integer is just a type, so a width-only intrinsic accesses the published
99 /// canonical integer; sequence-producing intrinsics likewise require their
100 /// result type to have been created before this method is called.
101 fn result_type(&self, types: &TypeManager, args: &[TypeId]) -> TypeId;
102
103 /// Evaluate on concrete operands `(bits, byte_width)`, producing an
104 /// `out_size`-byte result. `None` means "not foldable / trap" — constant
105 /// folding bails, the emulator raises.
106 fn eval(&self, args: &[(u128, usize)], out_size: usize) -> Option<u128>;
107
108 /// IR shape this intrinsic's idiom roots at, if it participates in
109 /// recognition.
110 fn root_op(&self) -> Option<RootOp> {
111 None
112 }
113
114 /// Recognize the raw-IR idiom rooted at `at`, returning the intrinsic's
115 /// operands when the instruction matches. Reads the IR through a
116 /// [`BodyView`] and mints any derived operand literals through the shared
117 /// interner (e.g. a rotate amount
118 /// recovered as the `log2` of a strength-reduced multiplier).
119 fn recognize(&self, _view: BodyView<'_, '_>, _at: InstructionId) -> Option<Vec<ValueId>> {
120 None
121 }
122
123 /// Algebraic simplification on the intrinsic's own operands — e.g.
124 /// `rol(x, 0) → x` or `rol(a, c) → rol(a, c mod bits)`. Receives the
125 /// applied [`IntrinsicId`] (so a shared simplifier can branch on `rol` vs
126 /// `ror`), the result byte width, and the operands. Reads through a
127 /// [`BodyView`] and mints replacement literals through the shared interner.
128 fn simplify(
129 &self,
130 _view: BodyView<'_, '_>,
131 _id: IntrinsicId,
132 _out_size: usize,
133 _args: &[ValueId],
134 ) -> Option<Simplified> {
135 None
136 }
137}
138
139/// The result of an intrinsic's [`simplify`](Intrinsic::simplify) hook.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum Simplified {
142 /// Forward all uses of the intrinsic to this existing value, e.g.
143 /// `rol(x, 0) → x` or `ror(rol(a, c), c) → a`.
144 Value(ValueId),
145 /// Replace the intrinsic instruction with a new expression of the same
146 /// width. The mnemonic is free to be a different intrinsic (e.g.
147 /// `rol(a, c) → ror(a, bits - c)`) or a plain operation (e.g. an
148 /// [`IntBinop`]-rooted `+`).
149 Expression(super::Mnemonic),
150}
151
152/// One intrinsic's registration, submitted via [`inventory::submit!`] (see
153/// `register_intrinsic!`) and collected into the global registry.
154pub struct IntrinsicRegistration(pub &'static dyn Intrinsic);
155
156inventory::collect!(IntrinsicRegistration);
157
158struct Registry {
159 /// Definitions indexed by [`IntrinsicId`].
160 descs: Vec<&'static dyn Intrinsic>,
161 /// Name → id, for parsing and serde.
162 by_name: HashMap<&'static str, IntrinsicId>,
163 /// Ids grouped by recognition root, so the recognizer pass can fetch only
164 /// the relevant ones per instruction.
165 by_root: HashMap<RootOp, Vec<IntrinsicId>>,
166}
167
168fn registry() -> &'static Registry {
169 static REG: OnceLock<Registry> = OnceLock::new();
170 REG.get_or_init(|| {
171 // Sort by name so ids are deterministic within a build regardless of
172 // inventory iteration order.
173 let mut descs: Vec<&'static dyn Intrinsic> = inventory::iter::<IntrinsicRegistration>()
174 .map(|r| r.0)
175 .collect();
176 descs.sort_by_key(|d| d.name());
177
178 let mut by_name = HashMap::default();
179 let mut by_root: HashMap<RootOp, Vec<IntrinsicId>> = HashMap::default();
180 for (idx, desc) in descs.iter().enumerate() {
181 let id = IntrinsicId(idx);
182 let prev = by_name.insert(desc.name(), id);
183 assert!(
184 prev.is_none(),
185 "duplicate intrinsic registration: {}",
186 desc.name()
187 );
188 if let Some(root) = desc.root_op() {
189 by_root.entry(root).or_default().push(id);
190 }
191 }
192
193 Registry {
194 descs,
195 by_name,
196 by_root,
197 }
198 })
199}
200
201/// Every intrinsic whose recognition idiom roots at `root`. Empty if none.
202pub fn recognizers_for(root: RootOp) -> &'static [IntrinsicId] {
203 registry()
204 .by_root
205 .get(&root)
206 .map(Vec::as_slice)
207 .unwrap_or(&[])
208}
209
210/// A pure intrinsic instruction: an [`IntrinsicId`] applied to its operands.
211#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
212pub struct IntrinsicApp {
213 pub id: IntrinsicId,
214 pub args: Vec<LocalValueId>,
215}
216
217impl MnemonicKind for IntrinsicApp {
218 fn opcode(&self) -> &'static str {
219 self.id.name()
220 }
221
222 fn args(&self) -> Args {
223 SmallVec::from_vec(self.args.clone())
224 }
225}
226
227// ---------------------------------------------------------------------------
228// Registration macro
229// ---------------------------------------------------------------------------
230
231/// Register a built-in intrinsic with the global registry.
232///
233/// Takes a unit-struct value implementing [`Intrinsic`]; the registry stores it
234/// as a `&'static dyn Intrinsic`.
235///
236/// ```ignore
237/// struct Rol;
238/// impl Intrinsic for Rol { /* … */ }
239/// register_intrinsic!(Rol);
240/// ```
241#[macro_export]
242macro_rules! register_intrinsic {
243 ($def:expr $(,)?) => {
244 inventory::submit! {
245 $crate::value::insn::IntrinsicRegistration(&$def)
246 }
247 };
248}
249
250// ---------------------------------------------------------------------------
251// Helpers shared by the built-in intrinsics (see `crate::intrinsics`)
252// ---------------------------------------------------------------------------
253
254/// The unsigned mask for a `bytes`-wide value, saturating at 128 bits.
255pub(crate) fn mask_for(bytes: usize) -> u128 {
256 let bits = (bytes * 8).min(128);
257 if bits == 0 {
258 0
259 } else if bits == 128 {
260 u128::MAX
261 } else {
262 (1u128 << bits) - 1
263 }
264}
265
266/// The non-symbolic constant value of `v`, or `None`.
267pub(crate) fn const_u64<'ctx, 'str: 'ctx>(
268 view: impl QCodeView<'ctx, 'str>,
269 v: ValueId,
270) -> Option<u64> {
271 match ValueRef::from_view(view, v) {
272 ValueRef::Literal(lit) => {
273 let ValueId::Literal(id) = v else {
274 return None;
275 };
276 if view.shared().values.literals[id].symbolic.is_some() {
277 return None;
278 }
279 Some(lit.value())
280 }
281 _ => None,
282 }
283}
284
285/// If `v` is defined by an `IntBinop::want`, return its `(lhs, rhs)`.
286pub(crate) fn as_int_binop<'ctx, 'str: 'ctx>(
287 view: impl QCodeView<'ctx, 'str>,
288 v: ValueId,
289 want: IntBinop,
290) -> Option<(ValueId, ValueId)> {
291 use super::{Binary, Binop, Mnemonic};
292 let ValueId::Instruction(id) = v else {
293 return None;
294 };
295 match view.instruction(id).mnemonic() {
296 Mnemonic::Binop(Binary {
297 lhs,
298 rhs,
299 op: Binop::Int(op),
300 }) if *op == want => Some((lhs.qualify(id.func), rhs.qualify(id.func))),
301 _ => None,
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn intrinsic_id_serializes_by_name() {
311 let config = bincode::config::standard();
312 let rol = IntrinsicId::from_name("rol").unwrap();
313
314 // Serialized form is the name string, so it round-trips by name.
315 let bytes = bincode::serde::encode_to_vec(rol, config).unwrap();
316 let name: String = bincode::serde::decode_from_slice(&bytes, config).unwrap().0;
317 assert_eq!(name, "rol");
318
319 let (back, _): (IntrinsicId, usize) =
320 bincode::serde::decode_from_slice(&bytes, config).unwrap();
321 assert_eq!(back, rol);
322
323 // An unknown name is a clean error, not a corrupt id.
324 let bad = bincode::serde::encode_to_vec("nope", config).unwrap();
325 assert!(bincode::serde::decode_from_slice::<IntrinsicId, _>(&bad, config).is_err());
326 }
327}