Skip to main content

handwired_rs/
circuit.rs

1//! The circuit: source cells plus an ordered list of operations over f32.
2//!
3//! Ported from `Circuit` in handwired's `src/runtime/numeric-ui-core.ts`. A
4//! program has four kinds of operation, each appending one number:
5//!
6//! ```txt
7//! affine(left)      = left
8//! reglu(left, gate) = f(left × max(0, gate))
9//! boolean(score)    = f(f(f(score) + 8_388_609) - 8_388_609)
10//! index(form, table, below, above) = values[table[floor(form)]], with a rule outside the table
11//! ```
12//!
13//! where `left`, `gate` and `form` are affine forms evaluated term by term in
14//! stored order, rounding to f32 after every product and sum.
15
16use std::collections::HashMap;
17
18use crate::Error;
19use crate::numeric::{
20    EXACT_GRID_STEPS, Range, evaluate_boolean, fround, is_power_of_two_grid, largest_grid, lowest_power_of_two, relu32,
21    smallest_grid,
22};
23use crate::wire::{Lin, Term, Wire};
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
26pub enum OpKind {
27    Affine,
28    Reglu,
29    Boolean,
30    Index,
31}
32
33/// The builder operation a wire came from, so the backward pass can
34/// differentiate the whole product or select instead of its ReLUs one by one.
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub enum Macro {
37    Multiply { left: Wire, right: Wire },
38    Select { gate: Wire, off: Wire, on: Wire },
39}
40
41/// What an index read returns when floor(index) is outside the table.
42#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
43pub enum OutOfRange {
44    /// The nearest end of the table.
45    Clamp,
46    /// +0.
47    Zero,
48}
49
50/// Why an operation's FP32 evaluation, term order or operand wires carry
51/// meaning. See handwired's docs/execution-model.md, "Rounding points".
52#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
53pub enum RoundingPointKind {
54    Boolean,
55    IntegerBit,
56    SignCascade,
57    Marked,
58}
59
60#[derive(Clone, Debug)]
61pub struct IndexRead {
62    pub table: usize,
63    pub below: OutOfRange,
64    pub above: OutOfRange,
65    /// The index range the builder accepted.
66    pub index_range: Range,
67}
68
69#[derive(Clone, Debug)]
70pub struct CandidateTable {
71    pub candidates: Vec<Wire>,
72    pub maximum_depth: u32,
73}
74
75#[derive(Clone, Debug)]
76pub struct Operation {
77    pub kind: OpKind,
78    pub left_terms: Vec<Term>,
79    pub left_bias: f64,
80    /// For an index read, the gate side holds the index form.
81    pub gate_terms: Vec<Term>,
82    pub gate_bias: f64,
83    pub depth: u32,
84    pub preserve_rounding: bool,
85    pub macro_op: Option<Macro>,
86    pub index_read: Option<IndexRead>,
87    pub rounding_point: Option<RoundingPointKind>,
88    /// Set on the bit that `emit_compare` returns.
89    pub comparison_bit: bool,
90}
91
92/// Gradient rule at a boolean atom, which is a step function.
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
94pub enum BooleanGradient {
95    #[default]
96    Reject,
97    StraightThrough,
98}
99
100/// Gradient rule for the index of an index read, which is a step function.
101#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
102pub enum IndexGradient {
103    #[default]
104    Reject,
105    Zero,
106    BackwardDifference,
107}
108
109#[derive(Copy, Clone, Debug, Default)]
110pub struct BackpropOptions {
111    pub boolean_gradient: BooleanGradient,
112    pub index_gradient: IndexGradient,
113}
114
115/// Small affine producers with fewer terms than this are copied into the
116/// operations that read them; wider sums stay one shared rounding point.
117const AFFINE_SHARE_THRESHOLD: usize = 4;
118
119/// Every integer within ±2^24 is an f32 value, so an index range must lie
120/// strictly inside it.
121const INDEX_MAGNITUDE_LIMIT: f64 = 16_777_216.0;
122
123#[derive(Clone, Debug)]
124pub struct Circuit {
125    source_size: usize,
126    pub(crate) operations: Vec<Operation>,
127    depths: Vec<u32>,
128    constants: Vec<Option<f64>>,
129    expression_wires: HashMap<Vec<u64>, Wire>,
130    pub(crate) ranges: Vec<Range>,
131    grids: Vec<Option<f64>>,
132    exact_bits: Vec<bool>,
133    has_readers: Vec<bool>,
134    pub(crate) tables: Vec<CandidateTable>,
135    table_by_content: HashMap<Vec<Wire>, usize>,
136}
137
138impl Circuit {
139    pub fn new(source_size: usize) -> Circuit {
140        Circuit {
141            source_size,
142            operations: Vec::new(),
143            depths: vec![0; source_size],
144            constants: vec![None; source_size],
145            expression_wires: HashMap::new(),
146            ranges: vec![Range::UNKNOWN; source_size],
147            grids: vec![None; source_size],
148            exact_bits: vec![false; source_size],
149            has_readers: vec![false; source_size],
150            tables: Vec::new(),
151            table_by_content: HashMap::new(),
152        }
153    }
154
155    pub fn source_size(&self) -> usize {
156        self.source_size
157    }
158
159    /// Source cells plus operations: the length of an evaluation's values.
160    pub fn width(&self) -> usize {
161        self.source_size + self.operations.len()
162    }
163
164    pub fn operations(&self) -> &[Operation] {
165        &self.operations
166    }
167
168    pub fn operation(&self, wire: Wire) -> Option<&Operation> {
169        wire.index().checked_sub(self.source_size).and_then(|index| self.operations.get(index))
170    }
171
172    pub fn range(&self, wire: Wire) -> Range {
173        self.ranges[wire.index()]
174    }
175
176    pub fn grid(&self, wire: Wire) -> Option<f64> {
177        self.grids[wire.index()]
178    }
179
180    pub fn is_exact_bit(&self, wire: Wire) -> bool {
181        self.exact_bits[wire.index()]
182    }
183
184    /// The value a constant wire holds before rounding, e.g. `Some(3.0)` for `constant(3.0)`.
185    pub fn constant_value(&self, wire: Wire) -> Option<f64> {
186        self.constants[wire.index()]
187    }
188
189    /// The build-time layer of a wire: 0 for a source cell.
190    pub fn depth(&self, wire: Wire) -> u32 {
191        self.depths[wire.index()]
192    }
193
194    pub fn is_non_negative(&self, wire: Wire) -> bool {
195        self.ranges[wire.index()].min >= 0.0
196    }
197
198    pub(crate) fn assert_wire(&self, wire: Wire) {
199        assert!(wire.index() < self.width(), "wire {} is not in the circuit", wire.index());
200    }
201
202    // ---- Declarations ---------------------------------------------------------------------------------------------
203
204    /// Declare the range of a wire's values, a promise from the caller.
205    /// Panics when it narrows the range of a wire that operations already
206    /// read, since they were built with the old range.
207    pub fn assume_range(&mut self, wire: Wire, min: f64, max: f64) {
208        self.declare_range(wire, min, max, false);
209    }
210
211    /// `assume_range` for a wire that operations already read. Those
212    /// operations keep the range they were built with.
213    pub fn assume_range_allowing_readers(&mut self, wire: Wire, min: f64, max: f64) {
214        self.declare_range(wire, min, max, true);
215    }
216
217    fn declare_range(&mut self, wire: Wire, min: f64, max: f64, allow_existing_readers: bool) {
218        self.assert_wire(wire);
219        assert!(min <= max, "numeric range is inverted");
220        let current = self.ranges[wire.index()];
221        let narrowed = Range::new(current.min.max(min), current.max.min(max));
222        assert!(narrowed.min <= narrowed.max, "numeric range contradicts known bounds");
223        let narrows = narrowed.min > current.min || narrowed.max < current.max;
224        if narrows && self.has_readers[wire.index()] && !allow_existing_readers {
225            panic!(
226                "operations that already read wire {} were built with its range {}..{}, and declaring {}..{} now \
227                 doesn't change them. Declare the range before the first operation that reads the wire",
228                wire.index(),
229                current.min,
230                current.max,
231                min,
232                max
233            );
234        }
235        self.ranges[wire.index()] = narrowed;
236    }
237
238    /// Declare that every value of the wire is a multiple of `step`, a power of two.
239    pub fn assume_grid(&mut self, wire: Wire, step: f64) {
240        self.declare_grid(wire, step, false);
241    }
242
243    pub fn assume_grid_allowing_readers(&mut self, wire: Wire, step: f64) {
244        self.declare_grid(wire, step, true);
245    }
246
247    fn declare_grid(&mut self, wire: Wire, step: f64, allow_existing_readers: bool) {
248        self.assert_wire(wire);
249        assert!(is_power_of_two_grid(step), "a grid step must be a power of two from 2^-149 to 2^127, got {step}");
250        let current = self.grids[wire.index()];
251        let grid = current.unwrap_or(step).max(step);
252        if Some(grid) != current && self.has_readers[wire.index()] && !allow_existing_readers {
253            panic!(
254                "operations that already read wire {} were built with grid {:?}, and declaring the grid {} now \
255                 doesn't change them. Declare the grid before the first operation that reads the wire",
256                wire.index(),
257                current,
258                step
259            );
260        }
261        self.grids[wire.index()] = Some(grid);
262    }
263
264    /// Declare that the wire holds exactly 0 or 1. Narrows its range to 0..1.
265    pub fn assume_bit(&mut self, wire: Wire) {
266        self.assume_range(wire, 0.0, 1.0);
267        self.exact_bits[wire.index()] = true;
268    }
269
270    /// Record an operation as a rounding point, e.g. each step of a round chain.
271    pub fn mark_rounding_point(&mut self, wire: Wire) {
272        self.record_rounding_point(wire, RoundingPointKind::Marked);
273    }
274
275    /// Earlier records win, e.g. a boolean atom that a comparison returns stays `Boolean`.
276    pub(crate) fn record_rounding_point(&mut self, wire: Wire, kind: RoundingPointKind) {
277        self.assert_wire(wire);
278        let index = wire.index().checked_sub(self.source_size).expect("a rounding point must be an operation");
279        self.operations[index].rounding_point.get_or_insert(kind);
280    }
281
282    /// Every rounding point in wire order.
283    pub fn rounding_points(&self) -> Vec<(Wire, RoundingPointKind)> {
284        self.operations
285            .iter()
286            .enumerate()
287            .filter_map(|(index, operation)| {
288                operation.rounding_point.map(|kind| (Wire::from_index(self.source_size + index), kind))
289            })
290            .collect()
291    }
292
293    // ---- Builder --------------------------------------------------------------------------------------------------
294
295    pub fn source(&self, index: usize) -> Wire {
296        assert!(index < self.source_size, "source cell {index} is outside the source table");
297        Wire::from_index(index)
298    }
299
300    /// A wire that always holds f(value). Ordinary affine readers fold it into their bias.
301    pub fn constant(&mut self, value: f64) -> Wire {
302        self.affine(value)
303    }
304
305    /// A weighted sum, e.g. `circuit.affine(width * 0.5 - 26.0)`. Small sums
306    /// are copied into the operations that read them.
307    pub fn affine(&mut self, form: impl Into<Lin>) -> Wire {
308        self.append(OpKind::Affine, form.into(), Lin::default(), false, None)
309    }
310
311    /// A weighted sum that keeps its evaluation, term order and operand wires,
312    /// including constants, so readers never copy its terms.
313    pub fn round_affine(&mut self, form: impl Into<Lin>) -> Wire {
314        self.append(OpKind::Affine, form.into(), Lin::default(), true, None)
315    }
316
317    /// `left × max(0, gate)`.
318    pub fn reglu(&mut self, left: impl Into<Lin>, gate: impl Into<Lin>) -> Wire {
319        self.append(OpKind::Reglu, left.into(), gate.into(), false, None)
320    }
321
322    /// `max(0, value)`.
323    pub fn relu(&mut self, value: impl Into<Lin>) -> Wire {
324        self.reglu(1.0, value)
325    }
326
327    /// Rounds a score within 0..1 to 0 below 0.5 and to 1 from 0.5 up.
328    pub fn boolean(&mut self, score: Wire) -> Wire {
329        let range = self.ranges[score.index()];
330        assert!(range.min >= 0.0 && range.max <= 1.0, "boolean input must be within zero and one");
331        self.append(OpKind::Boolean, Lin::from(score), Lin::default(), false, None)
332    }
333
334    /// `left × right`, one ReGLU when either side is declared nonnegative, three otherwise.
335    pub fn multiply(&mut self, left: Wire, right: Wire) -> Wire {
336        let macro_op = Some(Macro::Multiply { left, right });
337        if self.is_non_negative(right) {
338            return self.append(OpKind::Reglu, Lin::from(left), Lin::from(right), true, macro_op);
339        }
340        if self.is_non_negative(left) {
341            return self.append(OpKind::Reglu, Lin::from(right), Lin::from(left), true, macro_op);
342        }
343        let positive = self.append(OpKind::Reglu, Lin::from(left), Lin::from(right), true, None);
344        let negative = self.append(OpKind::Reglu, Lin::from(left), -right, true, None);
345        self.append(OpKind::Affine, positive - negative, Lin::default(), true, macro_op)
346    }
347
348    /// `gate × on + (1 - gate) × off`. Both sides always compute.
349    pub fn select(&mut self, gate: Wire, off: Wire, on: Wire) -> Wire {
350        let complement = self.round_affine(1.0 - gate);
351        let selected_on = self.multiply(gate, on);
352        let selected_off = self.multiply(complement, off);
353        self.append(
354            OpKind::Affine,
355            selected_on + selected_off,
356            Lin::default(),
357            true,
358            Some(Macro::Select { gate, off, on }),
359        )
360    }
361
362    /// `candidates[floor(index)]` as one operation. The index needs a declared
363    /// range strictly inside ±2^24 and must hold an integer.
364    pub fn index_read(&mut self, index: Wire, candidates: &[Wire], out_of_range: OutOfRange) -> Wire {
365        self.assert_wire(index);
366        let declared = self.ranges[index.index()];
367        self.index_read_affine(Lin::from(index), candidates, out_of_range, out_of_range, Some(declared))
368    }
369
370    /// An index read with the index written as an affine form and one rule per side.
371    pub fn index_read_affine(
372        &mut self,
373        index: Lin,
374        candidates: &[Wire],
375        below: OutOfRange,
376        above: OutOfRange,
377        declared_index_range: Option<Range>,
378    ) -> Wire {
379        for &(wire, coefficient) in &index.terms {
380            self.assert_wire(wire);
381            assert!(coefficient.is_finite(), "index coefficient must be finite");
382        }
383        assert!(index.bias.is_finite(), "index bias must be finite");
384        let table = self.register_candidate_table(candidates);
385        let (terms, bias) = self.fold_constant_terms(normalize_terms(&index.terms), index.bias);
386        let read_terms = terms.clone();
387        let (terms, bias) = self.compose_affine_terms(&terms, bias);
388        if terms.is_empty() {
389            let constant_index = fround(bias);
390            assert!(!constant_index.is_nan(), "a constant index must not be NaN");
391            let count = self.tables[table].candidates.len();
392            let position = resolve_index_position(constant_index, count, below, above);
393            return if position >= 0 { self.tables[table].candidates[position as usize] } else { self.affine(0.0) };
394        }
395        let mut index_range = evaluate_affine_range(&terms, bias, &self.ranges);
396        // A NaN endpoint passes this check and fails the magnitude check below.
397        #[allow(clippy::neg_cmp_op_on_partial_ord)]
398        if let Some(declared) = declared_index_range {
399            index_range = Range::new(index_range.min.max(declared.min), index_range.max.min(declared.max));
400            assert!(
401                !(index_range.min > index_range.max),
402                "the declared index range contradicts the range inferred from the index form"
403            );
404        }
405        assert!(
406            index_range.min > -INDEX_MAGNITUDE_LIMIT && index_range.max < INDEX_MAGNITUDE_LIMIT,
407            "index read needs an index range declared strictly inside ±2^24, got {}..{}",
408            index_range.min,
409            index_range.max
410        );
411        let mut key = expression_key(OpKind::Index, &[], 0.0, &terms, bias);
412        key.extend([u64::MAX, below as u64, above as u64, table as u64]);
413        if let Some(&existing) = self.expression_wires.get(&key) {
414            return existing;
415        }
416        let mut depth = self.tables[table].maximum_depth;
417        for &(dependency, _) in &terms {
418            depth = depth.max(self.depths[dependency.index()]);
419        }
420        let wire = Wire::from_index(self.width());
421        let range = index_read_range(&self.tables[table], index_range, below, above, &self.ranges);
422        self.operations.push(Operation {
423            kind: OpKind::Index,
424            left_terms: Vec::new(),
425            left_bias: 0.0,
426            gate_terms: terms,
427            gate_bias: bias,
428            depth: depth + 1,
429            preserve_rounding: false,
430            macro_op: None,
431            index_read: Some(IndexRead { table, below, above, index_range }),
432            rounding_point: None,
433            comparison_bit: false,
434        });
435        self.push_wire_metadata(depth + 1, None, None, range);
436        for &(read, _) in &read_terms {
437            self.has_readers[read.index()] = true;
438        }
439        self.expression_wires.insert(key, wire);
440        wire
441    }
442
443    fn register_candidate_table(&mut self, candidates: &[Wire]) -> usize {
444        assert!(!candidates.is_empty(), "an index read needs at least one candidate");
445        if let Some(&table) = self.table_by_content.get(candidates) {
446            return table;
447        }
448        let mut maximum_depth = 0;
449        for &candidate in candidates {
450            self.assert_wire(candidate);
451            maximum_depth = maximum_depth.max(self.depths[candidate.index()]);
452            self.has_readers[candidate.index()] = true;
453        }
454        let table = self.tables.len();
455        self.tables.push(CandidateTable { candidates: candidates.to_vec(), maximum_depth });
456        self.table_by_content.insert(candidates.to_vec(), table);
457        table
458    }
459
460    /// Append one operation, or return an existing wire that computes the same
461    /// expression. Without `preserve_rounding`, terms are sorted and merged,
462    /// constants fold into the bias, small affine producers are copied in, and
463    /// a ReGLU with a constant gate becomes an affine.
464    pub(crate) fn append(
465        &mut self,
466        mut kind: OpKind,
467        left: Lin,
468        gate: Lin,
469        preserve_rounding: bool,
470        macro_op: Option<Macro>,
471    ) -> Wire {
472        debug_assert!(kind != OpKind::Index);
473        for &(wire, _) in left.terms.iter().chain(&gate.terms) {
474            self.assert_wire(wire);
475        }
476        let (mut left_terms, mut left_bias) = (left.terms, left.bias);
477        let (mut gate_terms, mut gate_bias) = (gate.terms, gate.bias);
478        let read_left_terms;
479        let read_gate_terms;
480        if !preserve_rounding {
481            (left_terms, left_bias) = self.fold_constant_terms(normalize_terms(&left_terms), left_bias);
482            (gate_terms, gate_bias) = self.fold_constant_terms(normalize_terms(&gate_terms), gate_bias);
483            read_left_terms = left_terms.clone();
484            read_gate_terms = gate_terms.clone();
485            if kind == OpKind::Affine
486                && let Some(alias) = identity_wire(&left_terms, left_bias)
487            {
488                return alias;
489            }
490            (left_terms, left_bias) = self.compose_affine_terms(&left_terms, left_bias);
491            (gate_terms, gate_bias) = self.compose_affine_terms(&gate_terms, gate_bias);
492            if kind == OpKind::Reglu && gate_terms.is_empty() {
493                let gate_value = if gate_bias.is_nan() { f64::NAN } else { gate_bias.max(0.0) };
494                kind = OpKind::Affine;
495                if gate_value == 0.0 {
496                    left_terms.clear();
497                } else {
498                    for term in &mut left_terms {
499                        term.1 *= gate_value;
500                    }
501                }
502                left_bias *= gate_value;
503                gate_bias = 0.0;
504            } else if kind == OpKind::Reglu && left_terms.is_empty() && left_bias == 0.0 {
505                kind = OpKind::Affine;
506                gate_terms.clear();
507                gate_bias = 0.0;
508            }
509            if kind == OpKind::Affine
510                && let Some(alias) = identity_wire(&left_terms, left_bias)
511            {
512                return alias;
513            }
514        } else {
515            read_left_terms = left_terms.clone();
516            read_gate_terms = gate_terms.clone();
517        }
518        let mut key = expression_key(kind, &left_terms, left_bias, &gate_terms, gate_bias);
519        if preserve_rounding {
520            key.push(u64::MAX - 1);
521        }
522        match macro_op {
523            Some(Macro::Multiply { left, right }) => key.extend([u64::MAX - 2, left.0 as u64, right.0 as u64]),
524            Some(Macro::Select { gate, off, on }) => {
525                key.extend([u64::MAX - 3, gate.0 as u64, off.0 as u64, on.0 as u64])
526            }
527            None => {}
528        }
529        if let Some(&existing) = self.expression_wires.get(&key) {
530            return existing;
531        }
532        let wire = Wire::from_index(self.width());
533        let gate_dependencies: &[Term] = if kind == OpKind::Reglu { &gate_terms } else { &[] };
534        let depth = left_terms
535            .iter()
536            .chain(gate_dependencies)
537            .map(|&(dependency, _)| self.depths[dependency.index()])
538            .max()
539            .unwrap_or(0);
540        let constant =
541            if !preserve_rounding && kind == OpKind::Affine && left_terms.is_empty() { Some(left_bias) } else { None };
542        let grid =
543            infer_operation_grid(kind, &left_terms, left_bias, &gate_terms, gate_bias, &self.grids, &self.ranges);
544        let left_range = evaluate_affine_range(&left_terms, left_bias, &self.ranges);
545        let range = match kind {
546            OpKind::Affine => left_range,
547            OpKind::Reglu => {
548                multiply_by_relu_range(left_range, evaluate_affine_range(&gate_terms, gate_bias, &self.ranges))
549            }
550            _ => Range::new(0.0, 1.0),
551        };
552        if !left_terms.is_empty() {
553            for &(read, _) in &read_left_terms {
554                self.has_readers[read.index()] = true;
555            }
556        }
557        if !gate_terms.is_empty() {
558            for &(read, _) in &read_gate_terms {
559                self.has_readers[read.index()] = true;
560            }
561        }
562        self.operations.push(Operation {
563            kind,
564            left_terms,
565            left_bias,
566            gate_terms,
567            gate_bias,
568            depth: depth + 1,
569            preserve_rounding,
570            macro_op,
571            index_read: None,
572            rounding_point: if kind == OpKind::Boolean { Some(RoundingPointKind::Boolean) } else { None },
573            comparison_bit: false,
574        });
575        self.push_wire_metadata(depth + 1, constant, grid, range);
576        self.expression_wires.insert(key, wire);
577        wire
578    }
579
580    fn push_wire_metadata(&mut self, depth: u32, constant: Option<f64>, grid: Option<f64>, range: Range) {
581        self.depths.push(depth);
582        self.constants.push(constant);
583        self.grids.push(grid);
584        self.exact_bits.push(false);
585        self.has_readers.push(false);
586        self.ranges.push(range);
587    }
588
589    fn fold_constant_terms(&self, terms: Vec<Term>, mut bias: f64) -> (Vec<Term>, f64) {
590        let mut dynamic_terms = Vec::with_capacity(terms.len());
591        for (wire, coefficient) in terms {
592            match self.constants[wire.index()] {
593                Some(constant) => bias += coefficient * constant,
594                None => dynamic_terms.push((wire, coefficient)),
595            }
596        }
597        (dynamic_terms, bias)
598    }
599
600    fn compose_affine_terms(&self, terms: &[Term], mut bias: f64) -> (Vec<Term>, f64) {
601        let mut composed = Vec::with_capacity(terms.len());
602        for &(wire, coefficient) in terms {
603            let producer = self.operation(wire);
604            match producer {
605                Some(operation)
606                    if operation.kind == OpKind::Affine
607                        && !operation.preserve_rounding
608                        && operation.left_terms.len() < AFFINE_SHARE_THRESHOLD =>
609                {
610                    bias += coefficient * operation.left_bias;
611                    for &(dependency, dependency_coefficient) in &operation.left_terms {
612                        composed.push((dependency, coefficient * dependency_coefficient));
613                    }
614                }
615                _ => composed.push((wire, coefficient)),
616            }
617        }
618        (normalize_terms(&composed), bias)
619    }
620
621    // ---- Evaluation -----------------------------------------------------------------------------------------------
622
623    /// Check that every source cell is within its declared range.
624    pub fn validate_source_ranges(&self, source: &[f32]) -> Result<(), Error> {
625        if source.len() != self.source_size {
626            return Err(Error::SourceLength { expected: self.source_size, actual: source.len() });
627        }
628        for (index, &value) in source.iter().enumerate() {
629            let range = self.ranges[index];
630            let value = value as f64;
631            if !(value >= range.min && value <= range.max) {
632                return Err(Error::SourceOutOfRange { wire: Wire::from_index(index), value, range });
633            }
634        }
635        Ok(())
636    }
637
638    /// What the program computes: every wire's value, one operation at a time
639    /// in stored order. A compiled program must reproduce every live value.
640    pub fn evaluate_reference(&self, source: &[f32]) -> Result<Vec<f32>, Error> {
641        self.validate_source_ranges(source)?;
642        Ok(self.evaluate_unchecked(source))
643    }
644
645    pub(crate) fn evaluate_unchecked(&self, source: &[f32]) -> Vec<f32> {
646        let mut values = Vec::with_capacity(self.width());
647        values.extend_from_slice(source);
648        for operation in &self.operations {
649            let left = evaluate_terms(&values, &operation.left_terms, operation.left_bias);
650            let value = match operation.kind {
651                OpKind::Affine => left,
652                OpKind::Reglu => left * relu32(evaluate_terms(&values, &operation.gate_terms, operation.gate_bias)),
653                OpKind::Boolean => evaluate_boolean(left),
654                OpKind::Index => {
655                    let read = operation.index_read.as_ref().unwrap();
656                    let index = evaluate_terms(&values, &operation.gate_terms, operation.gate_bias);
657                    read_indexed_value(&values, &self.tables[read.table].candidates, read, index as f64)
658                }
659            };
660            values.push(value);
661        }
662        values
663    }
664
665    /// Reverse-mode gradients of the intended real arithmetic: `values` from
666    /// `evaluate_reference`, `output_gradients` one per wire. ReLUs get slope 0
667    /// at their corner; products and selects are differentiated as a whole.
668    pub fn backpropagate_reference(
669        &self,
670        values: &[f32],
671        output_gradients: &[f32],
672        options: BackpropOptions,
673    ) -> Result<Vec<f32>, Error> {
674        assert_eq!(values.len(), self.width(), "forward values have the wrong length");
675        assert_eq!(output_gradients.len(), self.width(), "output gradients have the wrong length");
676        let mut gradients = output_gradients.to_vec();
677        for (operation_index, operation) in self.operations.iter().enumerate().rev() {
678            let output_wire = self.source_size + operation_index;
679            let output_gradient = gradients[output_wire];
680            if output_gradient == 0.0 {
681                continue;
682            }
683            match operation.macro_op {
684                Some(Macro::Multiply { left, right }) => {
685                    accumulate(&mut gradients, &[(left, values[right.index()] as f64)], output_gradient);
686                    accumulate(&mut gradients, &[(right, values[left.index()] as f64)], output_gradient);
687                    continue;
688                }
689                Some(Macro::Select { gate, off, on }) => {
690                    let gate_value = values[gate.index()];
691                    accumulate(
692                        &mut gradients,
693                        &[(off, (1.0 - gate_value) as f64), (on, gate_value as f64)],
694                        output_gradient,
695                    );
696                    let swing = values[on.index()] - values[off.index()];
697                    accumulate(&mut gradients, &[(gate, swing as f64)], output_gradient);
698                    continue;
699                }
700                None => {}
701            }
702            match operation.kind {
703                OpKind::Index => {
704                    let read = operation.index_read.as_ref().unwrap();
705                    if options.index_gradient == IndexGradient::Reject {
706                        return Err(Error::IndexGradient { wire: Wire::from_index(output_wire) });
707                    }
708                    let candidates = &self.tables[read.table].candidates;
709                    let index_value = evaluate_terms(values, &operation.gate_terms, operation.gate_bias) as f64;
710                    let position = resolve_index_position(index_value, candidates.len(), read.below, read.above);
711                    if position >= 0 {
712                        accumulate(&mut gradients, &[(candidates[position as usize], 1.0)], output_gradient);
713                    }
714                    if options.index_gradient == IndexGradient::BackwardDifference && index_value.is_finite() {
715                        let floor_index = index_value.floor();
716                        let difference = read_indexed_value(values, candidates, read, floor_index) as f64
717                            - read_indexed_value(values, candidates, read, floor_index - 1.0) as f64;
718                        let scaled = (output_gradient as f64 * difference) as f32;
719                        accumulate(&mut gradients, &operation.gate_terms, scaled);
720                    }
721                }
722                OpKind::Boolean => {
723                    if options.boolean_gradient != BooleanGradient::StraightThrough {
724                        return Err(Error::BooleanGradient { wire: Wire::from_index(output_wire) });
725                    }
726                    accumulate(&mut gradients, &operation.left_terms, output_gradient);
727                }
728                OpKind::Affine => accumulate(&mut gradients, &operation.left_terms, output_gradient),
729                OpKind::Reglu => {
730                    let left = evaluate_terms(values, &operation.left_terms, operation.left_bias);
731                    let gate = evaluate_terms(values, &operation.gate_terms, operation.gate_bias);
732                    accumulate(&mut gradients, &operation.left_terms, output_gradient * relu32(gate));
733                    if gate > 0.0 {
734                        accumulate(&mut gradients, &operation.gate_terms, output_gradient * left);
735                    }
736                }
737            }
738        }
739        Ok(gradients)
740    }
741}
742
743// ---- Free helpers ---------------------------------------------------------------------------------------------------
744
745fn key_number(value: f64) -> u64 {
746    // JavaScript's keys print -0 as 0, so both share one key.
747    (value + 0.0).to_bits()
748}
749
750fn expression_key(kind: OpKind, left_terms: &[Term], left_bias: f64, gate_terms: &[Term], gate_bias: f64) -> Vec<u64> {
751    let mut key = Vec::with_capacity(4 + 2 * (left_terms.len() + gate_terms.len()));
752    key.push(kind as u64);
753    key.push(key_number(left_bias));
754    for &(wire, coefficient) in left_terms {
755        key.extend([wire.0 as u64, key_number(coefficient)]);
756    }
757    key.push(u64::MAX - 4);
758    key.push(key_number(gate_bias));
759    for &(wire, coefficient) in gate_terms {
760        key.extend([wire.0 as u64, key_number(coefficient)]);
761    }
762    key
763}
764
765fn identity_wire(terms: &[Term], bias: f64) -> Option<Wire> {
766    match terms {
767        [(wire, coefficient)] if bias == 0.0 && *coefficient == 1.0 => Some(*wire),
768        _ => None,
769    }
770}
771
772/// Sort terms by wire, merge repeated wires, drop zero coefficients.
773pub(crate) fn normalize_terms(terms: &[Term]) -> Vec<Term> {
774    let mut sorted = terms.to_vec();
775    sorted.sort_by_key(|&(wire, _)| wire);
776    let mut normalized: Vec<Term> = Vec::with_capacity(sorted.len());
777    for (wire, coefficient) in sorted {
778        match normalized.last_mut() {
779            Some(previous) if previous.0 == wire => previous.1 += coefficient,
780            _ => normalized.push((wire, coefficient)),
781        }
782    }
783    normalized.retain(|&(_, coefficient)| coefficient != 0.0);
784    normalized
785}
786
787/// `f(bias)`, then `value = f(value + f(f(coefficient) × values[wire]))` per term in stored order.
788pub(crate) fn evaluate_terms(values: &[f32], terms: &[Term], bias: f64) -> f32 {
789    let mut value = bias as f32;
790    for &(wire, coefficient) in terms {
791        value += coefficient as f32 * values[wire.index()];
792    }
793    value
794}
795
796fn accumulate(gradients: &mut [f32], terms: &[Term], output_gradient: f32) {
797    for &(wire, coefficient) in terms {
798        gradients[wire.index()] += output_gradient * coefficient as f32;
799    }
800}
801
802/// floor(index) as a table position: 0..count-1 inside the table; outside it
803/// the nearest end, or -1 where that side reads +0; -2 for a NaN index.
804pub(crate) fn resolve_index_position(index: f64, count: usize, below: OutOfRange, above: OutOfRange) -> i64 {
805    if index.is_nan() {
806        return -2;
807    }
808    let position = index.floor();
809    if position < 0.0 {
810        return if below == OutOfRange::Zero { -1 } else { 0 };
811    }
812    if position > (count - 1) as f64 {
813        return if above == OutOfRange::Zero { -1 } else { count as i64 - 1 };
814    }
815    position as i64
816}
817
818fn read_indexed_value(values: &[f32], candidates: &[Wire], read: &IndexRead, index: f64) -> f32 {
819    match resolve_index_position(index, candidates.len(), read.below, read.above) {
820        -2 => f32::NAN,
821        -1 => 0.0,
822        position => values[candidates[position as usize].index()],
823    }
824}
825
826pub(crate) fn evaluate_affine_range(terms: &[Term], bias: f64, ranges: &[Range]) -> Range {
827    let mut min = fround(bias);
828    let mut max = fround(bias);
829    for &(wire, coefficient) in terms {
830        let range = ranges[wire.index()];
831        let rounded = fround(coefficient);
832        if rounded > 0.0 {
833            min = fround(min + fround(rounded * range.min));
834            max = fround(max + fround(rounded * range.max));
835        } else {
836            min = fround(min + fround(rounded * range.max));
837            max = fround(max + fround(rounded * range.min));
838        }
839        if min.is_nan() || max.is_nan() {
840            return Range::UNKNOWN;
841        }
842    }
843    if min.is_nan() || max.is_nan() {
844        return Range::UNKNOWN;
845    }
846    Range::new(min, max)
847}
848
849fn multiply_by_relu_range(left: Range, gate: Range) -> Range {
850    let gate_min = gate.min.max(0.0);
851    let gate_max = gate.max.max(0.0);
852    let products = [
853        fround(left.min * gate_min),
854        fround(left.min * gate_max),
855        fround(left.max * gate_min),
856        fround(left.max * gate_max),
857    ];
858    // Zero times an infinite endpoint is NaN; never turn that into a finite bound.
859    if products.iter().any(|product| product.is_nan()) {
860        return Range::UNKNOWN;
861    }
862    Range::new(
863        products.iter().copied().fold(f64::INFINITY, f64::min),
864        products.iter().copied().fold(f64::NEG_INFINITY, f64::max),
865    )
866}
867
868fn index_read_range(
869    table: &CandidateTable,
870    index_range: Range,
871    below: OutOfRange,
872    above: OutOfRange,
873    ranges: &[Range],
874) -> Range {
875    let count = table.candidates.len() as f64;
876    let lowest = index_range.min.floor().min(count).max(-1.0) as i64;
877    let highest = index_range.max.floor().min(count).max(-1.0) as i64;
878    let count = table.candidates.len() as i64;
879    let mut min = f64::INFINITY;
880    let mut max = f64::NEG_INFINITY;
881    let mut include = |range: Range| {
882        min = min.min(range.min);
883        max = max.max(range.max);
884    };
885    for position in lowest.max(0)..=highest.min(count - 1) {
886        include(ranges[table.candidates[position as usize].index()]);
887    }
888    let zero = Range::new(0.0, 0.0);
889    if lowest < 0 {
890        include(if below == OutOfRange::Zero { zero } else { ranges[table.candidates[0].index()] });
891    }
892    if highest > count - 1 {
893        include(if above == OutOfRange::Zero { zero } else { ranges[table.candidates[(count - 1) as usize].index()] });
894    }
895    Range::new(min, max)
896}
897
898struct GridForm {
899    grid: f64,
900    magnitude: f64,
901}
902
903/// The grid of an affine form evaluated in FP32, when every term reads a wire
904/// with a grid and the declared ranges keep every product and partial sum
905/// within 2^24 grid steps, so nothing rounds.
906fn infer_form_grid(terms: &[Term], bias: f64, grids: &[Option<f64>], ranges: &[Range]) -> Option<GridForm> {
907    let rounded_bias = fround(bias);
908    let mut grid = if rounded_bias == 0.0 { f64::INFINITY } else { lowest_power_of_two(rounded_bias) };
909    let mut magnitude = rounded_bias.abs();
910    for &(wire, coefficient) in terms {
911        let wire_grid = grids[wire.index()]?;
912        let range = ranges[wire.index()];
913        let largest_operand = range.min.abs().max(range.max.abs());
914        if !largest_operand.is_finite() {
915            return None;
916        }
917        let rounded = fround(coefficient);
918        if rounded == 0.0 {
919            continue;
920        }
921        grid = grid.min(wire_grid * lowest_power_of_two(rounded));
922        magnitude += rounded.abs() * largest_operand;
923    }
924    if !(grid.is_finite() && grid >= smallest_grid() && magnitude <= EXACT_GRID_STEPS * grid) {
925        return None;
926    }
927    Some(GridForm { grid: grid.min(largest_grid()), magnitude })
928}
929
930fn infer_operation_grid(
931    kind: OpKind,
932    left_terms: &[Term],
933    left_bias: f64,
934    gate_terms: &[Term],
935    gate_bias: f64,
936    grids: &[Option<f64>],
937    ranges: &[Range],
938) -> Option<f64> {
939    if kind == OpKind::Boolean {
940        return Some(1.0);
941    }
942    let left = infer_form_grid(left_terms, left_bias, grids, ranges)?;
943    if kind == OpKind::Affine {
944        return Some(left.grid);
945    }
946    let gate = infer_form_grid(gate_terms, gate_bias, grids, ranges)?;
947    let grid = left.grid * gate.grid;
948    if !(grid >= smallest_grid() && left.magnitude * gate.magnitude <= EXACT_GRID_STEPS * grid) {
949        return None;
950    }
951    Some(grid.min(largest_grid()))
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957
958    #[test]
959    fn small_affine_sums_compose_and_constants_fold() {
960        let mut circuit = Circuit::new(2);
961        let (a, b) = (circuit.source(0), circuit.source(1));
962        let three = circuit.constant(3.0);
963        let sum = circuit.affine(a + b * 2.0);
964        let shifted = circuit.affine(sum - three);
965        let operation = circuit.operation(shifted).unwrap();
966        assert_eq!(operation.left_terms, vec![(a, 1.0), (b, 2.0)]);
967        assert_eq!(operation.left_bias, -3.0);
968        // The identical expression returns the same wire.
969        assert_eq!(circuit.affine(a + b * 2.0), sum);
970        // x × 1 + 0 is x itself.
971        assert_eq!(circuit.affine(a), a);
972    }
973
974    #[test]
975    fn ranges_and_grids_are_inferred() {
976        let mut circuit = Circuit::new(1);
977        let x = circuit.source(0);
978        circuit.assume_range(x, 0.0, 16_384.0);
979        circuit.assume_grid(x, 1.0 / 64.0);
980        let half = circuit.affine(x * 0.5 - 26.0);
981        assert_eq!(circuit.range(half), Range::new(-26.0, 8_166.0));
982        assert_eq!(circuit.grid(half), Some(1.0 / 128.0));
983    }
984
985    #[test]
986    #[should_panic(expected = "already read wire 0")]
987    fn late_range_declaration_is_refused() {
988        let mut circuit = Circuit::new(1);
989        let x = circuit.source(0);
990        circuit.relu(x);
991        circuit.assume_range(x, 0.0, 1.0);
992    }
993
994    #[test]
995    fn index_read_floors_and_applies_side_rules() {
996        let mut circuit = Circuit::new(1);
997        let index = circuit.source(0);
998        circuit.assume_range(index, -2.0, 10.0);
999        let table: Vec<Wire> = [28.0, 44.0, 28.0, 60.0].iter().map(|&value| circuit.constant(value)).collect();
1000        let clamped = circuit.index_read(index, &table, OutOfRange::Clamp);
1001        let zeroed = circuit.index_read(index, &table, OutOfRange::Zero);
1002        assert_eq!(circuit.range(zeroed), Range::new(0.0, 60.0));
1003        for (input, expected_clamped, expected_zero) in
1004            [(2.5, 28.0, 28.0), (1.0, 44.0, 44.0), (-1.0, 28.0, 0.0), (7.0, 60.0, 0.0)]
1005        {
1006            let values = circuit.evaluate_reference(&[input]).unwrap();
1007            assert_eq!(values[clamped.index()], expected_clamped);
1008            assert_eq!(values[zeroed.index()], expected_zero);
1009        }
1010        // A constant index selects while building.
1011        let constant_index =
1012            circuit.index_read_affine(Lin::constant(3.0), &table, OutOfRange::Clamp, OutOfRange::Clamp, None);
1013        assert_eq!(constant_index, table[3]);
1014    }
1015
1016    #[test]
1017    fn source_ranges_are_checked() {
1018        let mut circuit = Circuit::new(1);
1019        let x = circuit.source(0);
1020        circuit.assume_range(x, 0.0, 1.0);
1021        assert!(matches!(circuit.evaluate_reference(&[2.0]), Err(Error::SourceOutOfRange { .. })));
1022        assert!(matches!(circuit.evaluate_reference(&[f32::NAN]), Err(Error::SourceOutOfRange { .. })));
1023    }
1024}