1use std::collections::BTreeMap;
20
21use crate::ast::stmt::{Pattern, Stmt, TypeExpr};
22use crate::codegen_sva::fabric_rtl::{loop_fabric_from_source, LoopCell, Src};
23
24use super::cell::{ASrc, AnalogCell, AnalogNetlist};
25use super::control::{ControlProgram, CtrlStep};
26use super::error_budget::{outward_sum, ErrorSource, ErrorTerm, OutputBudget};
27use super::eval::eval_analog_dc;
28use super::gain::{
29 propagate_bounds, quantize_weights, select_scale, src_bound_of, GainSchedule, MathBound,
30 NetBound, RangeCertificate,
31};
32use super::lower::{map_float_cell, source_without_range_contracts, MappedCell, NetRef};
33use super::target::{AnaUnplaceable, AnalogCapability, AnalogCellKind, AnalogTarget};
34use super::{assumed_len_ranges_in, assumed_ranges_in, AnaLowerError, AssumedRange};
35
36#[derive(Clone, Debug, PartialEq)]
39pub struct AnalogReduction {
40 pub tick: AnalogNetlist,
41 pub init: f64,
43 pub min_len: usize,
45 pub max_len: usize,
46 pub elem: AssumedRange,
48 pub control: ControlProgram,
49}
50
51#[derive(Clone, Copy, PartialEq, Eq)]
54enum Lane {
55 Float,
56 Int,
57}
58
59pub fn analog_reduction_from_source(
62 src: &str,
63 fn_name: &str,
64 target: &AnalogTarget,
65) -> Result<AnalogReduction, AnaLowerError> {
66 reduction_impl(src, fn_name, target, Lane::Float)
67}
68
69pub fn analog_int_reduction_from_source(
73 src: &str,
74 fn_name: &str,
75 target: &AnalogTarget,
76) -> Result<AnalogReduction, AnaLowerError> {
77 reduction_impl(src, fn_name, target, Lane::Int)
78}
79
80fn reduction_impl(
81 src: &str,
82 fn_name: &str,
83 target: &AnalogTarget,
84 lane: Lane,
85) -> Result<AnalogReduction, AnaLowerError> {
86 let (elem, min_len, max_len) = check_reduction_contracts(src, fn_name, lane)?;
87
88 let twin = source_without_range_contracts(src);
89 let lf = loop_fabric_from_source(&twin, fn_name);
90
91 if lf.seq_input.is_none() {
93 return Err(AnaLowerError::UnloweredOp(
94 "not a sequence loop (no streamed input)".to_string(),
95 ));
96 }
97 if lf.n_state != 1 {
98 return Err(AnaLowerError::UnloweredOp(format!(
99 "{} loop-carried accumulators (the streamed lane holds exactly one in the S&H)",
100 lf.n_state
101 )));
102 }
103 if !lf.epilogue.is_empty() || !lf.prologue_heap.is_empty() || !lf.invariants.is_empty() {
104 return Err(AnaLowerError::UnloweredOp(
105 "loop epilogue/prologue/invariants are not yet streamed".to_string(),
106 ));
107 }
108 let init = match lf.init[0] {
109 Src::Const(bits) => match lane {
110 Lane::Float => f64::from_bits(bits as u64),
111 Lane::Int => bits as f64,
112 },
113 _ => {
114 return Err(AnaLowerError::UnloweredOp(
115 "accumulator initial value must be a compile-time constant".to_string(),
116 ))
117 }
118 };
119
120 for cell in &lf.body {
123 let uses_iv_as_value = |s: &Src| matches!(s, Src::In(k) if *k >= 1);
124 let illegal = match cell {
125 LoopCell::MemRead { .. } => false,
126 LoopCell::Arith { a, b, .. }
127 | LoopCell::Func { a, b, .. }
128 | LoopCell::VectorArith { a, b, .. } => uses_iv_as_value(a) || uses_iv_as_value(b),
129 LoopCell::Select { ca, cb, t, e, .. } => {
130 uses_iv_as_value(ca)
131 || uses_iv_as_value(cb)
132 || uses_iv_as_value(t)
133 || uses_iv_as_value(e)
134 }
135 LoopCell::HeapRead { addr } => uses_iv_as_value(addr),
136 LoopCell::HeapWrite { addr, val } => uses_iv_as_value(addr) || uses_iv_as_value(val),
137 LoopCell::HeapAlloc { size } => uses_iv_as_value(size),
138 };
139 if illegal {
140 return Err(AnaLowerError::UnloweredOp(
141 "the induction variable used as a value (only the element stream is analog)"
142 .to_string(),
143 ));
144 }
145 }
146
147 let mut map: BTreeMap<usize, NetRef> = BTreeMap::new();
149 for (i, cell) in lf.body.iter().enumerate() {
150 if let LoopCell::MemRead { addr } = cell {
151 if !matches!(addr, Src::In(1)) {
152 return Err(AnaLowerError::UnloweredOp(
153 "sequence read off the stream head (only `xs[iv]` streams)".to_string(),
154 ));
155 }
156 map.insert(i, NetRef::Cell(usize::MAX)); }
158 }
159 let mut cells: Vec<AnalogCell> = Vec::new();
160 let resolve_with = |map: &BTreeMap<usize, NetRef>, s: &Src| -> ASrc {
161 match s {
162 Src::In(0) => ASrc::In(1), Src::In(_) => ASrc::In(1), Src::Const(bits) => ASrc::Const(match lane {
165 Lane::Float => f64::from_bits(*bits as u64),
166 Lane::Int => *bits as f64,
167 }),
168 Src::Val(v) => match map.get(v) {
169 Some(NetRef::Cell(c)) if *c == usize::MAX => ASrc::In(0), Some(NetRef::Cell(c)) => ASrc::Val(*c),
171 Some(NetRef::Const(f)) => ASrc::Const(*f),
172 None => ASrc::Val(usize::MAX),
173 },
174 }
175 };
176 for (i, cell) in lf.body.iter().enumerate() {
177 if matches!(cell, LoopCell::MemRead { .. }) {
178 continue;
179 }
180 let resolve = |s: &Src| resolve_with(&map, s);
181 let mapped = match lane {
182 Lane::Float => map_float_cell(cell, &resolve)?,
183 Lane::Int => super::int_lane::map_int_cell(cell, &resolve)?,
184 };
185 let mapped = match mapped {
186 MappedCell::Tile(t) => NetRef::Cell(push(&mut cells, t)),
187 MappedCell::Tiles(ts) => NetRef::Cell(super::lower::push_tiles(&mut cells, ts)),
188 MappedCell::Fold(v) => NetRef::Const(v),
189 };
190 map.insert(i, mapped);
191 }
192 let out = resolve_with(&map, &lf.next[0]);
193 if matches!(out, ASrc::Val(usize::MAX)) {
194 return Err(AnaLowerError::UnloweredOp(
195 "accumulator update references an unlowered value".to_string(),
196 ));
197 }
198 let tick = AnalogNetlist {
199 n_in: 2,
200 cells,
201 out,
202 };
203 if lane == Lane::Int {
204 super::int_lane::assert_grid_preserving(&tick)?;
205 if init.fract() != 0.0 {
206 return Err(AnaLowerError::UnloweredOp(
207 "non-integer accumulator initial value in the integer lane".to_string(),
208 ));
209 }
210 }
211
212 gate_reduction_on_target(&tick, target)?;
213
214 let control = ControlProgram {
215 frame: vec![
216 CtrlStep::LoadDac { ch: 0 },
217 CtrlStep::Settle,
218 CtrlStep::SampleHold,
219 ],
220 frames: max_len,
221 epilogue: vec![CtrlStep::ReadAdc { ch: 0 }],
222 tick_ns: target.settle_ns.0,
223 };
224 Ok(AnalogReduction {
225 tick,
226 init,
227 min_len,
228 max_len,
229 elem,
230 control,
231 })
232}
233
234fn push(cells: &mut Vec<AnalogCell>, c: AnalogCell) -> usize {
235 cells.push(c);
236 cells.len() - 1
237}
238
239fn check_reduction_contracts(
242 src: &str,
243 fn_name: &str,
244 lane: Lane,
245) -> Result<(AssumedRange, usize, usize), AnaLowerError> {
246 crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
247 let (stmts, _types, _pol) = match parsed {
248 Ok(t) => t,
249 Err(advice) => {
250 return Err(AnaLowerError::UnloweredOp(format!(
251 "parse failed: {advice}"
252 )))
253 }
254 };
255 let Some((params, body)) = stmts.iter().find_map(|s| match s {
256 Stmt::FunctionDef {
257 name, params, body, ..
258 } if interner.resolve(*name) == fn_name => Some((params, *body)),
259 _ => None,
260 }) else {
261 return Err(AnaLowerError::UnloweredOp(format!(
262 "function `{fn_name}` not found"
263 )));
264 };
265 if params.len() != 1 {
266 return Err(AnaLowerError::UnloweredOp(
267 "the streamed lane takes exactly one sequence parameter".to_string(),
268 ));
269 }
270 let (pname_sym, pty) = ¶ms[0];
271 let pname = interner.resolve(*pname_sym);
272 let elem_matches = |s: &str| match lane {
273 Lane::Float => matches!(s, "Float" | "Real"),
274 Lane::Int => matches!(s, "Int" | "Nat"),
275 };
276 let is_lane_seq = matches!(
277 pty,
278 TypeExpr::Generic { base, params: ps }
279 if matches!(interner.resolve(*base), "Seq" | "List")
280 && ps.len() == 1
281 && matches!(&ps[0], TypeExpr::Primitive(s) | TypeExpr::Named(s)
282 if elem_matches(interner.resolve(*s)))
283 );
284 if !is_lane_seq {
285 return Err(AnaLowerError::NoAnalogRegion);
286 }
287
288 let len_ranges = assumed_len_ranges_in(body, interner);
289 let Some(len) = len_ranges.iter().find(|r| r.var == pname) else {
290 return Err(AnaLowerError::UnboundedInput(format!(
291 "sequence `{pname}` has no declared length range (`Assume length of {pname} is between A and B.`) — \
292 a budget over an unbounded stream would be a guess"
293 )));
294 };
295 if len.lo < 0 || len.hi < len.lo || len.hi == 0 {
296 return Err(AnaLowerError::UnboundedInput(format!(
297 "length range [{}, {}] is not a usable frame bound",
298 len.lo, len.hi
299 )));
300 }
301
302 let loop_var = body.iter().find_map(|s| match s {
304 Stmt::Repeat {
305 pattern: Pattern::Identifier(v),
306 ..
307 } => Some(*v),
308 _ => None,
309 });
310 let Some(loop_var) = loop_var else {
311 return Err(AnaLowerError::UnloweredOp(
312 "no `Repeat for each` loop found".to_string(),
313 ));
314 };
315 let vname = interner.resolve(loop_var);
316 let ranges = assumed_ranges_in(body, interner);
317 let Some(elem) = ranges.iter().find(|r| r.var == vname) else {
318 return Err(AnaLowerError::UnboundedInput(format!(
319 "element `{vname}` has no declared range (`Assume {vname} is between A and B.`)"
320 )));
321 };
322 if lane == Lane::Int && (elem.lo.fract() != 0.0 || elem.hi.fract() != 0.0) {
323 return Err(AnaLowerError::UnboundedInput(format!(
324 "element `{vname}`'s range [{}, {}] is not an integer range",
325 elem.lo, elem.hi
326 )));
327 }
328 Ok((elem.clone(), len.lo as usize, len.hi as usize))
329 })
330}
331
332fn gate_reduction_on_target(
333 tick: &AnalogNetlist,
334 target: &AnalogTarget,
335) -> Result<(), AnaLowerError> {
336 let mut need = vec![
337 AnalogCapability::Cell(AnalogCellKind::SampleHold),
338 AnalogCapability::DacChannels(1),
339 AnalogCapability::AdcChannels(1),
340 ];
341 for cell in &tick.cells {
342 need.push(AnalogCapability::Cell(cell.kind()));
343 }
344 for cap in need {
345 if !target.provides(cap) {
346 return Err(AnaLowerError::Unplaceable(
347 AnaUnplaceable::MissingCapability {
348 needed: cap,
349 target: target.name,
350 },
351 ));
352 }
353 }
354 let n_mul = tick
355 .cells
356 .iter()
357 .filter(|c| matches!(c, AnalogCell::Mul { .. }))
358 .count();
359 if n_mul > 0 && !target.provides(AnalogCapability::MulTiles(n_mul)) {
360 return Err(AnaLowerError::Unplaceable(
361 AnaUnplaceable::MissingCapability {
362 needed: AnalogCapability::MulTiles(n_mul),
363 target: target.name,
364 },
365 ));
366 }
367 if tick.cells.len() > target.rows * target.cols {
368 return Err(AnaLowerError::Unplaceable(AnaUnplaceable::TooSmall {
369 needed_cells: tick.cells.len(),
370 target_rows: target.rows,
371 target_cols: target.cols,
372 target: target.name,
373 }));
374 }
375 Ok(())
376}
377
378pub fn eval_analog_sampled(r: &AnalogReduction, xs: &[f64]) -> f64 {
381 let mut acc = r.init;
382 for x in xs {
383 acc = eval_analog_dc(&r.tick, &[*x, acc]);
384 }
385 acc
386}
387
388pub fn eval_analog_sampled_quantized(
392 r: &AnalogReduction,
393 schedule: &GainSchedule,
394 target: &AnalogTarget,
395 xs: &[f64],
396) -> f64 {
397 let grid = super::eval::ConverterGrid::of(schedule, target);
398 let mut acc = r.init;
399 for x in xs {
400 acc = eval_analog_dc(&schedule.quantized, &[grid.dac_q(*x), acc]);
401 }
402 grid.adc_q(acc)
403}
404
405pub fn reduction_scale_pass(
410 r: &AnalogReduction,
411 target: &AnalogTarget,
412) -> Result<(GainSchedule, RangeCertificate), AnaLowerError> {
413 let quantized = quantize_weights(&r.tick, target)?;
414 let elem = MathBound {
415 lo: r.elem.lo,
416 hi: r.elem.hi,
417 };
418 let mut acc = MathBound {
419 lo: r.init,
420 hi: r.init,
421 };
422 let mut hulls: Vec<MathBound> = vec![
423 MathBound {
424 lo: f64::INFINITY,
425 hi: f64::NEG_INFINITY
426 };
427 quantized.cells.len()
428 ];
429 let mut acc_hull = acc;
430 for _ in 0..r.max_len {
431 let inputs = [elem, acc];
432 let bounds = propagate_bounds(&quantized.cells, &inputs)?;
433 for (h, b) in hulls.iter_mut().zip(&bounds) {
434 h.lo = h.lo.min(b.lo);
435 h.hi = h.hi.max(b.hi);
436 }
437 acc = src_bound_of(&quantized.out, &inputs, &bounds);
438 acc_hull.lo = acc_hull.lo.min(acc.lo);
439 acc_hull.hi = acc_hull.hi.max(acc.hi);
440 if !(acc.lo.is_finite() && acc.hi.is_finite()) {
441 return Err(AnaLowerError::UnloweredOp(
442 "accumulator range diverges over the declared frame count".to_string(),
443 ));
444 }
445 }
446 for h in hulls.iter_mut() {
448 if h.lo > h.hi {
449 *h = MathBound { lo: 0.0, hi: 0.0 };
450 }
451 }
452
453 let has_mul = quantized
454 .cells
455 .iter()
456 .any(|c| matches!(c, AnalogCell::Mul { .. }));
457 let input_bounds = vec![elem, acc_hull];
458 let all: Vec<MathBound> = input_bounds.iter().chain(hulls.iter()).copied().collect();
459 let scale_log2 = select_scale(&all, has_mul, target)?;
460 let s = f64::powi(2.0, scale_log2);
461
462 let mut per_net = Vec::with_capacity(hulls.len());
463 for b in &hulls {
464 per_net.push(NetBound {
465 math: *b,
466 volt_lo: (b.lo * s).next_down(),
467 volt_hi: (b.hi * s).next_up(),
468 });
469 }
470 let per_net_scale_log2 = vec![scale_log2; quantized.cells.len()];
471 let schedule = GainSchedule {
472 scale_log2,
473 out_descale_log2: -scale_log2,
474 mul_vref_v: s,
475 quantized,
476 per_net_scale_log2,
477 };
478 let cert = RangeCertificate {
479 input_bounds,
480 per_net,
481 rail_half_span_v: target.half_span_v(),
482 noise_floor_v: 0.0,
483 min_full_scale_over_noise: 1.0,
484 };
485 Ok((schedule, cert))
486}
487
488pub fn reduction_error_budget(
492 r: &AnalogReduction,
493 cert: &RangeCertificate,
494 schedule: &GainSchedule,
495 target: &AnalogTarget,
496) -> OutputBudget {
497 use super::error_budget::{eval_rounding_guard, half_lsb, out_lanes, propagate_eps_pass};
498 let dac_eps = half_lsb(cert, schedule, target.dac_bits);
499 let adc_eps = half_lsb(cert, schedule, target.adc_bits);
500 let bound_of = |src: &ASrc| -> MathBound {
501 match src {
502 ASrc::In(k) => cert.input_bounds[*k],
503 ASrc::Val(v) => cert.per_net[*v].math,
504 ASrc::Const(c) => MathBound { lo: *c, hi: *c },
505 }
506 };
507 let mut acc_eps = vec![0.0_f64; 3];
509 for _ in 0..r.max_len {
510 let carried = acc_eps.clone();
511 let input_eps = |src: &ASrc, lane: usize| -> f64 {
512 match src {
513 ASrc::In(0) if lane == 0 => dac_eps,
514 ASrc::In(0) => 0.0,
515 _ => carried[lane],
516 }
517 };
518 let input_total = |src: &ASrc| -> f64 {
519 match src {
520 ASrc::In(0) => dac_eps,
521 _ => outward_sum(carried.iter().copied()),
522 }
523 };
524 let pass = propagate_eps_pass(
525 &r.tick.cells,
526 &schedule.quantized.cells,
527 &bound_of,
528 3,
529 1,
530 2,
531 &input_eps,
532 &input_total,
533 );
534 acc_eps = out_lanes(&pass, &schedule.quantized.out, 3, &input_eps);
535 }
536
537 let mut terms = Vec::new();
538 if acc_eps[0] > 0.0 {
539 terms.push(ErrorTerm {
540 eps_math: acc_eps[0],
541 source: ErrorSource::DacQuant {
542 input: 0,
543 bits: target.dac_bits,
544 },
545 });
546 }
547 if acc_eps[1] > 0.0 {
548 terms.push(ErrorTerm {
549 eps_math: acc_eps[1],
550 source: ErrorSource::WeightQuant,
551 });
552 }
553 if acc_eps[2] > 0.0 {
554 terms.push(ErrorTerm {
555 eps_math: acc_eps[2],
556 source: ErrorSource::MulInteraction,
557 });
558 }
559 terms.push(ErrorTerm {
560 eps_math: adc_eps,
561 source: ErrorSource::AdcQuant {
562 bits: target.adc_bits,
563 },
564 });
565 terms.push(ErrorTerm {
566 eps_math: eval_rounding_guard(cert, r.max_len * schedule.quantized.cells.len()),
567 source: ErrorSource::EvalRounding,
568 });
569 let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
570 OutputBudget { eps_math, terms }
571}