1use std::path::Path;
20
21use serde::{Deserialize, Serialize};
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Flow {
26 pub status: Option<String>,
27 pub version: Option<String>,
28 pub revision: Option<String>,
29 pub top: Option<String>,
30 pub family: Option<String>,
31 pub device: Option<String>,
32 pub started: Option<String>,
33 pub phases: Vec<Phase>,
34}
35
36impl Flow {
37 pub fn succeeded(&self) -> bool {
38 self.status.as_deref().is_some_and(|s| s.starts_with("Successful"))
39 }
40
41 pub fn phase(&self, name: &str) -> Option<&Phase> {
43 self.phases.iter().find(|p| p.name == name)
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct Phase {
50 pub name: String,
51 pub seconds: u64,
52 pub peak_mb: Option<u64>,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Ratio {
57 pub used: u64,
58 pub total: u64,
59}
60
61#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
63pub struct Usage {
64 pub status: Option<String>,
65 pub alms: Option<Ratio>,
66 pub registers: Option<u64>,
67 pub block_memory_bits: Option<Ratio>,
68 pub ram_blocks: Option<Ratio>,
69 pub dsp_blocks: Option<Ratio>,
70}
71
72#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
74pub struct Estimate {
75 pub alms: Option<u64>,
76 pub registers: Option<u64>,
77 pub memory_bits: Option<u64>,
78 pub dsp_blocks: Option<u64>,
79 pub fixed_point_dsp: Option<u64>,
80 pub floating_point_dsp: Option<u64>,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct PartitionEstimate {
86 pub partition: String,
87 pub estimate: Estimate,
88}
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92pub struct Slack {
93 pub analysis: String,
94 pub clock: String,
95 pub slack_ns: f64,
96 pub tns_ns: f64,
97 pub corner: String,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
102pub struct WireDemand {
103 pub wire: String,
104 pub percent: u32,
105 pub direction: String,
106 pub region: String,
107}
108
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct HoldDelay {
112 pub source: String,
113 pub destination: String,
114 pub ns: f64,
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
119pub struct ClockNet {
120 pub name: String,
121 pub fanout: u64,
122 pub global: bool,
123}
124
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
127pub struct Node {
128 pub partition: String,
129 pub path: String,
130 pub entity: String,
131 pub depth: usize,
132 pub aluts: u64,
133 pub registers: u64,
134 pub memory_bits: u64,
135 pub dsp: u64,
136 pub max_depth: Option<f64>,
138}
139
140#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
143pub struct EntityTotal {
144 pub entity: String,
145 pub instances: usize,
146 pub aluts: u64,
147 pub registers: u64,
148 pub memory_bits: u64,
149 pub dsp: u64,
150 pub max_depth: Option<f64>,
151}
152
153#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
155pub struct Build {
156 pub flow: Flow,
157 pub synthesis: Vec<PartitionEstimate>,
158 pub fit: Option<Usage>,
159 pub clocks: Vec<Slack>,
160 pub retime: Vec<(String, String)>,
161 pub wire_demand: Vec<WireDemand>,
162 pub processors: Option<u32>,
163 pub top: Vec<Node>,
165 pub entities: Vec<EntityTotal>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub hold: Option<Vec<HoldDelay>>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub clock_nets: Option<Vec<ClockNet>>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub regions: Vec<RegionUsage>,
178}
179
180pub const ENTITY_ROWS: usize = 32;
182pub const TOP_ROWS: usize = 32;
184
185impl Build {
186 pub fn worst_setup(&self) -> Option<&Slack> {
188 worst_setup(&self.clocks)
189 }
190
191 pub fn synthesis_of(&self, partition: &str) -> Option<&Estimate> {
193 self.synthesis.iter().find(|p| p.partition == partition).map(|p| &p.estimate)
194 }
195}
196
197fn cells(line: &str) -> Option<Vec<&str>> {
199 let t = line.trim_end();
200 if !t.starts_with(';') || !t.ends_with(';') || t.len() < 2 {
201 return None;
202 }
203 Some(t[1..t.len() - 1].split(';').map(str::trim).collect())
204}
205
206pub(crate) fn number(s: &str) -> Option<u64> {
207 let digits: String = s.chars().filter(|&c| c != ',').collect();
208 digits.trim().parse().ok()
209}
210
211fn ratio(s: &str) -> Option<Ratio> {
213 let (used, rest) = s.split_once('/')?;
214 let total = rest.split('(').next()?;
215 Some(Ratio { used: number(used)?, total: number(total)? })
216}
217
218fn hms(s: &str) -> Option<u64> {
220 let parts: Vec<u64> = s.split(':').map(|p| p.trim().parse().ok()).collect::<Option<_>>()?;
221 match parts.as_slice() {
222 [h, m, sec] => Some(h * 3600 + m * 60 + sec),
223 _ => None,
224 }
225}
226
227pub fn parse_flow(text: &str) -> Flow {
229 let mut flow = Flow::default();
230 let mut in_elapsed = false;
231 for line in text.lines() {
232 if line.starts_with('+') {
233 continue;
234 }
235 let Some(c) = cells(line) else {
236 in_elapsed = false;
237 continue;
238 };
239 if c.len() == 1 {
240 in_elapsed = c[0] == "Flow Elapsed Time";
241 continue;
242 }
243 if in_elapsed {
244 if c.len() >= 3 && c[0] != "Module Name" {
245 if let Some(seconds) = hms(c[1]) {
246 let peak_mb = c[2].strip_suffix("MB").and_then(|m| number(m));
247 flow.phases.push(Phase { name: c[0].to_string(), seconds, peak_mb });
248 }
249 }
250 continue;
251 }
252 if c.len() >= 2 {
253 let slot = match c[0] {
254 "Flow Status" => &mut flow.status,
255 "Quartus Prime Version" => &mut flow.version,
256 "Revision Name" => &mut flow.revision,
257 "Top-level Entity Name" => &mut flow.top,
258 "Family" => &mut flow.family,
259 "Device" => &mut flow.device,
260 "Start date & time" => &mut flow.started,
261 _ => continue,
262 };
263 if slot.is_none() && !c[1].is_empty() {
264 *slot = Some(c[1].to_string());
265 }
266 }
267 }
268 flow
269}
270
271pub fn parse_summary(text: &str) -> Usage {
273 let mut u = Usage::default();
274 for line in text.lines() {
275 let Some((key, value)) = line.split_once(" : ") else { continue };
276 let value = value.trim();
277 match key.trim() {
278 "Fitter Status" | "Synthesis Status" | "Flow Status" => u.status = Some(value.to_string()),
279 "Logic utilization (in ALMs)" => u.alms = ratio(value),
280 "Total dedicated logic registers" => u.registers = number(value),
281 "Total block memory bits" => u.block_memory_bits = ratio(value),
282 "Total RAM Blocks" => u.ram_blocks = ratio(value),
283 "Total DSP Blocks" => u.dsp_blocks = ratio(value),
284 _ => {}
285 }
286 }
287 u
288}
289
290fn partition_of(title: &str) -> String {
293 let mut quoted = title.split('"');
294 match (quoted.next(), quoted.next()) {
295 (Some(_), Some(name)) if title.contains("Partition") => name.to_string(),
296 _ => "root_partition".to_string(),
297 }
298}
299
300pub fn parse_synthesis(text: &str) -> Vec<PartitionEstimate> {
302 let mut out: Vec<PartitionEstimate> = Vec::new();
303 let mut current: Option<usize> = None;
304 for line in text.lines() {
305 if line.starts_with('+') {
306 continue;
307 }
308 let Some(c) = cells(line) else {
309 current = None;
310 continue;
311 };
312 if c.len() == 1 {
313 current = c[0].starts_with("Synthesis Resource Usage Summary").then(|| {
314 out.push(PartitionEstimate { partition: partition_of(c[0]), estimate: Estimate::default() });
315 out.len() - 1
316 });
317 continue;
318 }
319 let Some(k) = current else { continue };
320 if c.len() != 2 {
321 continue;
322 }
323 let e = &mut out[k].estimate;
324 let slot = match c[0] {
325 "Estimate of Logic utilization (ALMs needed)" => &mut e.alms,
326 "Dedicated logic registers" => &mut e.registers,
327 "Total block memory bits" => &mut e.memory_bits,
328 "Total DSP Blocks" => &mut e.dsp_blocks,
329 "-- [A] Total Fixed Point DSP Blocks" => &mut e.fixed_point_dsp,
330 "-- [B] Total Floating Point DSP Blocks" => &mut e.floating_point_dsp,
331 _ => continue,
332 };
333 if slot.is_none() {
334 *slot = number(c[1]);
335 }
336 }
337 out
338}
339
340pub fn parse_sta_summary(text: &str) -> Vec<Slack> {
342 let mut out = Vec::new();
343 let lines: Vec<&str> = text.lines().collect();
344 let field = |line: &str, key: &str| -> Option<String> {
345 let (k, v) = line.split_once(':')?;
346 (k.trim() == key).then(|| v.trim().to_string())
347 };
348 for (i, line) in lines.iter().enumerate() {
349 let Some(ty) = field(line, "Type") else { continue };
350 let Some(q) = ty.find('\'') else { continue };
351 let analysis = ty[..q].trim().to_string();
352 let clock = ty[q + 1..].trim_end_matches('\'').to_string();
353 let rest = &lines[i + 1..(i + 4).min(lines.len())];
354 let get = |key: &str| rest.iter().find_map(|l| field(l, key));
355 let (Some(slack), Some(tns), Some(corner)) = (get("Slack"), get("TNS"), get("Corner")) else { continue };
356 let (Ok(slack_ns), Ok(tns_ns)) = (slack.parse::<f64>(), tns.parse::<f64>()) else { continue };
357 out.push(Slack { analysis, clock, slack_ns, tns_ns, corner });
358 }
359 out
360}
361
362pub fn worst_setup(clocks: &[Slack]) -> Option<&Slack> {
364 clocks.iter().filter(|s| s.analysis == "Setup").min_by(|a, b| a.slack_ns.total_cmp(&b.slack_ns))
365}
366
367pub fn parse_retime(text: &str) -> Vec<(String, String)> {
369 let mut out = Vec::new();
370 let mut inside = false;
371 for line in text.lines() {
372 if line.starts_with('+') {
373 continue;
374 }
375 let Some(c) = cells(line) else {
376 inside = false;
377 continue;
378 };
379 if c.len() == 1 {
380 inside = c[0] == "Retiming Limit Summary";
381 continue;
382 }
383 if inside && c.len() >= 2 && c[0] != "Clock Transfer" {
384 out.push((c[0].to_string(), c[1].to_string()));
385 }
386 }
387 out
388}
389
390fn table<'a>(text: &'a str, title: &str) -> Option<Vec<Vec<&'a str>>> {
393 headed_table(text, title).map(|(_, rows)| rows)
394}
395
396pub(crate) fn headed_table<'a>(text: &'a str, title: &str) -> Option<(Vec<&'a str>, Vec<Vec<&'a str>>)> {
398 let mut lines = text.lines();
399 lines.find(|l| cells(l).is_some_and(|c| c.len() == 1 && c[0] == title))?;
400 let mut rows = Vec::new();
401 let mut header: Option<Vec<&str>> = None;
402 for line in lines {
403 if line.starts_with('+') {
404 continue;
405 }
406 let Some(c) = cells(line) else { break };
407 if header.is_none() {
408 header = Some(c);
409 continue;
410 }
411 rows.push(c);
412 }
413 Some((header.unwrap_or_default(), rows))
414}
415
416#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
419pub struct RegionUsage {
420 pub name: String,
421 pub alms_needed: f64,
422 pub alms: u64,
423 pub registers: Option<Ratio>,
424 pub m20k: Option<Ratio>,
425 pub dsp: Option<Ratio>,
426 pub placement: Vec<((u32, u32), (u32, u32))>,
427}
428
429pub fn parse_region_usage(text: &str) -> Vec<RegionUsage> {
433 let Some((header, rows)) = headed_table(text, "Logic Lock Region Usage Summary") else { return Vec::new() };
434 let names = header.get(1..).unwrap_or_default();
435 let row = |stat: &str| rows.iter().find(|r| r.first() == Some(&stat));
436 let rectangles = |cells: &[&str]| -> Vec<((u32, u32), (u32, u32))> {
437 let text = cells.join(";");
438 let numbers: Vec<u32> = text
439 .split(|c: char| !c.is_ascii_digit())
440 .filter(|t| !t.is_empty())
441 .filter_map(|t| t.parse().ok())
442 .collect();
443 numbers.chunks_exact(4).map(|c| ((c[0], c[1]), (c[2], c[3]))).collect()
444 };
445 names
446 .iter()
447 .enumerate()
448 .filter_map(|(k, &name)| {
449 let cell = |stat: &str| row(stat).and_then(|r| r.get(k + 1)).copied();
450 let (needed, alms) = cell("ALMs needed [=A-B+C]")?.split_once('/')?;
451 let alms_needed: f64 = needed.trim().parse().ok()?;
452 let alms = number(alms.split('(').next()?)?;
453 let placement = match row("Region Placement") {
454 Some(r) if names.len() == 1 => rectangles(&r[1..]),
455 Some(r) => r.get(k + 1).map(|c| rectangles(&[c])).unwrap_or_default(),
456 None => Vec::new(),
457 };
458 Some(RegionUsage {
459 name: name.to_string(),
460 alms_needed,
461 alms,
462 registers: cell("Dedicated Logic Registers").and_then(ratio),
463 m20k: cell("M20Ks").and_then(ratio),
464 dsp: cell("DSP Blocks needed [=A-B]").and_then(ratio),
465 placement,
466 })
467 })
468 .collect()
469}
470
471pub fn parse_hold(text: &str) -> Option<Vec<HoldDelay>> {
473 let rows = table(text, "Estimated Delay Added for Hold Timing Summary")?;
474 Some(
475 rows.iter()
476 .filter_map(|c| match c.as_slice() {
477 [source, destination, ns] => Some(HoldDelay { source: source.to_string(), destination: destination.to_string(), ns: ns.parse().ok()? }),
478 _ => None,
479 })
480 .collect(),
481 )
482}
483
484pub fn clock_nets(control: Option<&str>, globals: Option<&str>) -> Option<Vec<ClockNet>> {
488 let control = table(control?, "Control Signals")?;
489 let unbuffered = |name: &'_ str| -> String {
490 match name.rsplit_once("~FITTER_INSERTED_") {
491 Some((net, n)) if !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => net.to_string(),
492 _ => name.to_string(),
493 }
494 };
495 let global: std::collections::HashSet<String> =
496 table(globals?, "Global & Other Fast Signals Summary")?.iter().filter_map(|c| c.first().map(|n| unbuffered(n))).collect();
497 let mut nets: Vec<ClockNet> = control
498 .iter()
499 .filter_map(|c| match c.as_slice() {
500 [name, _, fanout, "Clock", ..] => Some(ClockNet { name: name.to_string(), fanout: number(fanout)?, global: global.contains(*name) }),
501 _ => None,
502 })
503 .collect();
504 nets.sort_by(|a, b| a.global.cmp(&b.global).then(b.fanout.cmp(&a.fanout)).then_with(|| a.name.cmp(&b.name)));
505 nets.dedup();
506 Some(nets)
507}
508
509pub fn parse_wire_demand(text: &str) -> Vec<WireDemand> {
511 let mut out = Vec::new();
512 for line in text.lines() {
513 let Some(rest) = line.trim().strip_prefix("Info (20215): Router estimated peak ") else { continue };
514 let Some((wire, tail)) = rest.split_once(" demand : ") else { continue };
515 let Some((pct, tail)) = tail.split_once("% of ") else { continue };
516 let Some((direction, region)) = tail.split_once(" directional wire in region ") else { continue };
517 let Ok(percent) = pct.trim().parse() else { continue };
518 let w = WireDemand { wire: wire.to_string(), percent, direction: direction.to_string(), region: region.trim().to_string() };
519 if !out.contains(&w) {
520 out.push(w);
521 }
522 }
523 out
524}
525
526pub fn parse_processors(text: &str) -> Option<u32> {
529 text.lines().find_map(|l| {
530 if let Some(rest) = l.split("Parallel compilation is enabled and will use ").nth(1) {
531 return rest.trim_start_matches("up to ").split_whitespace().next()?.parse().ok();
532 }
533 match cells(l)?.as_slice() {
534 ["Maximum allowed", n] => n.parse().ok(),
535 _ => None,
536 }
537 })
538}
539
540fn total(s: &str) -> Option<u64> {
542 number(s.split('(').next()?)
543}
544
545fn total_depth(s: &str) -> Option<f64> {
546 s.split('(').next()?.trim().parse().ok()
547}
548
549pub fn parse_hierarchy(text: &str) -> Vec<Node> {
551 let mut out = Vec::new();
552 let mut columns: Option<Vec<String>> = None;
553 let mut partition = "root_partition".to_string();
554 for line in text.lines() {
555 let t = line.trim_end();
556 if !t.starts_with(';') {
557 if !t.starts_with('+') {
558 columns = None;
559 }
560 continue;
561 }
562 let raw: Vec<&str> = t[1..t.len().saturating_sub(1)].split(';').collect();
563 let c: Vec<&str> = raw.iter().map(|s| s.trim()).collect();
564 if c.len() == 1 && c[0].ends_with("Resource Utilization by Entity") {
565 partition = partition_of(c[0]);
566 continue;
567 }
568 if c.first() == Some(&"Compilation Hierarchy Node") {
569 columns = Some(c.iter().map(|s| s.to_string()).collect());
570 continue;
571 }
572 let Some(cols) = &columns else { continue };
573 let at = |name: &str| cols.iter().position(|c| c == name).and_then(|i| c.get(i).copied());
574 let (Some(aluts), Some(registers)) = (at("Combinational ALUTs").and_then(total), at("Dedicated Logic Registers").and_then(total))
575 else {
576 continue;
577 };
578 let indent = raw[0].len() - raw[0].trim_start().len();
579 out.push(Node {
580 partition: partition.clone(),
581 path: at("Full Hierarchy Name").unwrap_or("").to_string(),
582 entity: at("Entity Name").unwrap_or("").to_string(),
583 depth: indent.saturating_sub(1) / 3,
584 aluts,
585 registers,
586 memory_bits: at("Block Memory Bits").and_then(total).unwrap_or(0),
587 dsp: at("DSP Blocks").and_then(total).unwrap_or(0),
588 max_depth: at("Max Depth").and_then(total_depth),
589 });
590 }
591 out
592}
593
594pub fn by_entity(nodes: &[Node]) -> Vec<EntityTotal> {
596 let mut totals: Vec<EntityTotal> = Vec::new();
597 let mut ancestors: Vec<&str> = Vec::new();
598 for n in nodes {
599 ancestors.truncate(n.depth);
600 let nested = ancestors.contains(&n.entity.as_str());
601 let k = match totals.iter().position(|t| t.entity == n.entity) {
602 Some(k) => k,
603 None => {
604 totals.push(EntityTotal {
605 entity: n.entity.clone(),
606 instances: 0,
607 aluts: 0,
608 registers: 0,
609 memory_bits: 0,
610 dsp: 0,
611 max_depth: None,
612 });
613 totals.len() - 1
614 }
615 };
616 let t = &mut totals[k];
617 t.instances += 1;
618 if !nested {
619 t.aluts += n.aluts;
620 t.registers += n.registers;
621 t.memory_bits += n.memory_bits;
622 t.dsp += n.dsp;
623 }
624 t.max_depth = match (t.max_depth, n.max_depth) {
625 (Some(a), Some(b)) => Some(a.max(b)),
626 (a, b) => a.or(b),
627 };
628 ancestors.push(&n.entity);
629 }
630 totals.sort_by(|a, b| b.aluts.cmp(&a.aluts).then_with(|| a.entity.cmp(&b.entity)));
631 totals
632}
633
634fn top_of(nodes: Vec<Node>) -> Vec<Node> {
636 let mut partitions: Vec<&str> = Vec::new();
637 for n in &nodes {
638 if !partitions.contains(&n.partition.as_str()) {
639 partitions.push(&n.partition);
640 }
641 }
642 let mut out = Vec::new();
643 for p in partitions {
644 let level: Vec<&Node> = nodes.iter().filter(|n| n.partition == p && n.depth <= 1).collect();
645 let mut children: Vec<usize> = (0..level.len()).filter(|&i| level[i].depth == 1).collect();
646 children.sort_by(|&a, &b| level[b].aluts.cmp(&level[a].aluts).then(a.cmp(&b)));
647 children.truncate(TOP_ROWS);
648 out.extend(
649 level
650 .iter()
651 .enumerate()
652 .filter(|&(i, n)| n.depth == 0 || children.contains(&i))
653 .map(|(_, n)| (*n).clone()),
654 );
655 }
656 out
657}
658
659#[derive(Clone, Debug, PartialEq, Eq)]
661pub enum QuartusError {
662 NoFlowReport(String),
664 Io(String),
665 Recorded(String),
667 Unrecorded(String),
669}
670
671impl std::fmt::Display for QuartusError {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 match self {
674 QuartusError::NoFlowReport(path) => write!(f, "no flow report at {path}"),
675 QuartusError::Io(why) => write!(f, "{why}"),
676 QuartusError::Recorded(name) => write!(f, "a compile named {name} is recorded already"),
677 QuartusError::Unrecorded(name) => write!(f, "no compile named {name} is recorded"),
678 }
679 }
680}
681
682impl std::error::Error for QuartusError {}
683
684pub fn read(output_files: &Path, revision: &str) -> Result<Build, QuartusError> {
686 let path = |suffix: &str| output_files.join(format!("{revision}.{suffix}"));
687 let text = |suffix: &str| -> Result<Option<String>, QuartusError> {
688 let p = path(suffix);
689 match std::fs::read(&p) {
690 Ok(bytes) => Ok(Some(String::from_utf8_lossy(&bytes).into_owned())),
691 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
692 Err(e) => Err(QuartusError::Io(format!("{}: {e}", p.display()))),
693 }
694 };
695 let flow_text = text("flow.rpt")?.ok_or_else(|| QuartusError::NoFlowReport(path("flow.rpt").display().to_string()))?;
696 let syn = text("syn.rpt")?;
697 let (fit_report, route) = (text("fit.rpt")?, text("fit.route.rpt")?);
698 let router_messages = route.as_deref().or(fit_report.as_deref()).unwrap_or("");
700 let processors = [fit_report.as_deref(), route.as_deref(), Some(flow_text.as_str())].into_iter().flatten().find_map(parse_processors);
701 let nodes = syn.as_deref().map(parse_hierarchy).unwrap_or_default();
702 let mut entities = by_entity(&nodes);
703 entities.truncate(ENTITY_ROWS);
704 let (plan, place) = (text("fit.plan.rpt")?, text("fit.place.rpt")?);
706 fn holding<'a>(title: &str, reports: [Option<&'a str>; 2]) -> Option<&'a str> {
707 reports.into_iter().flatten().find(|t| table(t, title).is_some())
708 }
709 let control = holding("Control Signals", [fit_report.as_deref(), plan.as_deref()]);
710 let globals = holding("Global & Other Fast Signals Summary", [fit_report.as_deref(), place.as_deref()]);
711 Ok(Build {
712 flow: parse_flow(&flow_text),
713 synthesis: syn.as_deref().map(parse_synthesis).unwrap_or_default(),
714 fit: text("fit.summary")?.map(|t| parse_summary(&t)),
715 clocks: text("sta.summary")?.map(|t| parse_sta_summary(&t)).unwrap_or_default(),
716 retime: text("fit.retime.rpt")?.map(|t| parse_retime(&t)).unwrap_or_default(),
717 wire_demand: parse_wire_demand(router_messages),
718 processors,
719 top: top_of(nodes),
720 entities,
721 hold: fit_report.as_deref().and_then(|t| parse_hold(t).or_else(|| table(t, "Routing Usage Summary").map(|_| Vec::new()))),
722 regions: fit_report.as_deref().map(parse_region_usage).unwrap_or_default(),
723 clock_nets: clock_nets(control, globals),
724 })
725}
726
727#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
729pub struct Input {
730 pub path: String,
731 pub sha256: String,
732}
733
734#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
738pub struct Row {
739 pub name: String,
740 pub recorded: String,
741 pub inputs: Vec<Input>,
742 pub build: Build,
743 pub verdict: Verdict,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub resumed: Option<String>,
746}
747
748impl Row {
749 pub fn synthesis_sources(&self) -> Option<(&Input, &Input)> {
752 if !self.verdict.passed || self.verdict.expected != ["Synthesis"] {
753 return None;
754 }
755 let pick = |ext: &str| self.inputs.iter().filter(|i| i.path.ends_with(ext)).collect::<Vec<_>>();
756 match (pick(".v").as_slice(), pick(".qsf").as_slice()) {
757 ([v], [q]) => Some((*v, *q)),
758 _ => None,
759 }
760 }
761}
762
763#[derive(Clone, Debug, Default, PartialEq, Eq)]
766pub struct Evidence {
767 pub exit: Option<i32>,
769 pub expected: Vec<String>,
771 pub log: Option<String>,
773 pub started: Option<u64>,
775 pub flow_written: Option<u64>,
776}
777
778#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
782pub struct Verdict {
783 pub passed: bool,
784 pub failures: Vec<String>,
785 pub exit: Option<i32>,
786 pub expected: Vec<String>,
787}
788
789pub fn log_failure(log: &str) -> Option<String> {
792 log.lines()
793 .map(str::trim)
794 .find(|l| l.starts_with("Fatal Error:") || l.starts_with("*** Fatal Error") || l.contains(" was unsuccessful") || *l == "Quartus build failed")
795 .map(str::to_string)
796}
797
798pub fn judge(build: &Build, evidence: &Evidence) -> Verdict {
802 let mut failures = Vec::new();
803 match build.flow.status.as_deref() {
804 Some(s) if s.starts_with("Successful") => {}
805 Some(s) => failures.push(format!("the flow report says {s}")),
806 None => failures.push("the flow report has no status".to_string()),
807 }
808 for module in &evidence.expected {
809 if build.flow.phase(module).is_none() {
810 failures.push(format!("the job runs {module}, which the flow report does not hold"));
811 }
812 }
813 if let Some(code) = evidence.exit.filter(|&c| c != 0) {
814 failures.push(format!("the flow exited {code}"));
815 }
816 if let Some(line) = evidence.log.as_deref().and_then(log_failure) {
817 failures.push(format!("the log: {line}"));
818 }
819 if let (Some(started), Some(written)) = (evidence.started, evidence.flow_written) {
820 if written < started {
821 failures.push("the flow report was written before the job started".to_string());
822 }
823 }
824 Verdict { passed: failures.is_empty(), failures, exit: evidence.exit, expected: evidence.expected.clone() }
825}
826
827#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
830pub struct Ledger {
831 pub rows: Vec<Row>,
832}
833
834impl Ledger {
835 pub fn load(path: &Path) -> Result<Ledger, QuartusError> {
837 match std::fs::read_to_string(path) {
838 Ok(text) => serde_json::from_str(&text).map_err(|e| QuartusError::Io(format!("{}: {e}", path.display()))),
839 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Ledger::default()),
840 Err(e) => Err(QuartusError::Io(format!("{}: {e}", path.display()))),
841 }
842 }
843
844 pub fn save(&self, path: &Path) -> Result<(), QuartusError> {
845 let mut text = serde_json::to_string_pretty(self).map_err(|e| QuartusError::Io(e.to_string()))?;
846 text.push('\n');
847 std::fs::write(path, text).map_err(|e| QuartusError::Io(format!("{}: {e}", path.display())))
848 }
849
850 pub fn record(&mut self, row: Row) -> Result<(), QuartusError> {
852 if self.rows.iter().any(|r| r.name == row.name) {
853 return Err(QuartusError::Recorded(row.name));
854 }
855 self.rows.push(row);
856 Ok(())
857 }
858
859 pub fn replace(&mut self, row: Row) -> Result<(), QuartusError> {
861 let at = self.rows.iter().position(|r| r.name == row.name).ok_or_else(|| QuartusError::Unrecorded(row.name.clone()))?;
862 self.rows[at] = row;
863 Ok(())
864 }
865
866 pub fn row(&self, name: &str) -> Option<&Row> {
867 self.rows.iter().find(|r| r.name == name)
868 }
869}
870
871pub fn utc(seconds: u64) -> String {
873 let (days, rem) = (seconds / 86_400, seconds % 86_400);
874 let z = days as i64 + 719_468;
876 let era = z.div_euclid(146_097);
877 let doe = z - era * 146_097;
878 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
879 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
880 let mp = (5 * doy + 2) / 153;
881 let day = doy - (153 * mp + 2) / 5 + 1;
882 let month = if mp < 10 { mp + 3 } else { mp - 9 };
883 let year = yoe + era * 400 + (month <= 2) as i64;
884 format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", rem / 3600, rem / 60 % 60, rem % 60)
885}
886
887#[cfg(test)]
888#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/quartus/tests.rs"]
889mod tests;