@@ -180,6 +180,90 @@ pub trait Op {
180180 }
181181}
182182
183+ /// Broadcast-expand `lhs` and `rhs`, apply `combine` element-wise in `i64`,
184+ /// and return the unclamped intermediate as `Tensor<i64>`.
185+ ///
186+ /// This is the re-executable kernel the proof system calls to recover the
187+ /// pre-saturation intermediate without storing it in the trace.
188+ pub ( super ) fn sat_accumulate_pair (
189+ lhs : & crate :: tensor:: Tensor < i32 > ,
190+ rhs : & crate :: tensor:: Tensor < i32 > ,
191+ op_name : & str ,
192+ combine : impl Fn ( i64 , i64 ) -> i64 + Sync ,
193+ ) -> crate :: tensor:: Tensor < i64 > {
194+ use crate :: tensor:: get_broadcasted_shape;
195+ use common:: parallel:: par_enabled;
196+ use rayon:: prelude:: * ;
197+
198+ let shape = get_broadcasted_shape ( lhs. dims ( ) , rhs. dims ( ) )
199+ . unwrap_or_else ( |_| panic ! ( "{op_name}: incompatible broadcast shapes" ) ) ;
200+ let lhs_exp = lhs
201+ . expand ( & shape)
202+ . unwrap_or_else ( |_| panic ! ( "{op_name}: expand lhs" ) ) ;
203+ let rhs_exp = rhs
204+ . expand ( & shape)
205+ . unwrap_or_else ( |_| panic ! ( "{op_name}: expand rhs" ) ) ;
206+ let data: Vec < i64 > = lhs_exp
207+ . data ( )
208+ . par_iter ( )
209+ . zip ( rhs_exp. data ( ) . par_iter ( ) )
210+ . with_min_len ( par_enabled ( ) )
211+ . map ( |( & a, & b) | combine ( a as i64 , b as i64 ) )
212+ . collect ( ) ;
213+ crate :: tensor:: Tensor :: new ( Some ( & data) , & shape)
214+ . unwrap_or_else ( |_| panic ! ( "{op_name}: Tensor::new" ) )
215+ }
216+
217+ /// Clamp each element of a `Tensor<i64>` into `[i32::MIN, i32::MAX]`.
218+ pub ( super ) fn clamp_to_i32 ( t : & crate :: tensor:: Tensor < i64 > ) -> crate :: tensor:: Tensor < i32 > {
219+ use common:: parallel:: par_enabled;
220+ use rayon:: prelude:: * ;
221+
222+ let data: Vec < i32 > = t
223+ . data ( )
224+ . par_iter ( )
225+ . with_min_len ( par_enabled ( ) )
226+ . map ( |& v| v. clamp ( i32:: MIN as i64 , i32:: MAX as i64 ) as i32 )
227+ . collect ( ) ;
228+ crate :: tensor:: Tensor :: new ( Some ( & data) , t. dims ( ) )
229+ . unwrap_or_else ( |_| panic ! ( "clamp_to_i32: Tensor::new" ) )
230+ }
231+
232+ /// Saturating element-wise binary operation for [`Add`] and [`Sub`].
233+ ///
234+ /// For each consecutive input pair: accumulates via [`sat_accumulate_pair`]
235+ /// (in `i64`), then clamps to `i32` via [`clamp_to_i32`]. Per-step clamping
236+ /// preserves correct saturation semantics for variadic inputs.
237+ pub ( super ) fn sat_binop (
238+ inputs : Vec < & crate :: tensor:: Tensor < i32 > > ,
239+ op_name : & str ,
240+ combine : impl Fn ( i64 , i64 ) -> i64 + Sync + Copy ,
241+ ) -> crate :: tensor:: Tensor < i32 > {
242+ let mut output = inputs[ 0 ] . clone ( ) ;
243+ for & rhs in & inputs[ 1 ..] {
244+ output = clamp_to_i32 ( & sat_accumulate_pair ( & output, rhs, op_name, combine) ) ;
245+ }
246+ output
247+ }
248+
249+ /// Re-execute a binary [`Add`]/[`Sub`] node's accumulation, returning the
250+ /// pre-clamp `i64` intermediate.
251+ ///
252+ /// The proof system uses this to recover the accumulation (lookup-index)
253+ /// polynomial for the saturating-clamp lookup without storing it in the trace.
254+ /// Panics on non-`Add`/`Sub` operators or a non-binary operand list.
255+ pub fn sat_binop_intermediate (
256+ operator : & Operator ,
257+ lhs : & crate :: tensor:: Tensor < i32 > ,
258+ rhs : & crate :: tensor:: Tensor < i32 > ,
259+ ) -> crate :: tensor:: Tensor < i64 > {
260+ match operator {
261+ Operator :: Add ( _) => sat_accumulate_pair ( lhs, rhs, "Add" , |a, b| a + b) ,
262+ Operator :: Sub ( _) => sat_accumulate_pair ( lhs, rhs, "Sub" , |a, b| a - b) ,
263+ _ => panic ! ( "sat_binop_intermediate: expected Add or Sub, got {operator:?}" ) ,
264+ }
265+ }
266+
183267/// Shared evaluation for the periodic trig operators ([`Sin`], [`Cos`]).
184268///
185269/// Both reduce the input modulo a `4π` approximation before applying their
0 commit comments