Skip to main content

trident/cost/
report.rs

1use super::analyzer::{FunctionCost, ProgramCost};
2use super::visit::next_power_of_two;
3use crate::diagnostic::Diagnostic;
4use crate::span::Span;
5
6// --- Report formatting ---
7
8impl ProgramCost {
9    /// Format a table-style cost report.
10    pub fn format_report(&self) -> String {
11        let short = self.short_names();
12        let n = short.len();
13        let mut out = String::new();
14        out.push_str(&format!("Cost report: {}\n", self.program_name));
15
16        // Header
17        out.push_str(&format!("{:<24}", "Function"));
18        for name in &short {
19            out.push_str(&format!(" {:>6}", name));
20        }
21        out.push_str("  dominant\n");
22        let line_width = 24 + n * 7 + 10;
23        out.push_str(&"-".repeat(line_width));
24        out.push('\n');
25
26        for func in &self.functions {
27            out.push_str(&format!("{:<24}", func.name));
28            for i in 0..n {
29                out.push_str(&format!(" {:>6}", func.cost.get(i)));
30            }
31            out.push_str(&format!("  {}\n", func.cost.dominant_table(&short)));
32            if let Some((per_iter, bound)) = &func.per_iteration {
33                out.push_str(&format!("  per iteration (x{})", bound));
34                let label_len = format!("  per iteration (x{})", bound).len();
35                // Pad to align with columns
36                for _ in label_len..24 {
37                    out.push(' ');
38                }
39                for i in 0..n {
40                    out.push_str(&format!(" {:>6}", per_iter.get(i)));
41                }
42                out.push('\n');
43            }
44        }
45
46        out.push_str(&"-".repeat(line_width));
47        out.push('\n');
48        out.push_str(&format!("{:<24}", "TOTAL"));
49        for i in 0..n {
50            out.push_str(&format!(" {:>6}", self.total.get(i)));
51        }
52        out.push_str(&format!("  {}\n", self.total.dominant_table(&short)));
53        out.push('\n');
54        out.push_str(&format!(
55            "Padded height:           {}\n",
56            self.padded_height
57        ));
58        out.push_str(&format!(
59            "Program attestation:     {} hash rows\n",
60            self.attestation_hash_rows
61        ));
62        let secs = self.estimated_proving_ns / 1_000_000_000;
63        let tenths = (self.estimated_proving_ns / 100_000_000) % 10;
64        out.push_str(&format!("Estimated proving time:  ~{}.{}s\n", secs, tenths));
65
66        // Power-of-2 boundary warning.
67        let headroom = self.padded_height - self.total.max_height();
68        if headroom < self.padded_height / 8 {
69            out.push_str(&format!(
70                "\nwarning: {} rows below padded height boundary ({})\n",
71                headroom, self.padded_height
72            ));
73            out.push_str(&format!(
74                "  adding {}+ rows to any table will double proving cost to {}\n",
75                headroom + 1,
76                self.padded_height * 2
77            ));
78        }
79
80        out
81    }
82
83    /// Format a hotspots report (top N cost contributors).
84    pub fn format_hotspots(&self, top_n: usize) -> String {
85        let short = self.short_names();
86        let mut out = String::new();
87        out.push_str(&format!("Top {} cost contributors:\n", top_n));
88
89        let dominant = self.total.dominant_table(&short);
90        let dominant_idx = self.dominant_index();
91        let dominant_total = self.total.get(dominant_idx);
92
93        let mut ranked: Vec<&FunctionCost> = self.functions.iter().collect();
94        ranked.sort_by(|a, b| {
95            let av = a.cost.get(dominant_idx);
96            let bv = b.cost.get(dominant_idx);
97            bv.cmp(&av)
98        });
99
100        for (i, func) in ranked.iter().take(top_n).enumerate() {
101            let val = func.cost.get(dominant_idx);
102            let pct = if dominant_total > 0 {
103                val * 100 / dominant_total
104            } else {
105                0
106            };
107            out.push_str(&format!(
108                "  {}. {:<24} {:>6} {} rows ({}% of {} table)\n",
109                i + 1,
110                func.name,
111                val,
112                dominant,
113                pct,
114                dominant
115            ));
116        }
117
118        out.push_str(&format!(
119            "\nDominant table: {} ({} rows). Reduce {} operations to lower padded height.\n",
120            dominant, dominant_total, dominant
121        ));
122
123        out
124    }
125
126    /// Generate optimization hints (H0001, H0002, H0004).
127    pub fn optimization_hints(&self) -> Vec<Diagnostic> {
128        let short = self.short_names();
129        let mut hints = Vec::new();
130
131        // H0001: Secondary table dominance — a non-primary table is much taller than primary.
132        // (For Triton: hash[1] vs processor[0]; generalized to dominant vs first.)
133        if self.total.count >= 2 && self.total.get(0) > 0 {
134            let dominant_idx = self.dominant_index();
135            if dominant_idx > 0 {
136                let dominant_val = self.total.get(dominant_idx);
137                let primary_val = self.total.get(0);
138                // ratio > 2.0 equivalent: dominant_val > 2 * primary_val
139                if dominant_val > 2 * primary_val {
140                    let dominant_name = short.get(dominant_idx).unwrap_or(&"?");
141                    let primary_name = short.first().unwrap_or(&"?");
142                    // Integer ratio with one decimal: ratio_10 = dominant * 10 / primary
143                    let ratio_10 = if primary_val > 0 {
144                        dominant_val * 10 / primary_val
145                    } else {
146                        0
147                    };
148                    let mut diag = Diagnostic::warning(
149                        format!(
150                            "hint[H0001]: {} table is {}.{}x taller than {} table",
151                            dominant_name,
152                            ratio_10 / 10,
153                            ratio_10 % 10,
154                            primary_name
155                        ),
156                        Span::dummy(),
157                    );
158                    diag.notes.push(format!(
159                        "{} optimizations will not reduce proving cost",
160                        primary_name
161                    ));
162                    diag.help = Some(format!(
163                        "focus on reducing {} table usage to lower padded height",
164                        dominant_name
165                    ));
166                    hints.push(diag);
167                }
168            }
169        }
170
171        // H0002: Headroom hint (far below boundary = room to grow)
172        let max_height = self.total.max_height().max(self.attestation_hash_rows);
173        let headroom = self.padded_height - max_height;
174        if headroom > self.padded_height / 4 && self.padded_height >= 16 {
175            let headroom_pct = if self.padded_height > 0 {
176                headroom * 100 / self.padded_height
177            } else {
178                0
179            };
180            let mut diag = Diagnostic::warning(
181                format!(
182                    "hint[H0002]: padded height is {}, but max table height is only {}",
183                    self.padded_height, max_height
184                ),
185                Span::dummy(),
186            );
187            diag.notes.push(format!(
188                "you have {} rows of headroom ({}%) before the next doubling",
189                headroom, headroom_pct
190            ));
191            diag.help = Some(format!(
192                "this program could be {}% more complex at zero additional proving cost",
193                headroom_pct
194            ));
195            hints.push(diag);
196        }
197
198        // H0004: Loop bound waste (entries already filtered at 4x+ in analyzer)
199        // Also handles unknown-bound entries (bound == 0) from non-constant loops.
200        for (fn_name, end_val, bound) in &self.loop_bound_waste {
201            if *bound == 0 {
202                // Non-constant loop end with no `bounded` annotation
203                let mut diag = Diagnostic::warning(
204                    format!(
205                        "hint[H0004]: loop in '{}' has non-constant bound, cost assumes {} iteration(s)",
206                        fn_name, end_val
207                    ),
208                    Span::dummy(),
209                );
210                diag.help = Some(
211                    "add a `bounded N` annotation to set a realistic worst-case iteration count"
212                        .to_string(),
213                );
214                hints.push(diag);
215            } else {
216                let actual = *end_val.max(&1);
217                let ratio = *bound / actual;
218                let mut diag = Diagnostic::warning(
219                    format!(
220                        "hint[H0004]: loop in '{}' bounded {} but iterates only {} times",
221                        fn_name, bound, end_val
222                    ),
223                    Span::dummy(),
224                );
225                diag.notes.push(format!(
226                    "declared bound is {}x the actual iteration count",
227                    ratio
228                ));
229                diag.help = Some(format!(
230                    "tightening the bound to {} would reduce worst-case cost",
231                    next_power_of_two(*end_val)
232                ));
233                hints.push(diag);
234            }
235        }
236
237        hints
238    }
239}