binius_frontend/ir/hints.rs
1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3//! Hint system.
4//!
5//! Hints are deterministic computations that happen on the prover side.
6//!
7//! They can be used for operations that require many constraints to compute but few constraints
8//! to verify.
9
10use std::hash::{DefaultHasher, Hash, Hasher};
11
12use binius_core::Word;
13use rustc_hash::FxHashMap;
14
15/// Registry key for one prover-side computation.
16///
17/// Derived from the declared name rather than assigned in order, so registration order never
18/// changes it.
19pub type HintId = u32;
20
21/// Hint handler trait for extensible operations.
22///
23/// Each implementor declares a globally unique name, and the registry keys on the hash of that
24/// name alone.
25///
26/// Every gate using the same hint type therefore shares one handler entry.
27///
28/// A hint's fields are not part of its identity.
29/// Only the first value registered under a name is kept; later values are dropped.
30/// Gates that differ only in those fields fold together under deduplication.
31/// Parameterize a hint through its dimensions, never through its fields.
32///
33/// # The `dimensions` parameter
34///
35/// Both [`shape`](Hint::shape) and [`execute`](Hint::execute) take a `dimensions: &[usize]`
36/// slice. This is hint-defined parameterization for a single gate — the values the caller
37/// passes when invoking the hint via
38/// [`CircuitBuilder::call_hint`](crate::builder::CircuitBuilder::call_hint). The same slice
39/// is then handed back to `execute` at witness-generation time.
40///
41/// `dimensions` controls input/output arity: `shape(dimensions) -> (n_in, n_out)` tells the
42/// builder how many wires the gate consumes and produces, and `execute` is later called with
43/// `inputs.len() == n_in` and `outputs.len() == n_out`. A hint whose arity is fixed
44/// (e.g. always 4 inputs / 6 outputs) takes an empty slice and ignores it. A hint that is
45/// parameterized over, say, big-integer limb counts takes those counts as `dimensions`.
46///
47/// Two arity modes illustrate the contract:
48/// - A parameterized hint reads limb counts from `dimensions` and derives its arity from them.
49/// - A fixed-arity hint ignores `dimensions` (an empty slice) and returns a constant shape.
50pub trait Hint: Send + Sync + 'static {
51 /// Globally unique name for this hint, which the registry hashes into its key.
52 const NAME: &'static str;
53
54 /// Compute the gate's input/output arity as a function of `dimensions`.
55 ///
56 /// Called once when the gate is emitted by `call_hint` to allocate output wires. The
57 /// returned `(n_in, n_out)` is the contract for the matching [`execute`](Hint::execute)
58 /// call: the builder will provide `n_in` input wires and expect `n_out` outputs.
59 ///
60 /// Implementations must be a pure function of `dimensions` and must agree with
61 /// [`execute`](Hint::execute) on the same `dimensions`.
62 fn shape(&self, dimensions: &[usize]) -> (usize, usize);
63
64 /// Compute the hint's outputs from its inputs at witness-generation time.
65 ///
66 /// Receives the same `dimensions` slice that was passed to [`shape`](Hint::shape) when the
67 /// gate was emitted. `inputs.len() == n_in` and `outputs.len() == n_out` where
68 /// `(n_in, n_out) == self.shape(dimensions)`. Implementations write all `n_out` output
69 /// slots — including zero-padding when the natural result has fewer significant words.
70 fn execute(&self, dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]);
71}
72
73/// Derive a [`HintId`] from a hint's name.
74///
75/// Hashes the name and folds the resulting 64-bit value down to 32 bits by XORing its two halves.
76///
77/// The hash algorithm is unspecified, so an id is stable only within one process.
78/// Never persist an id or compare one across builds.
79pub fn hint_id_of(name: &str) -> HintId {
80 let mut hasher = DefaultHasher::new();
81 name.hash(&mut hasher);
82 let h = hasher.finish();
83 (h as u32) ^ ((h >> 32) as u32)
84}
85
86/// Object-safe adapter so the registry can store hints behind `Box<dyn _>`.
87///
88/// `Hint` itself is not dyn-compatible because it carries an associated `const NAME`.
89/// A blanket impl adapts any `Hint` to this trait.
90pub(crate) trait ErasedHint: Send + Sync {
91 fn shape(&self, dimensions: &[usize]) -> (usize, usize);
92 fn execute(&self, dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]);
93}
94
95impl<T: Hint> ErasedHint for T {
96 fn shape(&self, dimensions: &[usize]) -> (usize, usize) {
97 <T as Hint>::shape(self, dimensions)
98 }
99
100 fn execute(&self, dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
101 <T as Hint>::execute(self, dimensions, inputs, outputs);
102 }
103}
104
105/// Registry for hint handlers keyed by [`HintId`].
106///
107/// Each entry keeps the name it was registered under.
108/// An id shared by two names is then caught, instead of running one hint as the other.
109pub struct HintRegistry {
110 handlers: FxHashMap<HintId, (&'static str, Box<dyn ErasedHint>)>,
111}
112
113impl HintRegistry {
114 /// An empty registry.
115 pub fn new() -> Self {
116 Self {
117 handlers: FxHashMap::default(),
118 }
119 }
120
121 /// Register a hint, returning its [`HintId`].
122 ///
123 /// Registering a name already present is a no-op, the handler's fields included.
124 ///
125 /// # Panics
126 ///
127 /// Panics if a different name already holds this id.
128 pub fn register<T: Hint>(&mut self, handler: T) -> HintId {
129 let id = hint_id_of(T::NAME);
130 let entry = self
131 .handlers
132 .entry(id)
133 .or_insert_with(|| (T::NAME, Box::new(handler)));
134 assert_eq!(entry.0, T::NAME, "hint id collision: {} and {}", entry.0, T::NAME);
135 id
136 }
137
138 /// Compute the `(n_in, n_out)` arity of the hint identified by `hint_id`.
139 pub fn shape(&self, hint_id: HintId, dimensions: &[usize]) -> (usize, usize) {
140 self.handlers[&hint_id].1.shape(dimensions)
141 }
142
143 /// Run the handler under `hint_id`, writing its results into `outputs`.
144 ///
145 /// # Panics
146 ///
147 /// Panics if nothing is registered under `hint_id`.
148 pub fn execute(
149 &self,
150 hint_id: HintId,
151 dimensions: &[usize],
152 inputs: &[Word],
153 outputs: &mut [Word],
154 ) {
155 self.handler(hint_id).execute(dimensions, inputs, outputs);
156 }
157
158 /// Resolve the handler registered under `hint_id`.
159 ///
160 /// Lets a caller invoking one hint many times pay for the lookup once.
161 pub(crate) fn handler(&self, hint_id: HintId) -> &dyn ErasedHint {
162 &*self.handlers[&hint_id].1
163 }
164}
165
166impl Default for HintRegistry {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 struct AddK {
177 k: u64,
178 }
179
180 impl Hint for AddK {
181 const NAME: &'static str = "test::add_k";
182
183 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
184 (1, 1)
185 }
186
187 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
188 outputs[0] = Word(inputs[0].0.wrapping_add(self.k));
189 }
190 }
191
192 fn run(registry: &HintRegistry, id: HintId, input: u64) -> u64 {
193 let mut outputs = [Word::ZERO];
194 registry.execute(id, &[], &[Word(input)], &mut outputs);
195 outputs[0].0
196 }
197
198 #[test]
199 fn re_registering_one_hint_type_keeps_a_single_entry() {
200 let mut registry = HintRegistry::new();
201 let first = registry.register(AddK { k: 7 });
202 let second = registry.register(AddK { k: 7 });
203 assert_eq!(first, second);
204 assert_eq!(registry.handlers.len(), 1);
205 }
206
207 // Pins the known limitation the trait doc warns about.
208 #[test]
209 fn fields_of_a_second_registration_are_ignored() {
210 let mut registry = HintRegistry::new();
211 let id = registry.register(AddK { k: 7 });
212 registry.register(AddK { k: 1000 });
213 assert_eq!(run(®istry, id, 0), 7);
214 }
215
216 // A 32-bit collision is out of reach of a search over static names, so it is planted.
217 #[test]
218 #[should_panic(expected = "hint id collision: test::squatter and test::add_k")]
219 fn colliding_names_panic() {
220 let mut registry = HintRegistry::new();
221 registry
222 .handlers
223 .insert(hint_id_of(AddK::NAME), ("test::squatter", Box::new(AddK { k: 7 })));
224 registry.register(AddK { k: 7 });
225 }
226}