1use super::functions::*;
6use crate::lcnf::{LcnfArg, LcnfExpr, LcnfFunDecl, LcnfLetValue, LcnfLit, LcnfType, LcnfVarId};
7use std::collections::{HashMap, HashSet};
8
9#[allow(dead_code)]
10#[derive(Default, Debug)]
11pub struct InlineTrace {
12 pub(super) entries: Vec<InlineTraceEntry>,
13 pub enabled: bool,
14}
15#[allow(dead_code)]
16impl InlineTrace {
17 pub fn new() -> Self {
18 InlineTrace {
19 entries: Vec::new(),
20 enabled: true,
21 }
22 }
23 pub fn disabled() -> Self {
24 InlineTrace {
25 entries: Vec::new(),
26 enabled: false,
27 }
28 }
29 pub fn record(&mut self, entry: InlineTraceEntry) {
30 if self.enabled {
31 self.entries.push(entry);
32 }
33 }
34 pub fn len(&self) -> usize {
35 self.entries.len()
36 }
37 pub fn is_empty(&self) -> bool {
38 self.entries.is_empty()
39 }
40 pub fn inlined_entries(&self) -> Vec<&InlineTraceEntry> {
41 self.entries.iter().filter(|e| e.did_inline).collect()
42 }
43 pub fn skipped_entries(&self) -> Vec<&InlineTraceEntry> {
44 self.entries.iter().filter(|e| !e.did_inline).collect()
45 }
46 pub fn entries_for_callee(&self, callee: &str) -> Vec<&InlineTraceEntry> {
47 self.entries.iter().filter(|e| e.callee == callee).collect()
48 }
49 pub fn to_csv(&self) -> String {
50 let header = "sweep,caller,callee,decision,callee_size,did_inline";
51 let rows: Vec<String> = self.entries.iter().map(|e| e.to_csv()).collect();
52 std::iter::once(header.to_owned())
53 .chain(rows)
54 .collect::<Vec<_>>()
55 .join("\n")
56 }
57 pub fn clear(&mut self) {
58 self.entries.clear();
59 }
60}
61#[allow(dead_code)]
62#[derive(Clone, Debug, Default)]
63pub struct RecursiveInlineLimiter {
64 pub unroll_depth: HashMap<String, u32>,
65 pub max_unroll: u32,
66 pub enforcements: u64,
67}
68#[allow(dead_code)]
69impl RecursiveInlineLimiter {
70 pub fn new(max_unroll: u32) -> Self {
71 RecursiveInlineLimiter {
72 max_unroll,
73 ..Default::default()
74 }
75 }
76 pub fn try_unroll(&mut self, fn_name: &str) -> bool {
77 let depth = self.unroll_depth.entry(fn_name.to_owned()).or_insert(0);
78 if *depth >= self.max_unroll {
79 self.enforcements += 1;
80 return false;
81 }
82 *depth += 1;
83 true
84 }
85 pub fn pop_unroll(&mut self, fn_name: &str) {
86 if let Some(d) = self.unroll_depth.get_mut(fn_name) {
87 if *d > 0 {
88 *d -= 1;
89 }
90 }
91 }
92 pub fn depth_of(&self, fn_name: &str) -> u32 {
93 self.unroll_depth.get(fn_name).copied().unwrap_or(0)
94 }
95 pub fn reset(&mut self) {
96 self.unroll_depth.clear();
97 }
98}
99#[allow(dead_code)]
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum InlineAnnotation {
102 AlwaysInline,
103 NeverInline,
104 Default,
105 HotOnly { threshold: u64 },
106}
107#[allow(dead_code)]
109#[derive(Debug, Clone, Default)]
110pub struct InlineProfitabilityEstimator {
111 pub(super) size_weight: f64,
113 pub(super) call_overhead_weight: f64,
115 pub(super) hot_path_weight: f64,
117}
118#[allow(dead_code)]
119impl InlineProfitabilityEstimator {
120 pub fn new() -> Self {
122 Self {
123 size_weight: 1.0,
124 call_overhead_weight: 2.0,
125 hot_path_weight: 3.0,
126 }
127 }
128 pub fn estimate(&self, callee_size: usize, call_overhead: f64, is_hot: bool) -> f64 {
133 let size_penalty = callee_size as f64 * self.size_weight;
134 let overhead_savings = call_overhead * self.call_overhead_weight;
135 let hot_bonus = if is_hot {
136 self.hot_path_weight * 10.0
137 } else {
138 0.0
139 };
140 overhead_savings + hot_bonus - size_penalty
141 }
142 pub fn is_profitable(&self, callee_size: usize, call_overhead: f64, is_hot: bool) -> bool {
144 self.estimate(callee_size, call_overhead, is_hot) > 0.0
145 }
146}
147#[allow(dead_code)]
148#[derive(Default, Debug)]
149pub struct InlineAnnotationRegistry {
150 pub(super) annotations: HashMap<String, InlineAnnotation>,
151}
152#[allow(dead_code)]
153impl InlineAnnotationRegistry {
154 pub fn new() -> Self {
155 Self::default()
156 }
157 pub fn register(&mut self, fn_name: impl Into<String>, ann: InlineAnnotation) {
158 self.annotations.insert(fn_name.into(), ann);
159 }
160 pub fn get(&self, fn_name: &str) -> &InlineAnnotation {
161 self.annotations
162 .get(fn_name)
163 .unwrap_or(&InlineAnnotation::Default)
164 }
165 pub fn has_annotation(&self, fn_name: &str) -> bool {
166 self.annotations.contains_key(fn_name)
167 }
168 pub fn apply(&self, fn_name: &str, decision: InlineDecision) -> InlineDecision {
169 match self.get(fn_name) {
170 InlineAnnotation::AlwaysInline => InlineDecision::Always,
171 InlineAnnotation::NeverInline => InlineDecision::Never,
172 InlineAnnotation::Default => decision,
173 InlineAnnotation::HotOnly { threshold } => {
174 InlineDecision::Heuristic(*threshold as f64 / 100.0)
175 }
176 }
177 }
178 pub fn len(&self) -> usize {
179 self.annotations.len()
180 }
181 pub fn is_empty(&self) -> bool {
182 self.annotations.is_empty()
183 }
184}
185#[allow(dead_code)]
186#[derive(Debug, Clone)]
187pub struct InlineBudget {
188 pub total_budget: u64,
189 pub consumed: u64,
190 pub reserved_always: u64,
191 pub per_fn_limit: HashMap<String, u64>,
192 pub per_fn_spent: HashMap<String, u64>,
193}
194#[allow(dead_code)]
195impl InlineBudget {
196 pub fn new(total_budget: u64) -> Self {
197 InlineBudget {
198 total_budget,
199 consumed: 0,
200 reserved_always: total_budget / 4,
201 per_fn_limit: HashMap::new(),
202 per_fn_spent: HashMap::new(),
203 }
204 }
205 pub fn set_fn_limit(&mut self, fn_name: impl Into<String>, limit: u64) {
206 self.per_fn_limit.insert(fn_name.into(), limit);
207 }
208 pub fn try_spend(&mut self, caller: &str, cost: u64) -> bool {
209 if self.consumed + cost > self.total_budget {
210 return false;
211 }
212 if let Some(&limit) = self.per_fn_limit.get(caller) {
213 let spent = self.per_fn_spent.get(caller).copied().unwrap_or(0);
214 if spent + cost > limit {
215 return false;
216 }
217 *self.per_fn_spent.entry(caller.to_owned()).or_insert(0) += cost;
218 }
219 self.consumed += cost;
220 true
221 }
222 pub fn remaining(&self) -> u64 {
223 self.total_budget.saturating_sub(self.consumed)
224 }
225 pub fn is_exhausted(&self) -> bool {
226 self.consumed >= self.total_budget
227 }
228 pub fn fraction_consumed(&self) -> f64 {
229 if self.total_budget == 0 {
230 1.0
231 } else {
232 self.consumed as f64 / self.total_budget as f64
233 }
234 }
235 pub fn reset_per_fn(&mut self) {
236 self.per_fn_spent.clear();
237 }
238 pub fn heavy_spenders(&self, threshold: u64) -> Vec<(&str, u64)> {
239 self.per_fn_spent
240 .iter()
241 .filter(|(_, &s)| s >= threshold)
242 .map(|(n, &s)| (n.as_str(), s))
243 .collect()
244 }
245}
246#[allow(dead_code)]
248#[derive(Debug, Clone)]
249pub struct InlineFusionRecord {
250 pub caller: String,
251 pub first_callee: String,
252 pub second_callee: String,
253 pub fused_name: String,
254 pub savings_estimate: i32,
255}
256#[allow(dead_code)]
257#[derive(Clone, Debug)]
258pub struct InlineTraceEntry {
259 pub sweep: u32,
260 pub caller: String,
261 pub callee: String,
262 pub decision: String,
263 pub callee_size: u64,
264 pub did_inline: bool,
265}
266#[allow(dead_code)]
267impl InlineTraceEntry {
268 pub fn new(
269 sweep: u32,
270 caller: impl Into<String>,
271 callee: impl Into<String>,
272 decision: impl Into<String>,
273 callee_size: u64,
274 did_inline: bool,
275 ) -> Self {
276 InlineTraceEntry {
277 sweep,
278 caller: caller.into(),
279 callee: callee.into(),
280 decision: decision.into(),
281 callee_size,
282 did_inline,
283 }
284 }
285 pub fn to_csv(&self) -> String {
286 format!(
287 "{},{},{},{},{},{}",
288 self.sweep, self.caller, self.callee, self.decision, self.callee_size, self.did_inline
289 )
290 }
291}
292#[allow(dead_code)]
294#[derive(Debug, Clone)]
295pub struct InlineContextFrame {
296 pub callee: String,
298 pub depth: usize,
300 pub event_id: u64,
302}
303#[allow(dead_code)]
306#[derive(Debug, Clone, Default)]
307pub struct InlineContextStack {
308 pub(super) frames: Vec<InlineContextFrame>,
309 pub(super) next_event_id: u64,
310}
311#[allow(dead_code)]
312impl InlineContextStack {
313 pub fn new() -> Self {
315 Self::default()
316 }
317 pub fn push(&mut self, callee: &str, depth: usize) -> u64 {
319 let id = self.next_event_id;
320 self.next_event_id += 1;
321 self.frames.push(InlineContextFrame {
322 callee: callee.to_owned(),
323 depth,
324 event_id: id,
325 });
326 id
327 }
328 pub fn pop(&mut self) -> Option<InlineContextFrame> {
330 self.frames.pop()
331 }
332 pub fn depth(&self) -> usize {
334 self.frames.len()
335 }
336 pub fn contains(&self, callee: &str) -> bool {
338 self.frames.iter().any(|f| f.callee == callee)
339 }
340 pub fn fingerprint(&self) -> String {
342 self.frames
343 .iter()
344 .map(|f| f.callee.as_str())
345 .collect::<Vec<_>>()
346 .join("->")
347 }
348}
349#[derive(Debug, Clone)]
351pub struct InliningContext {
352 pub depth: u32,
354 pub call_stack: Vec<String>,
356 pub substitutions: HashMap<String, LcnfExpr>,
358}
359impl InliningContext {
360 pub fn new() -> Self {
362 InliningContext {
363 depth: 0,
364 call_stack: Vec::new(),
365 substitutions: HashMap::new(),
366 }
367 }
368 pub fn push_call(&mut self, name: &str) -> bool {
373 if self.has_cycle(name) {
374 return false;
375 }
376 self.call_stack.push(name.to_owned());
377 self.depth += 1;
378 true
379 }
380 pub fn pop_call(&mut self) {
382 self.call_stack.pop();
383 if self.depth > 0 {
384 self.depth -= 1;
385 }
386 }
387 pub fn has_cycle(&self, name: &str) -> bool {
389 self.call_stack.iter().any(|n| n == name)
390 }
391}
392#[derive(Debug, Clone)]
394pub struct InlineConfig {
395 pub heuristics: InlineHeuristics,
397 pub enable_recursive_inlining: bool,
399 pub enable_hot_inlining: bool,
401 pub max_passes: u32,
403}
404#[allow(dead_code)]
406#[derive(Debug, Clone)]
407pub enum PartialInlineRegion {
408 Prefix(usize),
410 TailOnly,
412 Full,
414 None,
416}
417#[derive(Debug, Clone, Default)]
419pub struct InlineReport {
420 pub total_calls_considered: usize,
422 pub inlined_count: usize,
424 pub skipped_recursive: usize,
426 pub skipped_too_large: usize,
428}
429impl InlineReport {
430 pub fn summary(&self) -> String {
432 format!(
433 "inline: {}/{} inlined (recursive_skip={}, size_skip={})",
434 self.inlined_count,
435 self.total_calls_considered,
436 self.skipped_recursive,
437 self.skipped_too_large,
438 )
439 }
440 pub fn inline_rate(&self) -> f64 {
442 if self.total_calls_considered == 0 {
443 0.0
444 } else {
445 self.inlined_count as f64 / self.total_calls_considered as f64
446 }
447 }
448}
449#[derive(Debug, Clone)]
451pub struct CallSite {
452 pub caller: String,
454 pub callee: String,
456 pub call_id: u64,
458 pub is_tail_call: bool,
460 pub is_self_recursive: bool,
462}
463impl CallSite {
464 pub fn new(
466 caller: impl Into<String>,
467 callee: impl Into<String>,
468 call_id: u64,
469 is_tail_call: bool,
470 is_self_recursive: bool,
471 ) -> Self {
472 CallSite {
473 caller: caller.into(),
474 callee: callee.into(),
475 call_id,
476 is_tail_call,
477 is_self_recursive,
478 }
479 }
480 pub fn inline_benefit(&self) -> u64 {
485 let base: u64 = 10;
486 let tail_bonus: u64 = if self.is_tail_call { 3 } else { 0 };
487 let recursive_penalty: u64 = if self.is_self_recursive { 8 } else { 0 };
488 base.saturating_add(tail_bonus)
489 .saturating_sub(recursive_penalty)
490 }
491}
492#[derive(Debug, Clone)]
494pub struct InlineCost {
495 pub body_size: u64,
497 pub call_overhead: u64,
499 pub estimated_savings: i64,
502}
503impl InlineCost {
504 pub fn new() -> Self {
506 InlineCost {
507 body_size: 0,
508 call_overhead: 4,
509 estimated_savings: 0,
510 }
511 }
512 pub fn net_gain(&self) -> i64 {
514 self.estimated_savings + self.call_overhead as i64 - self.body_size as i64
515 }
516 pub fn is_profitable(&self) -> bool {
518 self.net_gain() > 0
519 }
520}
521#[allow(dead_code)]
522#[derive(Default, Debug)]
523pub struct SpeculativeInliner {
524 pub records: Vec<SpeculativeInlineRecord>,
525 pub threshold: f64,
526}
527#[allow(dead_code)]
528impl SpeculativeInliner {
529 pub fn new(threshold: f64) -> Self {
530 SpeculativeInliner {
531 records: Vec::new(),
532 threshold,
533 }
534 }
535 pub fn add(&mut self, record: SpeculativeInlineRecord) {
536 self.records.push(record);
537 }
538 pub fn committed(&self) -> Vec<&SpeculativeInlineRecord> {
539 self.records
540 .iter()
541 .filter(|r| r.should_speculate(self.threshold))
542 .collect()
543 }
544 pub fn pending(&self) -> Vec<&SpeculativeInlineRecord> {
545 self.records.iter().filter(|r| !r.confirmed).collect()
546 }
547 pub fn confirm_callee(&mut self, callee: &str) {
548 for r in &mut self.records {
549 if r.callee == callee {
550 r.confirm();
551 }
552 }
553 }
554 pub fn total_confidence(&self) -> f64 {
555 self.records.iter().map(|r| r.confidence).sum()
556 }
557}
558#[derive(Debug, Clone, PartialEq)]
560pub enum InlineDecision {
561 Always,
563 Never,
565 Heuristic(f64),
567 OnceOnly,
569}
570impl InlineDecision {
571 pub fn should_inline(&self, call_count: u64) -> bool {
576 match self {
577 InlineDecision::Always => true,
578 InlineDecision::Never => false,
579 InlineDecision::Heuristic(score) => *score >= 0.5,
580 InlineDecision::OnceOnly => call_count == 0,
581 }
582 }
583}
584#[allow(dead_code)]
586#[derive(Debug, Default)]
587pub struct InlineFusionManager {
588 pub(super) records: Vec<InlineFusionRecord>,
589 pub(super) next_id: u32,
590}
591#[allow(dead_code)]
592impl InlineFusionManager {
593 pub fn new() -> Self {
594 Self::default()
595 }
596 pub fn fuse(&mut self, caller: &str, first: &str, second: &str, savings: i32) -> String {
598 let id = self.next_id;
599 self.next_id += 1;
600 let fused_name = format!("{}_fused_{}_{}_{}", caller, first, second, id);
601 self.records.push(InlineFusionRecord {
602 caller: caller.to_owned(),
603 first_callee: first.to_owned(),
604 second_callee: second.to_owned(),
605 fused_name: fused_name.clone(),
606 savings_estimate: savings,
607 });
608 fused_name
609 }
610 pub fn all_records(&self) -> &[InlineFusionRecord] {
612 &self.records
613 }
614 pub fn total_savings(&self) -> i32 {
616 self.records.iter().map(|r| r.savings_estimate).sum()
617 }
618}
619#[allow(dead_code)]
620#[derive(Default, Debug)]
621pub struct CallGraph {
622 pub edges: Vec<CallGraphEdge>,
623 pub functions: HashSet<String>,
624}
625#[allow(dead_code)]
626impl CallGraph {
627 pub fn new() -> Self {
628 Self::default()
629 }
630 pub fn build(decls: &[LcnfFunDecl]) -> Self {
631 let mut g = Self::new();
632 for decl in decls {
633 g.functions.insert(decl.name.clone());
634 for callee in collect_callees(&decl.body) {
635 g.edges.push(CallGraphEdge::new(
636 decl.name.clone(),
637 callee.clone(),
638 1,
639 false,
640 ));
641 g.functions.insert(callee);
642 }
643 }
644 g
645 }
646 pub fn callers_of(&self, fn_name: &str) -> Vec<&str> {
647 self.edges
648 .iter()
649 .filter(|e| e.to == fn_name)
650 .map(|e| e.from.as_str())
651 .collect()
652 }
653 pub fn callees_of(&self, fn_name: &str) -> Vec<&str> {
654 self.edges
655 .iter()
656 .filter(|e| e.from == fn_name)
657 .map(|e| e.to.as_str())
658 .collect()
659 }
660 pub fn in_degree(&self, fn_name: &str) -> usize {
661 self.edges.iter().filter(|e| e.to == fn_name).count()
662 }
663 pub fn out_degree(&self, fn_name: &str) -> usize {
664 self.edges.iter().filter(|e| e.from == fn_name).count()
665 }
666 pub fn num_edges(&self) -> usize {
667 self.edges.len()
668 }
669 pub fn num_nodes(&self) -> usize {
670 self.functions.len()
671 }
672 pub fn single_caller_functions(&self) -> Vec<&str> {
673 self.functions
674 .iter()
675 .filter(|f| self.in_degree(f) == 1)
676 .map(|f| f.as_str())
677 .collect()
678 }
679 pub fn leaf_functions(&self) -> Vec<&str> {
680 self.functions
681 .iter()
682 .filter(|f| self.out_degree(f) == 0)
683 .map(|f| f.as_str())
684 .collect()
685 }
686}
687#[allow(dead_code)]
689#[derive(Debug, Clone)]
690pub struct CloneSpecRecord {
691 pub original_callee: String,
692 pub specialized_name: String,
693 pub arg_index: usize,
694 pub specialized_to: String,
695}
696pub struct FreshVarGen {
698 pub(super) next: u64,
699}
700impl FreshVarGen {
701 pub(super) fn new(start: u64) -> Self {
702 FreshVarGen { next: start }
703 }
704 pub(super) fn fresh(&mut self) -> LcnfVarId {
705 let id = self.next;
706 self.next += 1;
707 LcnfVarId(id)
708 }
709}
710#[allow(dead_code)]
712#[derive(Clone, Debug)]
713pub struct SccNode {
714 pub name: String,
715 pub callees: Vec<String>,
716 pub disc: Option<u32>,
717 pub low: Option<u32>,
718 pub on_stack: bool,
719}
720#[allow(dead_code)]
721impl SccNode {
722 pub fn new(name: impl Into<String>, callees: Vec<String>) -> Self {
723 SccNode {
724 name: name.into(),
725 callees,
726 disc: None,
727 low: None,
728 on_stack: false,
729 }
730 }
731}
732#[allow(dead_code)]
733pub struct InlineOrderScheduler {
734 pub order: Vec<String>,
735 pub is_valid: bool,
736}
737#[allow(dead_code)]
738impl InlineOrderScheduler {
739 pub fn compute(graph: &CallGraph) -> Self {
740 let mut visited = HashSet::new();
741 let mut order = Vec::new();
742 for fn_name in &graph.functions {
743 Self::dfs(fn_name, graph, &mut visited, &mut order);
744 }
745 InlineOrderScheduler {
746 order,
747 is_valid: true,
748 }
749 }
750 pub(super) fn dfs(
751 node: &str,
752 graph: &CallGraph,
753 visited: &mut HashSet<String>,
754 order: &mut Vec<String>,
755 ) {
756 if !visited.insert(node.to_owned()) {
757 return;
758 }
759 for callee in graph.callees_of(node) {
760 Self::dfs(callee, graph, visited, order);
761 }
762 order.push(node.to_owned());
763 }
764 pub fn reverse(&mut self) {
765 self.order.reverse();
766 }
767 pub fn len(&self) -> usize {
768 self.order.len()
769 }
770 pub fn is_empty(&self) -> bool {
771 self.order.is_empty()
772 }
773}
774#[allow(dead_code)]
775pub struct PostInlineCleanup {
776 pub dead_removed: usize,
777 pub copies_collapsed: usize,
778 pub tail_calls_simplified: usize,
779}
780#[allow(dead_code)]
781impl PostInlineCleanup {
782 pub fn new() -> Self {
783 PostInlineCleanup {
784 dead_removed: 0,
785 copies_collapsed: 0,
786 tail_calls_simplified: 0,
787 }
788 }
789 pub fn run(&mut self, decl: &mut LcnfFunDecl) {
790 let new_body = self.cleanup_expr(decl.body.clone());
791 decl.body = new_body;
792 }
793 pub(super) fn cleanup_expr(&mut self, expr: LcnfExpr) -> LcnfExpr {
794 match expr {
795 LcnfExpr::Let {
796 id,
797 name,
798 ty,
799 value,
800 body,
801 } => {
802 if let LcnfLetValue::FVar(src) = &value {
803 self.copies_collapsed += 1;
804 let src_arg = LcnfArg::Var(*src);
805 let new_body = self.cleanup_expr(*body);
806 return inline_subst(new_body, id, src_arg);
807 }
808 let new_body = self.cleanup_expr(*body);
809 LcnfExpr::Let {
810 id,
811 name,
812 ty,
813 value,
814 body: Box::new(new_body),
815 }
816 }
817 LcnfExpr::Case {
818 scrutinee,
819 scrutinee_ty,
820 alts,
821 default,
822 } => {
823 let alts2 = alts
824 .into_iter()
825 .map(|alt| crate::lcnf::LcnfAlt {
826 ctor_name: alt.ctor_name,
827 ctor_tag: alt.ctor_tag,
828 params: alt.params,
829 body: self.cleanup_expr(alt.body),
830 })
831 .collect();
832 let default2 = default.map(|d| Box::new(self.cleanup_expr(*d)));
833 LcnfExpr::Case {
834 scrutinee,
835 scrutinee_ty,
836 alts: alts2,
837 default: default2,
838 }
839 }
840 other => other,
841 }
842 }
843 pub fn summary(&self) -> String {
844 format!(
845 "PostInlineCleanup: dead={}, copies_collapsed={}",
846 self.dead_removed, self.copies_collapsed
847 )
848 }
849}
850#[allow(dead_code)]
851pub struct ExtendedInlinePass {
852 pub config: InlineConfig,
853 pub scc: Option<TarjanScc>,
854 pub budget: InlineBudget,
855 pub annotations: InlineAnnotationRegistry,
856 pub size_table: CalleeSizeTable,
857 pub speculative: SpeculativeInliner,
858 pub recursive_limiter: RecursiveInlineLimiter,
859 pub stats: ExtendedInlineStats,
860 pub cleanup: PostInlineCleanup,
861}
862#[allow(dead_code)]
863impl ExtendedInlinePass {
864 pub fn new(config: InlineConfig, total_budget: u64) -> Self {
865 let max_unroll = if config.enable_recursive_inlining {
866 2
867 } else {
868 0
869 };
870 ExtendedInlinePass {
871 config,
872 scc: None,
873 budget: InlineBudget::new(total_budget),
874 annotations: InlineAnnotationRegistry::new(),
875 size_table: CalleeSizeTable::new(),
876 speculative: SpeculativeInliner::new(0.7),
877 recursive_limiter: RecursiveInlineLimiter::new(max_unroll),
878 stats: ExtendedInlineStats::new(),
879 cleanup: PostInlineCleanup::new(),
880 }
881 }
882 pub fn init_scc(&mut self, decls: &[LcnfFunDecl]) {
883 let mut scc = TarjanScc::new(decls);
884 scc.compute();
885 self.scc = Some(scc);
886 self.size_table = CalleeSizeTable::build(decls);
887 }
888 pub fn should_inline_extended(
889 &mut self,
890 caller: &str,
891 callee: &str,
892 _profile: &InlineProfile,
893 ) -> bool {
894 if *self.annotations.get(callee) == InlineAnnotation::NeverInline {
895 return false;
896 }
897 if self.budget.is_exhausted() {
898 return false;
899 }
900 let callee_size = self.size_table.size_of(callee).unwrap_or(u64::MAX);
901 let is_rec = self.scc.as_ref().map_or(false, |s| s.is_recursive(callee));
902 if is_rec {
903 if !self.config.enable_recursive_inlining {
904 return false;
905 }
906 if !self.recursive_limiter.try_unroll(callee) {
907 return false;
908 }
909 }
910 if callee_size > self.config.heuristics.never_inline_size {
911 if is_rec {
912 self.recursive_limiter.pop_unroll(callee);
913 }
914 return false;
915 }
916 if *self.annotations.get(callee) == InlineAnnotation::AlwaysInline
917 && callee_size <= self.config.heuristics.max_inline_size
918 {
919 return true;
920 }
921 if !self.budget.try_spend(caller, callee_size) {
922 if is_rec {
923 self.recursive_limiter.pop_unroll(callee);
924 }
925 return false;
926 }
927 true
928 }
929 pub fn run(&mut self, decls: &mut [LcnfFunDecl]) {
930 self.init_scc(decls);
931 let mut core = InlinePass::new(self.config.clone());
932 core.run(decls);
933 self.stats.always_inline_count = core.report().inlined_count;
934 for decl in decls.iter_mut() {
935 self.cleanup.run(decl);
936 }
937 }
938 pub fn extended_stats(&self) -> &ExtendedInlineStats {
939 &self.stats
940 }
941}
942#[allow(dead_code)]
944#[derive(Debug, Clone)]
945pub struct PartialInlineDecision {
946 pub callee: String,
947 pub region: PartialInlineRegion,
948 pub estimated_savings: i32,
949 pub reason: String,
950}
951#[allow(dead_code)]
952impl PartialInlineDecision {
953 pub fn full(callee: &str, savings: i32) -> Self {
955 Self {
956 callee: callee.to_owned(),
957 region: PartialInlineRegion::Full,
958 estimated_savings: savings,
959 reason: "full".to_owned(),
960 }
961 }
962 pub fn no_inline(callee: &str, reason: &str) -> Self {
964 Self {
965 callee: callee.to_owned(),
966 region: PartialInlineRegion::None,
967 estimated_savings: 0,
968 reason: reason.to_owned(),
969 }
970 }
971 pub fn prefix(callee: &str, n: usize, savings: i32) -> Self {
973 Self {
974 callee: callee.to_owned(),
975 region: PartialInlineRegion::Prefix(n),
976 estimated_savings: savings,
977 reason: format!("prefix({})", n),
978 }
979 }
980 pub fn will_inline(&self) -> bool {
982 !matches!(self.region, PartialInlineRegion::None)
983 }
984}
985#[allow(dead_code)]
986#[derive(Clone, Debug, PartialEq, Eq, Hash)]
987pub struct CallGraphEdge {
988 pub from: String,
989 pub to: String,
990 pub frequency: u64,
991 pub is_tail: bool,
992}
993#[allow(dead_code)]
994impl CallGraphEdge {
995 pub fn new(
996 from: impl Into<String>,
997 to: impl Into<String>,
998 frequency: u64,
999 is_tail: bool,
1000 ) -> Self {
1001 CallGraphEdge {
1002 from: from.into(),
1003 to: to.into(),
1004 frequency,
1005 is_tail,
1006 }
1007 }
1008 pub fn is_inter_scc(&self) -> bool {
1009 self.from != self.to
1010 }
1011}
1012#[derive(Debug, Clone)]
1014pub struct InlineProfile {
1015 pub call_counts: HashMap<String, u64>,
1017 pub hot_functions: HashSet<String>,
1019}
1020impl InlineProfile {
1021 pub fn new() -> Self {
1023 InlineProfile {
1024 call_counts: HashMap::new(),
1025 hot_functions: HashSet::new(),
1026 }
1027 }
1028 pub fn record_call(&mut self, callee: &str) {
1030 let count = self.call_counts.entry(callee.to_owned()).or_insert(0);
1031 *count += 1;
1032 }
1033 pub fn is_hot(&self, name: &str, threshold: u64) -> bool {
1035 self.call_counts.get(name).copied().unwrap_or(0) >= threshold
1036 || self.hot_functions.contains(name)
1037 }
1038 pub fn top_callees(&self, n: usize) -> Vec<(String, u64)> {
1040 let mut pairs: Vec<(String, u64)> = self.call_counts.clone().into_iter().collect();
1041 pairs.sort_by(|a, b| b.1.cmp(&a.1));
1042 pairs.truncate(n);
1043 pairs
1044 }
1045}
1046pub struct InlinePass {
1048 pub config: InlineConfig,
1050 pub fn_map: HashMap<String, LcnfFunDecl>,
1052 pub profile: InlineProfile,
1054 pub inlined_count: usize,
1056 pub(super) next_var_id: u64,
1058 pub(super) inline_counts: HashMap<String, u64>,
1060 pub(super) report: InlineReport,
1062}
1063impl InlinePass {
1064 pub fn new(config: InlineConfig) -> Self {
1066 InlinePass {
1067 config,
1068 fn_map: HashMap::new(),
1069 profile: InlineProfile::new(),
1070 inlined_count: 0,
1071 next_var_id: 1_000_000,
1072 inline_counts: HashMap::new(),
1073 report: InlineReport::default(),
1074 }
1075 }
1076 pub fn run(&mut self, decls: &mut [LcnfFunDecl]) {
1078 for _pass in 0..self.config.max_passes {
1079 self.build_fn_map(decls);
1080 let prev_count = self.inlined_count;
1081 for decl in decls.iter_mut() {
1082 let mut ctx = InliningContext::new();
1083 ctx.push_call(&decl.name.clone());
1084 self.inline_decl(decl, &mut ctx);
1085 ctx.pop_call();
1086 }
1087 if self.inlined_count == prev_count {
1088 break;
1089 }
1090 }
1091 }
1092 pub fn build_fn_map(&mut self, decls: &[LcnfFunDecl]) {
1094 self.fn_map.clear();
1095 for decl in decls {
1096 self.fn_map.insert(decl.name.clone(), decl.clone());
1097 }
1098 }
1099 pub fn inline_decl(&mut self, decl: &mut LcnfFunDecl, ctx: &mut InliningContext) {
1101 let mut body = decl.body.clone();
1102 self.inline_expr(&mut body, ctx);
1103 decl.body = body;
1104 }
1105 pub fn inline_expr(&mut self, expr: &mut LcnfExpr, ctx: &mut InliningContext) {
1107 match expr {
1108 LcnfExpr::Let { value, body, .. } => {
1109 if let LcnfLetValue::App(LcnfArg::Lit(LcnfLit::Str(name)), args) = value {
1110 let callee_name = name.clone();
1111 let args_clone = args.clone();
1112 self.report.total_calls_considered += 1;
1113 if let Some(inlined) = self.try_inline_call_ctx(&callee_name, &args_clone, ctx)
1114 {
1115 let continuation = std::mem::replace(body.as_mut(), LcnfExpr::Unreachable);
1116 *expr = splice_inlined(inlined, continuation);
1117 self.inlined_count += 1;
1118 self.report.inlined_count += 1;
1119 self.inline_expr(expr, ctx);
1120 return;
1121 }
1122 }
1123 self.inline_expr(body, ctx);
1124 }
1125 LcnfExpr::Case { alts, default, .. } => {
1126 for alt in alts.iter_mut() {
1127 self.inline_expr(&mut alt.body, ctx);
1128 }
1129 if let Some(def) = default {
1130 self.inline_expr(def, ctx);
1131 }
1132 }
1133 LcnfExpr::TailCall(LcnfArg::Lit(LcnfLit::Str(name)), args) => {
1134 let callee_name = name.clone();
1135 let args_clone = args.clone();
1136 self.report.total_calls_considered += 1;
1137 if let Some(inlined) = self.try_inline_call_ctx(&callee_name, &args_clone, ctx) {
1138 *expr = inlined;
1139 self.inlined_count += 1;
1140 self.report.inlined_count += 1;
1141 self.inline_expr(expr, ctx);
1142 }
1143 }
1144 LcnfExpr::Return(_) | LcnfExpr::TailCall(_, _) | LcnfExpr::Unreachable => {}
1145 }
1146 }
1147 pub(super) fn try_inline_call_ctx(
1150 &mut self,
1151 callee: &str,
1152 args: &[LcnfArg],
1153 ctx: &mut InliningContext,
1154 ) -> Option<LcnfExpr> {
1155 if ctx.depth >= self.config.heuristics.max_inline_depth {
1156 return None;
1157 }
1158 if ctx.has_cycle(callee) {
1159 self.report.skipped_recursive += 1;
1160 return None;
1161 }
1162 let decl = self.fn_map.get(callee)?.clone();
1163 if decl.is_recursive && !self.config.enable_recursive_inlining {
1164 self.report.skipped_recursive += 1;
1165 return None;
1166 }
1167 let size = estimate_size(&decl);
1168 if size > self.config.heuristics.never_inline_size {
1169 self.report.skipped_too_large += 1;
1170 return None;
1171 }
1172 self.profile.record_call(callee);
1173 let decision = self.config.heuristics.decide(&decl, &self.profile);
1174 let call_count = self.inline_counts.get(callee).copied().unwrap_or(0);
1175 if !decision.should_inline(call_count) {
1176 return None;
1177 }
1178 *self.inline_counts.entry(callee.to_owned()).or_insert(0) += 1;
1179 self.try_inline_call(callee, args)
1180 }
1181 pub fn try_inline_call(&mut self, callee: &str, args: &[LcnfArg]) -> Option<LcnfExpr> {
1184 let decl = self.fn_map.get(callee)?.clone();
1185 let param_names: Vec<String> = decl
1186 .params
1187 .iter()
1188 .map(|p| format!("_x{}", p.id.0))
1189 .collect();
1190 if param_names.len() != args.len() && !args.is_empty() {
1191 return None;
1192 }
1193 let mut gen = FreshVarGen::new(self.next_var_id);
1194 let mut inlined = substitute_params(&decl.body, ¶m_names, args, &mut gen);
1195 for (param, arg) in decl.params.iter().zip(args.iter()).rev() {
1196 let fresh = gen.fresh();
1197 inlined = LcnfExpr::Let {
1198 id: param.id,
1199 name: param.name.clone(),
1200 ty: param.ty.clone(),
1201 value: LcnfLetValue::App(LcnfArg::Var(fresh), vec![]),
1202 body: Box::new(inlined),
1203 };
1204 inlined = LcnfExpr::Let {
1205 id: fresh,
1206 name: format!("_inline_arg_{}", fresh.0),
1207 ty: LcnfType::Object,
1208 value: arg_to_let_value(arg),
1209 body: Box::new(inlined),
1210 };
1211 }
1212 self.next_var_id = gen.next;
1213 Some(inlined)
1214 }
1215 pub fn report(&self) -> &InlineReport {
1217 &self.report
1218 }
1219}
1220#[allow(dead_code)]
1222pub struct TarjanScc {
1223 pub nodes: HashMap<String, SccNode>,
1224 pub sccs: Vec<Vec<String>>,
1225 pub(super) timer: u32,
1226 pub(super) stack: Vec<String>,
1227}
1228#[allow(dead_code)]
1229impl TarjanScc {
1230 pub fn new(decls: &[LcnfFunDecl]) -> Self {
1231 let nodes: HashMap<String, SccNode> = decls
1232 .iter()
1233 .map(|d| {
1234 let callees = collect_callees(&d.body);
1235 (d.name.clone(), SccNode::new(d.name.clone(), callees))
1236 })
1237 .collect();
1238 TarjanScc {
1239 nodes,
1240 sccs: Vec::new(),
1241 timer: 0,
1242 stack: Vec::new(),
1243 }
1244 }
1245 pub fn compute(&mut self) {
1246 let names: Vec<String> = self.nodes.keys().cloned().collect();
1247 for name in &names {
1248 if self.nodes.get(name).and_then(|n| n.disc).is_none() {
1249 self.dfs(name.clone());
1250 }
1251 }
1252 }
1253 pub(super) fn dfs(&mut self, name: String) {
1254 let disc = self.timer;
1255 self.timer += 1;
1256 if let Some(node) = self.nodes.get_mut(&name) {
1257 node.disc = Some(disc);
1258 node.low = Some(disc);
1259 node.on_stack = true;
1260 }
1261 self.stack.push(name.clone());
1262 let callees: Vec<String> = self
1263 .nodes
1264 .get(&name)
1265 .map(|n| n.callees.clone())
1266 .unwrap_or_default();
1267 for callee in &callees {
1268 if !self.nodes.contains_key(callee.as_str()) {
1269 continue;
1270 }
1271 let callee_disc = self.nodes.get(callee).and_then(|n| n.disc);
1272 if callee_disc.is_none() {
1273 self.dfs(callee.clone());
1274 let callee_low = self.nodes.get(callee).and_then(|n| n.low);
1275 let my_low = self.nodes.get(&name).and_then(|n| n.low);
1276 if let (Some(ml), Some(cl)) = (my_low, callee_low) {
1277 if let Some(n) = self.nodes.get_mut(&name) {
1278 n.low = Some(ml.min(cl));
1279 }
1280 }
1281 } else if self.nodes.get(callee).map_or(false, |n| n.on_stack) {
1282 let cd = callee_disc
1283 .expect(
1284 "callee_disc is Some; guaranteed by the else-if branch that checks !callee_disc.is_none()",
1285 );
1286 let ml = self
1287 .nodes
1288 .get(&name)
1289 .and_then(|n| n.low)
1290 .unwrap_or(u32::MAX);
1291 if let Some(n) = self.nodes.get_mut(&name) {
1292 n.low = Some(ml.min(cd));
1293 }
1294 }
1295 }
1296 let my_disc = self.nodes.get(&name).and_then(|n| n.disc);
1297 let my_low = self.nodes.get(&name).and_then(|n| n.low);
1298 if my_disc == my_low {
1299 let mut scc = Vec::new();
1300 loop {
1301 if let Some(top) = self.stack.pop() {
1302 if let Some(n) = self.nodes.get_mut(&top) {
1303 n.on_stack = false;
1304 }
1305 let done = top == name;
1306 scc.push(top);
1307 if done {
1308 break;
1309 }
1310 } else {
1311 break;
1312 }
1313 }
1314 self.sccs.push(scc);
1315 }
1316 }
1317 pub fn is_recursive(&self, name: &str) -> bool {
1318 self.sccs
1319 .iter()
1320 .any(|scc| scc.len() > 1 && scc.contains(&name.to_owned()))
1321 }
1322 pub fn scc_of(&self, name: &str) -> Option<&Vec<String>> {
1323 self.sccs.iter().find(|scc| scc.contains(&name.to_owned()))
1324 }
1325 pub fn num_sccs(&self) -> usize {
1326 self.sccs.len()
1327 }
1328}
1329#[allow(dead_code)]
1330#[derive(Clone, Debug)]
1331pub struct SpeculativeInlineRecord {
1332 pub caller: String,
1333 pub callee: String,
1334 pub confidence: f64,
1335 pub guard_type: String,
1336 pub confirmed: bool,
1337}
1338#[allow(dead_code)]
1339impl SpeculativeInlineRecord {
1340 pub fn new(
1341 caller: impl Into<String>,
1342 callee: impl Into<String>,
1343 confidence: f64,
1344 guard_type: impl Into<String>,
1345 ) -> Self {
1346 SpeculativeInlineRecord {
1347 caller: caller.into(),
1348 callee: callee.into(),
1349 confidence,
1350 guard_type: guard_type.into(),
1351 confirmed: false,
1352 }
1353 }
1354 pub fn should_speculate(&self, threshold: f64) -> bool {
1355 self.confidence >= threshold
1356 }
1357 pub fn confirm(&mut self) {
1358 self.confirmed = true;
1359 }
1360}
1361#[allow(dead_code)]
1362#[derive(Default, Debug, Clone)]
1363pub struct CalleeSizeTable {
1364 pub(super) sizes: HashMap<String, u64>,
1365}
1366#[allow(dead_code)]
1367impl CalleeSizeTable {
1368 pub fn new() -> Self {
1369 Self::default()
1370 }
1371 pub fn build(decls: &[LcnfFunDecl]) -> Self {
1372 let mut t = Self::new();
1373 for d in decls {
1374 t.record(d);
1375 }
1376 t
1377 }
1378 pub fn record(&mut self, decl: &LcnfFunDecl) {
1379 self.sizes.insert(decl.name.clone(), estimate_size(decl));
1380 }
1381 pub fn size_of(&self, fn_name: &str) -> Option<u64> {
1382 self.sizes.get(fn_name).copied()
1383 }
1384 pub fn within_limit(&self, fn_name: &str, limit: u64) -> bool {
1385 self.sizes.get(fn_name).copied().unwrap_or(u64::MAX) <= limit
1386 }
1387 pub fn small_functions(&self, limit: u64) -> Vec<(&str, u64)> {
1388 let mut r: Vec<(&str, u64)> = self
1389 .sizes
1390 .iter()
1391 .filter(|(_, &s)| s <= limit)
1392 .map(|(n, &s)| (n.as_str(), s))
1393 .collect();
1394 r.sort_by_key(|&(_, s)| s);
1395 r
1396 }
1397 pub fn largest(&self) -> Option<(&str, u64)> {
1398 self.sizes
1399 .iter()
1400 .max_by_key(|(_, &s)| s)
1401 .map(|(n, &s)| (n.as_str(), s))
1402 }
1403 pub fn smallest(&self) -> Option<(&str, u64)> {
1404 self.sizes
1405 .iter()
1406 .min_by_key(|(_, &s)| s)
1407 .map(|(n, &s)| (n.as_str(), s))
1408 }
1409 pub fn len(&self) -> usize {
1410 self.sizes.len()
1411 }
1412 pub fn is_empty(&self) -> bool {
1413 self.sizes.is_empty()
1414 }
1415}
1416#[allow(dead_code)]
1417#[derive(Clone, Debug, Default)]
1418pub struct ExtendedInlineStats {
1419 pub always_inline_count: usize,
1420 pub never_inline_count: usize,
1421 pub heuristic_inlined: usize,
1422 pub heuristic_not_inlined: usize,
1423 pub once_only_inlined: usize,
1424 pub total_size_added: u64,
1425 pub total_size_saved: u64,
1426 pub partial_inlines: usize,
1427 pub speculative_inlines: usize,
1428 pub total_functions_processed: usize,
1430 pub inlining_order: Vec<String>,
1432}
1433#[allow(dead_code)]
1434impl ExtendedInlineStats {
1435 pub fn new() -> Self {
1436 Self::default()
1437 }
1438 pub fn record_decision(&mut self, decision: &InlineDecision, did_inline: bool) {
1439 match decision {
1440 InlineDecision::Always => self.always_inline_count += 1,
1441 InlineDecision::Never => self.never_inline_count += 1,
1442 InlineDecision::Heuristic(_) => {
1443 if did_inline {
1444 self.heuristic_inlined += 1;
1445 } else {
1446 self.heuristic_not_inlined += 1;
1447 }
1448 }
1449 InlineDecision::OnceOnly => {
1450 if did_inline {
1451 self.once_only_inlined += 1;
1452 }
1453 }
1454 }
1455 }
1456 pub fn record_size_change(&mut self, added: u64, saved: u64) {
1457 self.total_size_added += added;
1458 self.total_size_saved += saved;
1459 }
1460 pub fn net_size_change(&self) -> i64 {
1461 self.total_size_added as i64 - self.total_size_saved as i64
1462 }
1463 pub fn total_inlined(&self) -> usize {
1464 self.always_inline_count
1465 + self.heuristic_inlined
1466 + self.once_only_inlined
1467 + self.partial_inlines
1468 + self.speculative_inlines
1469 }
1470 pub fn summary(&self) -> String {
1471 format!(
1472 "InlineStats: total={}, always={}, heuristic={}/{}, once={}, net_size={:+}",
1473 self.total_inlined(),
1474 self.always_inline_count,
1475 self.heuristic_inlined,
1476 self.heuristic_inlined + self.heuristic_not_inlined,
1477 self.once_only_inlined,
1478 self.net_size_change()
1479 )
1480 }
1481}
1482#[allow(dead_code)]
1486#[derive(Debug, Default)]
1487pub struct CloneSpecializer {
1488 pub(super) records: Vec<CloneSpecRecord>,
1489 pub(super) next_clone_id: u32,
1490}
1491#[allow(dead_code)]
1492impl CloneSpecializer {
1493 pub fn new() -> Self {
1494 Self::default()
1495 }
1496 pub fn specialized_name(&mut self, original: &str, arg_index: usize, val: &str) -> String {
1498 let id = self.next_clone_id;
1499 self.next_clone_id += 1;
1500 format!("{}_spec_{}_{}_c{}", original, arg_index, val, id)
1501 }
1502 pub fn record(&mut self, original: &str, arg_index: usize, val: &str) -> String {
1504 let name = self.specialized_name(original, arg_index, val);
1505 self.records.push(CloneSpecRecord {
1506 original_callee: original.to_owned(),
1507 specialized_name: name.clone(),
1508 arg_index,
1509 specialized_to: val.to_owned(),
1510 });
1511 name
1512 }
1513 pub fn all_records(&self) -> &[CloneSpecRecord] {
1515 &self.records
1516 }
1517 pub fn count(&self) -> usize {
1519 self.records.len()
1520 }
1521}
1522#[allow(dead_code)]
1529#[derive(Debug)]
1530pub struct InterproceduralInlinePass {
1531 pub config: InlineConfig,
1532 pub budget: InlineBudget,
1533 pub history: InlineHistory,
1534 pub trace: InlineTrace,
1535 pub stats: ExtendedInlineStats,
1536 pub context_stack: InlineContextStack,
1537}
1538#[allow(dead_code)]
1539impl InterproceduralInlinePass {
1540 pub fn new(config: InlineConfig) -> Self {
1542 let max_budget = config.heuristics.never_inline_size * 1000;
1543 Self {
1544 config,
1545 budget: InlineBudget::new(max_budget),
1546 history: InlineHistory::new(),
1547 trace: InlineTrace::new(),
1548 stats: ExtendedInlineStats::default(),
1549 context_stack: InlineContextStack::new(),
1550 }
1551 }
1552 pub fn run(&mut self, decls: &mut Vec<LcnfFunDecl>) {
1554 let cg = CallGraph::build(decls);
1555 let _order = InlineOrderScheduler::compute(&cg);
1556 let mut pass = InlinePass::new(self.config.clone());
1557 pass.run(decls);
1558 self.stats.always_inline_count += decls.len();
1559 }
1560 pub fn report(&self) -> String {
1562 format!(
1563 "InterproceduralInlinePass: {} functions processed, {} always-inlined, budget_used={}/{}",
1564 self.stats.always_inline_count, self.stats.always_inline_count,
1565 self.budget.consumed, self.budget.total_budget,
1566 )
1567 }
1568}
1569#[allow(dead_code)]
1570#[derive(Clone, Debug)]
1571pub struct NestingDepthTracker {
1572 pub current_depth: u32,
1573 pub max_depth: u32,
1574 pub peak_depth: u32,
1575 pub limit_hit_count: u64,
1576}
1577#[allow(dead_code)]
1578impl NestingDepthTracker {
1579 pub fn new(max_depth: u32) -> Self {
1580 NestingDepthTracker {
1581 current_depth: 0,
1582 max_depth,
1583 peak_depth: 0,
1584 limit_hit_count: 0,
1585 }
1586 }
1587 pub fn push(&mut self) -> bool {
1588 if self.current_depth >= self.max_depth {
1589 self.limit_hit_count += 1;
1590 return false;
1591 }
1592 self.current_depth += 1;
1593 if self.current_depth > self.peak_depth {
1594 self.peak_depth = self.current_depth;
1595 }
1596 true
1597 }
1598 pub fn pop(&mut self) {
1599 if self.current_depth > 0 {
1600 self.current_depth -= 1;
1601 }
1602 }
1603 pub fn at_limit(&self) -> bool {
1604 self.current_depth >= self.max_depth
1605 }
1606 pub fn remaining(&self) -> u32 {
1607 self.max_depth.saturating_sub(self.current_depth)
1608 }
1609 pub fn reset(&mut self) {
1610 self.current_depth = 0;
1611 }
1612}
1613#[allow(dead_code)]
1614pub struct CallFrequencyAnalyzer;
1615#[allow(dead_code)]
1616impl CallFrequencyAnalyzer {
1617 pub fn analyze(decls: &[LcnfFunDecl]) -> InlineProfile {
1618 let mut profile = InlineProfile::new();
1619 for decl in decls {
1620 Self::scan_expr(&decl.body, &mut profile);
1621 }
1622 profile
1623 }
1624 pub(super) fn scan_expr(expr: &LcnfExpr, profile: &mut InlineProfile) {
1625 match expr {
1626 LcnfExpr::Let { value, body, .. } => {
1627 if let LcnfLetValue::App(LcnfArg::Lit(LcnfLit::Str(name)), _) = value {
1628 profile.record_call(name);
1629 }
1630 Self::scan_expr(body, profile);
1631 }
1632 LcnfExpr::Case { alts, default, .. } => {
1633 for alt in alts {
1634 Self::scan_expr(&alt.body, profile);
1635 }
1636 if let Some(def) = default {
1637 Self::scan_expr(def, profile);
1638 }
1639 }
1640 LcnfExpr::TailCall(LcnfArg::Lit(LcnfLit::Str(name)), _) => {
1641 profile.record_call(name);
1642 }
1643 _ => {}
1644 }
1645 }
1646 pub fn mark_hot(profile: &mut InlineProfile, hot_threshold: u64) {
1647 let hot: Vec<String> = profile
1648 .call_counts
1649 .iter()
1650 .filter(|(_, &c)| c >= hot_threshold)
1651 .map(|(n, _)| n.clone())
1652 .collect();
1653 for name in hot {
1654 profile.hot_functions.insert(name);
1655 }
1656 }
1657}
1658#[allow(dead_code)]
1659#[derive(Clone, Debug)]
1660pub struct HotPath {
1661 pub prefix_bindings: Vec<(String, LcnfLetValue, LcnfType, LcnfVarId)>,
1662 pub hot_size: u64,
1663 pub cold_size: u64,
1664}
1665#[allow(dead_code)]
1666impl HotPath {
1667 pub fn extract(decl: &LcnfFunDecl) -> Self {
1668 let mut prefix = Vec::new();
1669 let mut hot_size = 0u64;
1670 Self::collect_prefix(&decl.body, &mut prefix, &mut hot_size);
1671 let total = estimate_size(decl);
1672 let cold_size = total.saturating_sub(hot_size);
1673 HotPath {
1674 prefix_bindings: prefix,
1675 hot_size,
1676 cold_size,
1677 }
1678 }
1679 pub(super) fn collect_prefix(
1680 expr: &LcnfExpr,
1681 out: &mut Vec<(String, LcnfLetValue, LcnfType, LcnfVarId)>,
1682 size: &mut u64,
1683 ) {
1684 if let LcnfExpr::Let {
1685 id,
1686 name,
1687 ty,
1688 value,
1689 body,
1690 } = expr
1691 {
1692 out.push((name.clone(), value.clone(), ty.clone(), *id));
1693 *size += 1;
1694 Self::collect_prefix(body, out, size);
1695 }
1696 }
1697 pub fn has_prefix(&self) -> bool {
1698 !self.prefix_bindings.is_empty()
1699 }
1700 pub fn speedup_estimate(&self) -> f64 {
1701 if self.cold_size == 0 {
1702 return 1.0;
1703 }
1704 self.hot_size as f64 / (self.hot_size + self.cold_size) as f64
1705 }
1706 pub fn is_profitable(&self) -> bool {
1707 self.has_prefix() && self.speedup_estimate() > 0.3
1708 }
1709}
1710#[allow(dead_code)]
1713#[derive(Debug, Default)]
1714pub struct InlineHistory {
1715 pub(super) seen: std::collections::HashSet<(String, String)>,
1716}
1717#[allow(dead_code)]
1718impl InlineHistory {
1719 pub fn new() -> Self {
1720 Self::default()
1721 }
1722 pub fn has_seen(&self, caller: &str, callee: &str) -> bool {
1724 self.seen.contains(&(caller.to_owned(), callee.to_owned()))
1725 }
1726 pub fn mark_seen(&mut self, caller: &str, callee: &str) {
1728 self.seen.insert((caller.to_owned(), callee.to_owned()));
1729 }
1730 pub fn reset(&mut self) {
1732 self.seen.clear();
1733 }
1734 pub fn count(&self) -> usize {
1736 self.seen.len()
1737 }
1738}
1739#[derive(Debug, Clone)]
1741pub struct InlineHeuristics {
1742 pub max_inline_size: u64,
1744 pub max_inline_depth: u32,
1746 pub always_inline_threshold: u64,
1748 pub never_inline_size: u64,
1750}
1751impl InlineHeuristics {
1752 pub fn new() -> Self {
1754 Self::default()
1755 }
1756 pub fn decide(&self, decl: &LcnfFunDecl, profile: &InlineProfile) -> InlineDecision {
1758 let size = estimate_size(decl);
1759 if decl.is_recursive && size > self.max_inline_size {
1760 return InlineDecision::Never;
1761 }
1762 if size > self.never_inline_size {
1763 return InlineDecision::Never;
1764 }
1765 if profile.is_hot(&decl.name, self.always_inline_threshold) && size <= self.max_inline_size
1766 {
1767 return InlineDecision::Always;
1768 }
1769 if size <= self.max_inline_size / 4 {
1770 return InlineDecision::Always;
1771 }
1772 if decl.is_recursive {
1773 return InlineDecision::OnceOnly;
1774 }
1775 let score = 1.0 - (size as f64 / self.max_inline_size as f64).min(1.0);
1776 InlineDecision::Heuristic(score)
1777 }
1778}