Skip to main content

logicaffeine_compile/codegen_ana/
lower.rs

1//! Float-function → analog-netlist lowering — the analog lane's Futamura moment.
2//!
3//! The region unit is a FUNCTION: its float parameters are the plane's DAC inputs, its `Assume`
4//! statements the declared input ranges, its return the ADC tap. Extraction reuses the digital
5//! fabric's certified if-conversion front-end ([`decision_fabric_from_source`], imported
6//! read-only) on a contract-free twin of the source: the `Assume` range checks are the CONTROL
7//! PLANE's job (the digital host validates a word before driving the DAC), so they are stripped
8//! from the analog dataflow — exactly the mixed-signal architecture, not an elision. The declared
9//! ranges themselves are read from the ORIGINAL function body.
10//!
11//! Every cell the fabric front-end emits is either mapped to an analog tile (`FAddF`/`FSubF` →
12//! weighted Kirchhoff [`AnalogCell::Sum`], `FMulF` → [`AnalogCell::GainConst`] when one side is a
13//! programmed constant, else the four-quadrant [`AnalogCell::Mul`]; `IntToF` of a constant folds)
14//! or refused with a NAMED [`AnaLowerError`] — fail-closed, never a silently-wrong circuit.
15
16use std::collections::BTreeMap;
17
18use crate::ast::stmt::{Stmt, TypeExpr};
19use crate::codegen_sva::fabric_rtl::{decision_fabric_from_source, FuncUnit, LoopCell, Src};
20
21use super::cell::{ASrc, AnalogCell, AnalogNetlist};
22use super::target::{AnaUnplaceable, AnalogCapability, AnalogTarget};
23use super::{assumed_ranges_in, AnaLowerError};
24
25/// Where a fabric SSA val lands in the analog netlist: a real tile's output net, or a
26/// compile-time fold (an `IntToF` of a constant).
27#[derive(Clone, Copy)]
28pub(crate) enum NetRef {
29    Cell(usize),
30    Const(f64),
31}
32
33/// The result of mapping one fabric cell into the analog vocabulary.
34pub(crate) enum MappedCell {
35    /// A real analog tile.
36    Tile(AnalogCell),
37    /// A short tile GROUP emitted for one fabric cell (e.g. Select = difference Sum + switch).
38    /// Inside the group, `ASrc::Val(PREV_TILE)` refers to the immediately preceding tile;
39    /// [`push_tiles`] rewrites it to the real index. The group's result is its LAST tile.
40    Tiles(Vec<AnalogCell>),
41    /// A compile-time fold (e.g. `IntToF` of a constant) — no tile, just a value.
42    Fold(f64),
43}
44
45/// The within-group back-reference sentinel for [`MappedCell::Tiles`].
46pub(crate) const PREV_TILE: usize = usize::MAX - 1;
47
48/// Push a tile group, rewriting `PREV_TILE` references, returning the LAST tile's index.
49pub(crate) fn push_tiles(cells: &mut Vec<AnalogCell>, tiles: Vec<AnalogCell>) -> usize {
50    let mut last = 0usize;
51    for mut t in tiles {
52        let prev = cells.len().wrapping_sub(1);
53        let fix = |s: &mut ASrc| {
54            if matches!(s, ASrc::Val(v) if *v == PREV_TILE) {
55                *s = ASrc::Val(prev);
56            }
57        };
58        match &mut t {
59            AnalogCell::Sum { inputs, .. } => inputs.iter_mut().for_each(fix),
60            AnalogCell::Mul { a, b } => {
61                fix(a);
62                fix(b);
63            }
64            AnalogCell::GainConst { a, .. } => fix(a),
65            AnalogCell::Select { c, t, f } => {
66                fix(c);
67                fix(t);
68                fix(f);
69            }
70            AnalogCell::Exp { a }
71            | AnalogCell::Log { a }
72            | AnalogCell::Tanh { a }
73            | AnalogCell::Sqrt { a } => fix(a),
74            AnalogCell::Div { a, b } => {
75                fix(a);
76                fix(b);
77            }
78        }
79        cells.push(t);
80        last = cells.len() - 1;
81    }
82    last
83}
84
85/// Map ONE fabric float cell to an analog tile — the shared vocabulary of the decision-fabric
86/// and reduction lanes. `asrc` resolves fabric operands into analog operands (each lane has its
87/// own input convention). `MemRead`/heap/state cells are the CALLER's job to intercept (the
88/// reduction lane maps the streamed element read); reaching here they refuse.
89pub(crate) fn map_float_cell(
90    cell: &LoopCell,
91    asrc: &dyn Fn(&Src) -> ASrc,
92) -> Result<MappedCell, AnaLowerError> {
93    Ok(match cell {
94        LoopCell::Func { unit: FuncUnit::FAddF, a, b } => {
95            MappedCell::Tile(AnalogCell::Sum { inputs: vec![asrc(a), asrc(b)], weights: vec![1.0, 1.0] })
96        }
97        LoopCell::Func { unit: FuncUnit::FSubF, a, b } => {
98            MappedCell::Tile(AnalogCell::Sum { inputs: vec![asrc(a), asrc(b)], weights: vec![1.0, -1.0] })
99        }
100        LoopCell::Func { unit: FuncUnit::FMulF, a, b } => {
101            let (aa, bb) = (asrc(a), asrc(b));
102            MappedCell::Tile(match (aa, bb) {
103                (ASrc::Const(k), other) | (other, ASrc::Const(k)) => AnalogCell::GainConst { a: other, k },
104                _ => AnalogCell::Mul { a: aa, b: bb },
105            })
106        }
107        // INT→FLOAT of a compile-time constant folds to a programmed reference; a RUNTIME
108        // integer has no voltage representation and refuses.
109        LoopCell::Func { unit: FuncUnit::IntToF, a, .. } => match a {
110            Src::Const(c) => MappedCell::Fold(*c as f64),
111            _ => {
112                return Err(AnaLowerError::UnloweredOp(
113                    "int→float promotion of a runtime value (no voltage representation for a digital word mid-plane)".to_string(),
114                ))
115            }
116        },
117        LoopCell::Func { unit: FuncUnit::FDivF, a, b } => {
118            // O6: the translinear ratio tile — the divisor's certified box must clear zero
119            // (the schedule's range rules refuse `DivisorRangeSpansZero` otherwise).
120            MappedCell::Tile(AnalogCell::Div { a: asrc(a), b: asrc(b) })
121        }
122        LoopCell::Func { unit: FuncUnit::FSqrtF, a, .. } => {
123            MappedCell::Tile(AnalogCell::Sqrt { a: asrc(a) })
124        }
125        LoopCell::Func { unit, .. } => {
126            return Err(AnaLowerError::UnloweredOp(format!("functional unit {unit:?} has no witnessed analog tile")))
127        }
128        LoopCell::Arith { op, .. } => {
129            return Err(AnaLowerError::UnloweredOp(format!("integer op {op:?} inside an analog region")))
130        }
131        LoopCell::Select { cmp, ca, cb, t, e } => {
132            // Normalize the decision to `d < 0` with `d = x − y`; strict vs inclusive is
133            // indistinguishable on the analog plane (the boundary lives inside the
134            // comparator's ambiguity band, priced by `CompareBand`). Equality is a
135            // measure-zero decision no continuous comparator realizes — refused.
136            use crate::codegen_sva::fabric_rtl::CmpKind;
137            let (x, y, tt, ff) = match cmp {
138                CmpKind::Lt | CmpKind::Le => (asrc(ca), asrc(cb), asrc(t), asrc(e)),
139                CmpKind::Gt | CmpKind::Ge => (asrc(cb), asrc(ca), asrc(t), asrc(e)),
140                CmpKind::Eq | CmpKind::Ne => {
141                    return Err(AnaLowerError::UnloweredOp(
142                        "equality select: a continuous comparator has no measure-zero decision"
143                            .to_string(),
144                    ))
145                }
146            };
147            MappedCell::Tiles(vec![
148                AnalogCell::Sum { inputs: vec![x, y], weights: vec![1.0, -1.0] },
149                AnalogCell::Select { c: ASrc::Val(PREV_TILE), t: tt, f: ff },
150            ])
151        }
152        LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. }
153        | LoopCell::HeapAlloc { .. } => {
154            return Err(AnaLowerError::UnloweredOp("memory/state-plane access inside an analog region".to_string()))
155        }
156        LoopCell::VectorArith { .. } => {
157            return Err(AnaLowerError::UnloweredOp("packed-SIMD op inside an analog region".to_string()))
158        }
159    })
160}
161
162/// Lower `fn_name` in `src` to an analog tile netlist realizable on `target`.
163///
164/// Refusals (fail-closed, in gate order): no float parameter → [`AnaLowerError::NoAnalogRegion`];
165/// a non-float parameter → [`AnaLowerError::UnloweredOp`]; a float parameter without a declared
166/// range in the function body → [`AnaLowerError::UnboundedInput`] naming it; an op outside the
167/// witnessed tile vocabulary → [`AnaLowerError::UnloweredOp`]; a netlist the target cannot
168/// realize → [`AnaLowerError::Unplaceable`].
169pub fn analog_netlist_from_source(
170    src: &str,
171    fn_name: &str,
172    target: &AnalogTarget,
173) -> Result<AnalogNetlist, AnaLowerError> {
174    // ── Gate 1: the function's parameter contract (from the ORIGINAL source). ──
175    check_params(src, fn_name)?; // ranges re-fetched by the schedule pass via `region_param_ranges`
176
177    // ── Extract: the certified fabric front-end on the contract-free twin. ──
178    let twin = source_without_range_contracts(src);
179    let df = decision_fabric_from_source(&twin, fn_name);
180    if df.escaped_first_return {
181        return Err(AnaLowerError::UnloweredOp(
182            "early-return branch shape: a branch continues past the first Return, and a \
183             tolerance-verified lane cannot detect a dropped branch — write the conditional \
184             as Let/Set so it if-converts"
185                .to_string(),
186        ));
187    }
188
189    // ── Lower: fabric cells → analog tiles, or a named refusal. ──
190    let mut cells: Vec<AnalogCell> = Vec::new();
191    let mut map: BTreeMap<usize, NetRef> = BTreeMap::new();
192
193    let asrc = |s: &Src, map: &BTreeMap<usize, NetRef>| -> ASrc {
194        match s {
195            Src::In(k) => ASrc::In(*k),
196            Src::Const(bits) => ASrc::Const(f64::from_bits(*bits as u64)),
197            Src::Val(v) => match map.get(v) {
198                Some(NetRef::Cell(c)) => ASrc::Val(*c),
199                Some(NetRef::Const(f)) => ASrc::Const(*f),
200                None => ASrc::Val(usize::MAX), // an unmapped val is caught by the refusal below
201            },
202        }
203    };
204
205    for (i, cell) in df.cells.iter().enumerate() {
206        let resolve = |s: &Src| asrc(s, &map);
207        let mapped = match map_float_cell(cell, &resolve)? {
208            MappedCell::Tile(t) => NetRef::Cell(push(&mut cells, t)),
209            MappedCell::Tiles(ts) => NetRef::Cell(push_tiles(&mut cells, ts)),
210            MappedCell::Fold(v) => NetRef::Const(v),
211        };
212        map.insert(i, mapped);
213    }
214
215    let out = asrc(&df.result, &map);
216    if matches!(out, ASrc::Val(usize::MAX)) {
217        return Err(AnaLowerError::UnloweredOp(
218            "region output references an unlowered value".to_string(),
219        ));
220    }
221
222    let netlist = AnalogNetlist {
223        n_in: df.n_in,
224        cells,
225        out,
226    };
227    gate_on_target(&netlist, target)?;
228    Ok(netlist)
229}
230
231fn push(cells: &mut Vec<AnalogCell>, c: AnalogCell) -> usize {
232    cells.push(c);
233    cells.len() - 1
234}
235
236/// The region's declared per-parameter ranges, IN PARAMETER ORDER, from the function's OWN
237/// body — a contract declared in another function does not bound this region (the multi-region
238/// programs of the mixed-signal partition made whole-source collection a bug).
239pub(crate) fn region_param_ranges(
240    src: &str,
241    fn_name: &str,
242) -> Result<Vec<super::AssumedRange>, AnaLowerError> {
243    check_params(src, fn_name)
244}
245
246/// Gate 1: every parameter must be a float with a declared range in the function's own body.
247/// Returns the ranges in parameter order.
248fn check_params(src: &str, fn_name: &str) -> Result<Vec<super::AssumedRange>, AnaLowerError> {
249    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
250        let (stmts, _types, _pol) = match parsed {
251            Ok(t) => t,
252            Err(advice) => {
253                return Err(AnaLowerError::UnloweredOp(format!(
254                    "parse failed: {advice}"
255                )))
256            }
257        };
258        let Some((params, body)) = stmts.iter().find_map(|s| match s {
259            Stmt::FunctionDef {
260                name, params, body, ..
261            } if interner.resolve(*name) == fn_name => Some((params, *body)),
262            _ => None,
263        }) else {
264            return Err(AnaLowerError::UnloweredOp(format!(
265                "function `{fn_name}` not found"
266            )));
267        };
268
269        let is_float = |t: &TypeExpr<'_>| match t {
270            TypeExpr::Primitive(s) | TypeExpr::Named(s) => {
271                matches!(interner.resolve(*s), "Float" | "Real")
272            }
273            _ => false,
274        };
275        if !params.iter().any(|(_, t)| is_float(t)) {
276            return Err(AnaLowerError::NoAnalogRegion);
277        }
278        // The ranges declared in THIS function's body — a contract elsewhere does not bound us.
279        let ranges = assumed_ranges_in(body, interner);
280        let mut ordered = Vec::with_capacity(params.len());
281        for (p, t) in params.iter() {
282            let pname = interner.resolve(*p);
283            if !is_float(t) {
284                return Err(AnaLowerError::UnloweredOp(format!(
285                    "non-float parameter `{pname}` (no voltage representation)"
286                )));
287            }
288            let Some(r) = ranges.iter().find(|r| r.var == pname) else {
289                return Err(AnaLowerError::UnboundedInput(format!(
290                    "parameter `{pname}` has no declared range (`Assume {pname} is between A and B.`)"
291                )));
292            };
293            if r.lo > r.hi {
294                return Err(AnaLowerError::ContradictoryContract {
295                    var: r.var.clone(),
296                    lo: r.lo,
297                    hi: r.hi,
298                });
299            }
300            ordered.push(r.clone());
301        }
302        Ok(ordered)
303    })
304}
305
306/// The contract-free twin: the `Assume` range checks belong to the digital control plane (the
307/// host validates each word BEFORE driving the DAC), so the analog dataflow is extracted without
308/// them. The statement is line-shaped in block grammar, so a line filter is exact for the
309/// canonical form; a `Require that` stays put (and refuses loudly downstream if it reaches the
310/// fabric front-end) — only the range-declaration surface moves planes.
311pub(crate) fn source_without_range_contracts(src: &str) -> String {
312    src.lines()
313        .filter(|l| !l.trim_start().starts_with("Assume "))
314        .collect::<Vec<_>>()
315        .join("\n")
316}
317
318/// Gate 3: the target must provide every tile kind, enough multiplier tiles (the scarce
319/// resource), enough DAC/ADC channels, and enough array room.
320pub(crate) fn gate_on_target(
321    netlist: &AnalogNetlist,
322    target: &AnalogTarget,
323) -> Result<(), AnaLowerError> {
324    for cell in &netlist.cells {
325        let cap = AnalogCapability::Cell(cell.kind());
326        if !target.provides(cap) {
327            return Err(AnaLowerError::Unplaceable(
328                AnaUnplaceable::MissingCapability {
329                    needed: cap,
330                    target: target.name,
331                },
332            ));
333        }
334    }
335    let n_mul = netlist
336        .cells
337        .iter()
338        .filter(|c| matches!(c, super::cell::AnalogCell::Mul { .. }))
339        .count();
340    if n_mul > 0 && !target.provides(AnalogCapability::MulTiles(n_mul)) {
341        return Err(AnaLowerError::Unplaceable(
342            AnaUnplaceable::MissingCapability {
343                needed: AnalogCapability::MulTiles(n_mul),
344                target: target.name,
345            },
346        ));
347    }
348    for (cap_needed, _n) in [
349        (AnalogCapability::DacChannels(netlist.n_in), netlist.n_in),
350        (AnalogCapability::AdcChannels(1), 1),
351    ] {
352        if !target.provides(cap_needed) {
353            return Err(AnaLowerError::Unplaceable(
354                AnaUnplaceable::MissingCapability {
355                    needed: cap_needed,
356                    target: target.name,
357                },
358            ));
359        }
360    }
361    if netlist.cells.len() > target.rows * target.cols {
362        return Err(AnaLowerError::Unplaceable(AnaUnplaceable::TooSmall {
363            needed_cells: netlist.cells.len(),
364            target_rows: target.rows,
365            target_cols: target.cols,
366            target: target.name,
367        }));
368    }
369    Ok(())
370}