Skip to main content

echidna/bytecode_tape/
forward.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::bytecode_tape::CustomOp;
5use crate::float::Float;
6use crate::opcode::{self, OpCode, UNUSED};
7
8/// Evaluate all non-Input, non-Const operations on a values buffer.
9///
10/// Shared by `forward` (in-place) and `forward_into` (external buffer).
11fn forward_dispatch<F: Float>(
12    opcodes: &[OpCode],
13    arg_indices: &[[u32; 2]],
14    values: &mut [F],
15    custom_ops: &[Arc<dyn CustomOp<F>>],
16    custom_second_args: &HashMap<u32, u32>,
17) {
18    for i in 0..opcodes.len() {
19        match opcodes[i] {
20            OpCode::Input | OpCode::Const => continue,
21            OpCode::Custom => {
22                let [a_idx, cb_idx] = arg_indices[i];
23                let a = values[a_idx as usize];
24                let b = custom_second_args
25                    .get(&(i as u32))
26                    .map(|&bi| values[bi as usize])
27                    .unwrap_or(F::zero());
28                values[i] = custom_ops[cb_idx as usize].eval(a, b);
29            }
30            op => {
31                let [a_idx, b_idx] = arg_indices[i];
32                let a = values[a_idx as usize];
33                if op == OpCode::Powi {
34                    let exp = opcode::powi_exp_decode_raw(b_idx);
35                    values[i] = a.powi(exp);
36                    continue;
37                }
38                let b = if b_idx != UNUSED {
39                    values[b_idx as usize]
40                } else {
41                    F::zero()
42                };
43                values[i] = opcode::eval_forward(op, a, b);
44            }
45        }
46    }
47}
48
49impl<F: Float> super::BytecodeTape<F> {
50    /// Re-evaluate the tape at new inputs (forward sweep).
51    ///
52    /// Overwrites `values` in-place — no allocation.
53    pub fn forward(&mut self, inputs: &[F]) {
54        assert_eq!(
55            inputs.len(),
56            self.num_inputs as usize,
57            "wrong number of inputs"
58        );
59
60        debug_assert!(
61            self.opcodes[..self.num_inputs as usize]
62                .iter()
63                .all(|&op| op == OpCode::Input),
64            "input slots must be contiguous Input opcodes at the start of the tape"
65        );
66        for (i, &v) in inputs.iter().enumerate() {
67            self.values[i] = v;
68        }
69
70        forward_dispatch(
71            &self.opcodes,
72            &self.arg_indices,
73            &mut self.values,
74            &self.custom_ops,
75            &self.custom_second_args,
76        );
77    }
78
79    /// Forward sweep with nonsmooth branch tracking.
80    ///
81    /// Calls [`forward`](Self::forward) to evaluate the tape, then scans for
82    /// nonsmooth operations and records which branch was taken at each one.
83    ///
84    /// Tracked operations:
85    /// - `Abs`, `Min`, `Max` — kinks with nontrivial subdifferentials
86    /// - `Signum`, `Floor`, `Ceil`, `Round`, `Trunc` — step-function
87    ///   discontinuities (zero derivative on both sides, tracked for proximity
88    ///   detection only)
89    ///
90    /// Returns [`crate::NonsmoothInfo`] containing all kink entries in tape order.
91    pub fn forward_nonsmooth(&mut self, inputs: &[F]) -> crate::nonsmooth::NonsmoothInfo<F> {
92        self.forward(inputs);
93
94        let mut kinks = Vec::new();
95        for i in 0..self.opcodes.len() {
96            let op = self.opcodes[i];
97            if !opcode::is_nonsmooth(op) {
98                continue;
99            }
100
101            let [a_idx, b_idx] = self.arg_indices[i];
102            let a = self.values[a_idx as usize];
103
104            match op {
105                OpCode::Abs => {
106                    kinks.push(crate::nonsmooth::KinkEntry {
107                        tape_index: i as u32,
108                        opcode: op,
109                        switching_value: a,
110                        branch: if a >= F::zero() { 1 } else { -1 },
111                    });
112                }
113                OpCode::Max => {
114                    let b = self.values[b_idx as usize];
115                    kinks.push(crate::nonsmooth::KinkEntry {
116                        tape_index: i as u32,
117                        opcode: op,
118                        switching_value: a - b,
119                        branch: if a >= b { 1 } else { -1 },
120                    });
121                }
122                OpCode::Min => {
123                    let b = self.values[b_idx as usize];
124                    kinks.push(crate::nonsmooth::KinkEntry {
125                        tape_index: i as u32,
126                        opcode: op,
127                        switching_value: a - b,
128                        branch: if a <= b { 1 } else { -1 },
129                    });
130                }
131                OpCode::Signum => {
132                    // Kink at x = 0 (same as Abs).
133                    kinks.push(crate::nonsmooth::KinkEntry {
134                        tape_index: i as u32,
135                        opcode: op,
136                        switching_value: a,
137                        branch: if a >= F::zero() { 1 } else { -1 },
138                    });
139                }
140                OpCode::Floor | OpCode::Ceil | OpCode::Trunc => {
141                    // Kink at integer values. switching_value = distance to
142                    // nearest integer: zero exactly at kink points, works
143                    // symmetrically for both approach directions.
144                    kinks.push(crate::nonsmooth::KinkEntry {
145                        tape_index: i as u32,
146                        opcode: op,
147                        switching_value: a - a.round(),
148                        branch: if a - a.floor() < F::from(0.5).unwrap() {
149                            1
150                        } else {
151                            -1
152                        },
153                    });
154                }
155                OpCode::Fract => {
156                    // Kink at integer values, same as Floor/Ceil/Trunc.
157                    kinks.push(crate::nonsmooth::KinkEntry {
158                        tape_index: i as u32,
159                        opcode: op,
160                        switching_value: a - a.round(),
161                        branch: if a.fract() >= F::zero() { 1 } else { -1 },
162                    });
163                }
164                OpCode::Round => {
165                    // Round has kinks at half-integers (0.5, 1.5, ...),
166                    // not at integers. Shift by 0.5 to measure distance
167                    // to the nearest half-integer.
168                    let half = F::from(0.5).unwrap();
169                    let shifted = a + half;
170                    kinks.push(crate::nonsmooth::KinkEntry {
171                        tape_index: i as u32,
172                        opcode: op,
173                        switching_value: shifted - shifted.round(),
174                        branch: if a - a.floor() < half { 1 } else { -1 },
175                    });
176                }
177                _ => unreachable!(),
178            }
179        }
180
181        crate::nonsmooth::NonsmoothInfo { kinks }
182    }
183
184    /// Forward evaluation into an external buffer.
185    ///
186    /// Reads opcodes, constants, and argument indices from `self`, but writes
187    /// computed values into `values_buf` instead of `self.values`. This allows
188    /// parallel evaluation of the same tape at different inputs without cloning.
189    pub fn forward_into(&self, inputs: &[F], values_buf: &mut Vec<F>) {
190        assert_eq!(
191            inputs.len(),
192            self.num_inputs as usize,
193            "wrong number of inputs"
194        );
195
196        // Mirror the invariant enforced by `forward`: input slots must be the
197        // first `num_inputs` opcodes. Without this check the `values_buf[i]
198        // = v` loop below would silently overwrite non-Input slots if a
199        // caller mixed `new_input` / `push_op` out of order.
200        debug_assert!(
201            self.opcodes[..self.num_inputs as usize]
202                .iter()
203                .all(|&op| op == OpCode::Input),
204            "input slots must be contiguous Input opcodes at the start of the tape"
205        );
206
207        let n = self.num_variables as usize;
208        values_buf.clear();
209        values_buf.resize(n, F::zero());
210
211        // Copy constant values from the tape, then overwrite inputs.
212        values_buf.copy_from_slice(&self.values[..n]);
213        for (i, &v) in inputs.iter().enumerate() {
214            values_buf[i] = v;
215        }
216
217        forward_dispatch(
218            &self.opcodes,
219            &self.arg_indices,
220            values_buf,
221            &self.custom_ops,
222            &self.custom_second_args,
223        );
224    }
225}