1pub mod calibrate;
33pub mod cell;
34pub mod control;
35pub mod dims;
36pub mod dynamics;
37pub mod effects;
38pub mod error_budget;
39pub mod eval;
40pub mod gain;
41pub mod int_lane;
42pub mod integrator;
43pub mod lower;
44pub mod nonideal;
45pub mod ontology;
46pub mod place;
47pub mod reduction;
48pub mod spice_emit;
49pub mod stack;
50pub mod stack_place;
51pub mod target;
52pub mod thermal;
53pub mod wta;
54pub mod xbar;
55
56pub use calibrate::{
57 apply_calibration, calibrate, error_budget_calibrated, v1_trim, CalibrationTable, TrimSpec,
58};
59pub use cell::{ASrc, AnalogCell, AnalogNetlist};
60pub use control::{ControlProgram, CtrlStep};
61pub use error_budget::{
62 error_budget, error_budget_chopped, error_budget_nonideal, ErrorSource, ErrorTerm, OutputBudget,
63};
64pub use eval::{eval_analog_dc, eval_analog_quantized, ConverterGrid};
65pub use gain::{scale_pass, to_range_claim, GainSchedule, MathBound, NetBound, RangeCertificate};
66pub use int_lane::analog_int_netlist_from_source;
67pub use integrator::{
68 analog_integrator_from_source, eval_analog_integrated, eval_analog_integrated_quantized,
69 integrator_error_budget, integrator_scale_pass, AnalogIntegrator, InitSrc,
70};
71pub use lower::analog_netlist_from_source;
72pub use nonideal::{
73 eval_analog_instance, eval_analog_instance_chopped, sample_instance, sky130_nonideal,
74 InstanceSample, NonIdealityModel, KSIGMA,
75};
76pub use place::{place_analog, place_blind, AnalogPlacement};
77pub use reduction::{
78 analog_int_reduction_from_source, analog_reduction_from_source, eval_analog_sampled,
79 eval_analog_sampled_quantized, reduction_error_budget, reduction_scale_pass, AnalogReduction,
80};
81pub use spice_emit::{emit_spice_dc, SPICE_ABSTOL, SPICE_RELTOL, SPICE_VNTOL};
82pub use target::{
83 AnaUnplaceable, AnalogCapability, AnalogCellKind, AnalogTarget, ALL_ANALOG_CELL_KINDS,
84};
85pub use xbar::{fold_crossbar, fold_crossbar_for};
86
87use crate::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt};
88use crate::intern::Interner;
89use crate::vm::{CompiledProgram, Constant};
90
91#[derive(Debug, Clone, PartialEq)]
95pub struct AnalogArtifact {
96 pub fn_name: String,
97 pub netlist: AnalogNetlist,
98 pub schedule: GainSchedule,
99 pub cert: RangeCertificate,
100 pub budget: OutputBudget,
101 pub placement: AnalogPlacement,
102 pub spice: String,
103}
104
105pub fn contract_free_twin(src: &str) -> String {
111 lower::source_without_range_contracts(src)
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum RegionLane {
117 Float,
119 Int,
121}
122
123pub fn try_region(
129 src: &str,
130 fn_name: &str,
131 target: &AnalogTarget,
132) -> Result<(RegionLane, AnalogArtifact), AnaLowerError> {
133 let guard = |f: &dyn Fn() -> Result<AnalogArtifact, AnaLowerError>| {
134 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| {
135 Err(AnaLowerError::UnloweredOp(
136 "region outside the supported extraction family".to_string(),
137 ))
138 })
139 };
140 match guard(&|| analog_artifact_from_source(src, fn_name, target)) {
141 Ok(art) => Ok((RegionLane::Float, art)),
142 Err(AnaLowerError::NoAnalogRegion) => {
144 guard(&|| analog_int_artifact_from_source(src, fn_name, target))
145 .map(|a| (RegionLane::Int, a))
146 }
147 Err(e) => Err(e),
148 }
149}
150
151pub fn analog_int_artifact_from_source(
156 src: &str,
157 fn_name: &str,
158 target: &AnalogTarget,
159) -> Result<AnalogArtifact, AnaLowerError> {
160 let netlist = int_lane::analog_int_netlist_from_source(src, fn_name, target)?;
161 let ranges = int_lane::int_region_param_ranges(src, fn_name)?;
162 let (schedule, cert) = scale_pass(&netlist, &ranges, target)?;
163 let placement = place_analog(&schedule.quantized, target)?;
164 let model = sky130_nonideal();
165 let spec = calibrate::v1_trim(target);
166 let budget = error_budget_calibrated(
167 &netlist, &cert, &schedule, target, &model, &placement, &spec,
168 );
169 exactness_gate(&budget)?;
170 let mid: Vec<f64> = ranges
171 .iter()
172 .map(|r| ((r.lo + r.hi) / 2.0).round())
173 .collect();
174 let spice = spice_emit::emit_spice_dc(&schedule, target, &mid, "out.txt");
175 Ok(AnalogArtifact {
176 fn_name: fn_name.to_string(),
177 netlist,
178 schedule,
179 cert,
180 budget,
181 placement,
182 spice,
183 })
184}
185
186pub fn analog_artifact_from_source(
189 src: &str,
190 fn_name: &str,
191 target: &AnalogTarget,
192) -> Result<AnalogArtifact, AnaLowerError> {
193 let netlist = analog_netlist_from_source(src, fn_name, target)?;
194 let ranges = lower::region_param_ranges(src, fn_name)?;
195 let (schedule, cert) = scale_pass(&netlist, &ranges, target)?;
196 let placement = place_analog(&schedule.quantized, target)?;
197 let model = sky130_nonideal();
198 let budget = error_budget_nonideal(&netlist, &cert, &schedule, target, &model, &placement);
199 let mid: Vec<f64> = ranges.iter().map(|r| (r.lo + r.hi) / 2.0).collect();
200 let spice = spice_emit::emit_spice_dc(&schedule, target, &mid, "out.txt");
201 Ok(AnalogArtifact {
202 fn_name: fn_name.to_string(),
203 netlist,
204 schedule,
205 cert,
206 budget,
207 placement,
208 spice,
209 })
210}
211
212#[derive(Debug, Clone, PartialEq)]
214pub enum AnaLowerError {
215 Unsupported(&'static str),
217 NoAnalogRegion,
219 UnboundedInput(String),
222 UnloweredOp(String),
225 ExactnessUnprovable { eps_math: f64 },
229 ContradictoryContract { var: String, lo: f64, hi: f64 },
233 ConvergenceUnprovable { reason: String },
236 ThermalModelDisconnected,
239 ThermalBudgetExceeded {
244 node: usize,
245 tier: usize,
246 t_junction_c: f64,
247 t_max_c: f64,
248 cooling: &'static str,
249 },
250 ResidualExceedsBudget { linf: f64, required: f64 },
253 Unplaceable(AnaUnplaceable),
255}
256
257impl std::fmt::Display for AnaLowerError {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 match self {
260 AnaLowerError::Unsupported(what) => write!(f, "unsupported {what}"),
261 AnaLowerError::NoAnalogRegion => {
262 write!(f, "no analog region: the program has no float dataflow to map onto the analog plane")
263 }
264 AnaLowerError::UnboundedInput(msg) => write!(f, "unbounded input: {msg}"),
265 AnaLowerError::UnloweredOp(what) => write!(f, "unlowered op: {what}"),
266 AnaLowerError::ExactnessUnprovable { eps_math } => write!(
267 f,
268 "exactness unprovable: the error budget ε = {eps_math} reaches half an integer \
269 grid step — shrink ranges/depth, add converter bits, or calibrate"
270 ),
271 AnaLowerError::ContradictoryContract { var, lo, hi } => write!(
272 f,
273 "contradictory contract: `{var}` is bounded to [{lo}, {hi}], an empty range — \
274 no value can satisfy it"
275 ),
276 AnaLowerError::ConvergenceUnprovable { reason } => {
277 write!(f, "convergence unprovable: {reason}")
278 }
279 AnaLowerError::ThermalModelDisconnected => write!(
280 f,
281 "thermal model disconnected: heat has nowhere to go (floating island or no \
282 cooling boundary) — no steady state exists"
283 ),
284 AnaLowerError::ThermalBudgetExceeded {
285 node,
286 tier,
287 t_junction_c,
288 t_max_c,
289 cooling,
290 } => write!(
291 f,
292 "thermal budget exceeded: node {node} (tier {tier}) reaches {t_junction_c} degC \
293 against the declared {t_max_c} degC junction ceiling under {cooling} cooling — \
294 use a stronger cooling class or a cooler layout"
295 ),
296 AnaLowerError::ResidualExceedsBudget { linf, required } => write!(
297 f,
298 "residual exceeds budget: ‖A·x̂ − b‖∞ = {linf} > required {required} — \
299 under-settled or the demand is unrealistic; no certificate emitted"
300 ),
301 AnaLowerError::Unplaceable(u) => write!(f, "{u}"),
302 }
303 }
304}
305
306impl std::error::Error for AnaLowerError {}
307
308pub fn unsupported_reason(program: &CompiledProgram) -> Option<&'static str> {
313 use crate::aot_unsupported::Offload;
314 crate::aot_unsupported::first(program).map(|o| match o {
315 Offload::Channel => "channel op (no channels in the mixed-signal SoC model)",
316 Offload::Task => {
317 "task spawn/await (the analog control plane is a frame sequencer, not a thread pool)"
318 }
319 Offload::VirtualTime => "virtual-time / select (needs a cooperative scheduler)",
320 Offload::Net => "networking op (host-only IO)",
321 Offload::Dom => "DOM / browser op (no document in the mixed-signal SoC model)",
322 })
323}
324
325fn has_float_dataflow(program: &CompiledProgram) -> bool {
329 program
330 .constants
331 .iter()
332 .any(|c| matches!(c, Constant::Float(_)))
333}
334
335pub fn analog_refusal(program: &CompiledProgram, ranges: &[AssumedRange]) -> Option<AnaLowerError> {
338 if let Some(reason) = unsupported_reason(program) {
339 return Some(AnaLowerError::Unsupported(reason));
340 }
341 if !has_float_dataflow(program) {
342 return Some(AnaLowerError::NoAnalogRegion);
343 }
344 if ranges.is_empty() {
345 return Some(AnaLowerError::UnboundedInput(
346 "float dataflow is present but no input has a declared range (`Assume x is between A and B.`)"
347 .to_string(),
348 ));
349 }
350 if let Some(r) = ranges.iter().find(|r| r.lo > r.hi) {
351 return Some(AnaLowerError::ContradictoryContract {
352 var: r.var.clone(),
353 lo: r.lo,
354 hi: r.hi,
355 });
356 }
357 None
358}
359
360pub fn exactness_gate(budget: &OutputBudget) -> Result<(), AnaLowerError> {
365 if budget.eps_math < 0.5 {
366 Ok(())
367 } else {
368 Err(AnaLowerError::ExactnessUnprovable {
369 eps_math: budget.eps_math,
370 })
371 }
372}
373
374#[derive(Debug, Clone, PartialEq)]
378pub struct AssumedRange {
379 pub var: String,
380 pub lo: f64,
381 pub hi: f64,
382 pub dim: logicaffeine_base::dimension::Dimension,
387}
388
389pub fn assumed_ranges(source: &str) -> Vec<AssumedRange> {
392 crate::ui_bridge::with_parsed_program(source, |parsed, interner| match parsed {
393 Ok((stmts, _types, _pol)) => assumed_ranges_in(stmts, interner),
394 Err(_) => Vec::new(),
395 })
396}
397
398pub fn assumed_ranges_in(stmts: &[Stmt<'_>], interner: &Interner) -> Vec<AssumedRange> {
402 let mut out = Vec::new();
403 collect_ranges(stmts, interner, &mut out);
404 out
405}
406
407fn collect_ranges(stmts: &[Stmt<'_>], interner: &Interner, out: &mut Vec<AssumedRange>) {
408 for stmt in stmts {
409 match stmt {
410 Stmt::RuntimeAssert {
411 condition,
412 hard: true,
413 dim,
414 reason: _,
415 } => {
416 if let Some((var, lo, hi)) = match_between(condition) {
417 out.push(AssumedRange {
418 var: interner.resolve(var).to_string(),
419 lo,
420 hi,
421 dim: dim.unwrap_or(logicaffeine_base::dimension::Dimension::DIMENSIONLESS),
422 });
423 }
424 }
425 Stmt::FunctionDef { body, .. } => collect_ranges(body, interner, out),
426 Stmt::If {
427 then_block,
428 else_block,
429 ..
430 } => {
431 collect_ranges(then_block, interner, out);
432 if let Some(e) = else_block {
433 collect_ranges(e, interner, out);
434 }
435 }
436 Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
437 collect_ranges(body, interner, out)
438 }
439 _ => {}
440 }
441 }
442}
443
444#[derive(Debug, Clone, PartialEq)]
448pub struct AssumedLenRange {
449 pub var: String,
450 pub lo: i64,
451 pub hi: i64,
452}
453
454pub fn assumed_len_ranges_in(stmts: &[Stmt<'_>], interner: &Interner) -> Vec<AssumedLenRange> {
457 let mut out = Vec::new();
458 collect_len_ranges(stmts, interner, &mut out);
459 out
460}
461
462fn collect_len_ranges(stmts: &[Stmt<'_>], interner: &Interner, out: &mut Vec<AssumedLenRange>) {
463 for stmt in stmts {
464 match stmt {
465 Stmt::RuntimeAssert {
466 condition,
467 hard: true,
468 dim: _,
469 reason: _,
470 } => {
471 if let Some((var, lo, hi)) = match_len_between(condition) {
472 out.push(AssumedLenRange {
473 var: interner.resolve(var).to_string(),
474 lo,
475 hi,
476 });
477 }
478 }
479 Stmt::FunctionDef { body, .. } => collect_len_ranges(body, interner, out),
480 Stmt::If {
481 then_block,
482 else_block,
483 ..
484 } => {
485 collect_len_ranges(then_block, interner, out);
486 if let Some(e) = else_block {
487 collect_len_ranges(e, interner, out);
488 }
489 }
490 Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
491 collect_len_ranges(body, interner, out)
492 }
493 _ => {}
494 }
495 }
496}
497
498fn match_len_between(e: &Expr<'_>) -> Option<(crate::intern::Symbol, i64, i64)> {
500 let Expr::BinaryOp {
501 op: BinaryOpKind::And,
502 left,
503 right,
504 } = e
505 else {
506 return None;
507 };
508 let Expr::BinaryOp {
509 op: BinaryOpKind::LtEq,
510 left: lo,
511 right: lv,
512 } = left
513 else {
514 return None;
515 };
516 let Expr::BinaryOp {
517 op: BinaryOpKind::LtEq,
518 left: rv,
519 right: hi,
520 } = right
521 else {
522 return None;
523 };
524 let (Expr::Length { collection: ca }, Expr::Length { collection: cb }) = (lv, rv) else {
525 return None;
526 };
527 let (Expr::Identifier(a), Expr::Identifier(b)) = (ca, cb) else {
528 return None;
529 };
530 if a != b {
531 return None;
532 }
533 let int_lit = |e: &Expr<'_>| -> Option<i64> {
534 match e {
535 Expr::Literal(Literal::Number(n)) => Some(*n),
536 _ => None,
537 }
538 };
539 Some((*a, int_lit(lo)?, int_lit(hi)?))
540}
541
542fn match_between(e: &Expr<'_>) -> Option<(crate::intern::Symbol, f64, f64)> {
544 let Expr::BinaryOp {
545 op: BinaryOpKind::And,
546 left,
547 right,
548 } = e
549 else {
550 return None;
551 };
552 let Expr::BinaryOp {
553 op: BinaryOpKind::LtEq,
554 left: lo,
555 right: lv,
556 } = left
557 else {
558 return None;
559 };
560 let Expr::BinaryOp {
561 op: BinaryOpKind::LtEq,
562 left: rv,
563 right: hi,
564 } = right
565 else {
566 return None;
567 };
568 let (Expr::Identifier(a), Expr::Identifier(b)) = (lv, rv) else {
569 return None;
570 };
571 if a != b {
572 return None;
573 }
574 Some((*a, numeric_literal(lo)?, numeric_literal(hi)?))
575}
576
577fn numeric_literal(e: &Expr<'_>) -> Option<f64> {
579 match e {
580 Expr::Literal(Literal::Float(f)) => Some(*f),
581 Expr::Literal(Literal::Number(n)) => Some(*n as f64),
582 Expr::BinaryOp {
583 op: BinaryOpKind::Subtract,
584 left,
585 right,
586 } => {
587 match (numeric_literal(left), numeric_literal(right)) {
589 (Some(l), Some(r)) if l == 0.0 => Some(-r),
590 _ => None,
591 }
592 }
593 _ => None,
594 }
595}