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