1use std::collections::{HashMap, HashSet};
14
15use crate::ir::FnId;
16use crate::ir::hir::{
17 ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnDef, ResolvedMatchArm, ResolvedPattern,
18 ResolvedStmt, ResolvedStrPart,
19};
20use crate::types::Type;
21
22#[derive(Debug, Default)]
25pub struct Facts {
26 pub calls_to: HashSet<FnId>,
27 pub tail_calls: HashSet<FnId>,
28 pub builtin_calls: Vec<String>,
29 pub ctor_constructs: Vec<ResolvedCtor>,
30 pub ctor_match_patterns: usize,
31 pub other_match_patterns: usize,
32 pub has_match: bool,
33 pub has_error_prop: bool,
34 pub record_creates: usize,
35 pub last_stmt_is_match: bool,
36 pub only_stmt_is_literal: bool,
37 pub body_stmt_count: usize,
38 pub has_interp_str: bool,
39 pub string_builtin_calls: usize,
40 pub has_match_with_err_arm: bool,
41}
42
43fn walk_expr(e: &ResolvedExpr, facts: &mut Facts) {
44 match e {
45 ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {}
46 ResolvedExpr::Attr(inner, _) => walk_expr(&inner.node, facts),
47 ResolvedExpr::Call(callee, args) => {
48 match callee {
49 ResolvedCallee::Fn(id) => {
50 facts.calls_to.insert(*id);
51 }
52 ResolvedCallee::Builtin(name) => {
53 if name.starts_with("String.") {
54 facts.string_builtin_calls += 1;
55 }
56 facts.builtin_calls.push(name.clone());
57 }
58 ResolvedCallee::Intrinsic(_) => {}
59 ResolvedCallee::LocalSlot { .. } => {}
60 ResolvedCallee::Unresolved { callee } => walk_expr(&callee.node, facts),
61 }
62 for a in args {
63 walk_expr(&a.node, facts);
64 }
65 }
66 ResolvedExpr::BinOp(_, a, b) => {
67 walk_expr(&a.node, facts);
68 walk_expr(&b.node, facts);
69 }
70 ResolvedExpr::Neg(inner) => walk_expr(&inner.node, facts),
71 ResolvedExpr::Match { subject, arms } => {
72 facts.has_match = true;
73 walk_expr(&subject.node, facts);
74 for arm in arms {
75 count_arm_pattern(&arm.pattern, facts);
76 walk_match_arm(arm, facts);
77 }
78 }
79 ResolvedExpr::Ctor(c, args) => {
80 facts.ctor_constructs.push(c.clone());
81 for a in args {
82 walk_expr(&a.node, facts);
83 }
84 }
85 ResolvedExpr::ErrorProp(inner) => {
86 facts.has_error_prop = true;
87 walk_expr(&inner.node, facts);
88 }
89 ResolvedExpr::InterpolatedStr(parts) => {
90 facts.has_interp_str = true;
91 for p in parts {
92 if let ResolvedStrPart::Parsed(e) = p {
93 walk_expr(&e.node, facts);
94 }
95 }
96 }
97 ResolvedExpr::List(xs) | ResolvedExpr::Tuple(xs) => {
98 for x in xs {
99 walk_expr(&x.node, facts);
100 }
101 }
102 ResolvedExpr::MapLiteral(pairs) => {
103 for (k, v) in pairs {
104 walk_expr(&k.node, facts);
105 walk_expr(&v.node, facts);
106 }
107 }
108 ResolvedExpr::RecordCreate { fields, .. } => {
109 facts.record_creates += 1;
110 for (_, v) in fields {
111 walk_expr(&v.node, facts);
112 }
113 }
114 ResolvedExpr::RecordUpdate { base, updates, .. } => {
115 walk_expr(&base.node, facts);
116 for (_, v) in updates {
117 walk_expr(&v.node, facts);
118 }
119 }
120 ResolvedExpr::TailCall { target, args } => {
121 facts.tail_calls.insert(*target);
122 facts.calls_to.insert(*target);
123 for a in args {
124 walk_expr(&a.node, facts);
125 }
126 }
127 ResolvedExpr::IndependentProduct(xs, _) => {
128 for x in xs {
129 walk_expr(&x.node, facts);
130 }
131 }
132 }
133}
134
135fn count_arm_pattern(p: &ResolvedPattern, facts: &mut Facts) {
136 match p {
137 ResolvedPattern::Ctor(ctor, _) => {
138 facts.ctor_match_patterns += 1;
139 if matches!(
140 ctor,
141 ResolvedCtor::Builtin(crate::ir::hir::BuiltinCtor::ResultErr)
142 ) {
143 facts.has_match_with_err_arm = true;
144 }
145 }
146 _ => facts.other_match_patterns += 1,
147 }
148}
149
150fn walk_match_arm(arm: &ResolvedMatchArm, facts: &mut Facts) {
151 walk_expr(&arm.body.node, facts);
152}
153
154pub fn extract_facts(fd: &ResolvedFnDef) -> Facts {
155 let mut facts = Facts::default();
156 let stmts = fd.body.stmts();
157 facts.body_stmt_count = stmts.len();
158 for stmt in stmts {
159 match stmt {
160 ResolvedStmt::Binding { value, .. } => walk_expr(&value.node, &mut facts),
161 ResolvedStmt::Expr(value) => walk_expr(&value.node, &mut facts),
162 }
163 }
164 if let Some(last) = stmts.last() {
165 let expr = match last {
166 ResolvedStmt::Binding { value, .. } => &value.node,
167 ResolvedStmt::Expr(value) => &value.node,
168 };
169 facts.last_stmt_is_match = matches!(expr, ResolvedExpr::Match { .. });
170 }
171 facts.only_stmt_is_literal = stmts.len() == 1 && {
172 let expr = match &stmts[0] {
173 ResolvedStmt::Binding { value, .. } => &value.node,
174 ResolvedStmt::Expr(value) => &value.node,
175 };
176 matches!(
177 expr,
178 ResolvedExpr::Literal(_)
179 | ResolvedExpr::List(_)
180 | ResolvedExpr::Tuple(_)
181 | ResolvedExpr::MapLiteral(_)
182 | ResolvedExpr::RecordCreate { .. }
183 | ResolvedExpr::Ctor(_, _)
184 )
185 };
186 facts
187}
188
189pub fn classify(fd: &ResolvedFnDef, facts: &Facts, scc: &HashSet<FnId>) -> Vec<Archetype> {
192 let mut labels = Vec::new();
193
194 let self_call = facts.calls_to.contains(&fd.fn_id) || facts.tail_calls.contains(&fd.fn_id);
195 if self_call {
196 labels.push(Archetype::StructuralRecursion);
197 }
198 if scc.contains(&fd.fn_id) {
199 labels.push(Archetype::SccMutual);
200 }
201
202 if facts.last_stmt_is_match {
203 if facts.ctor_match_patterns >= facts.other_match_patterns && facts.ctor_match_patterns > 0
204 {
205 labels.push(Archetype::MatchDispatcher);
206 } else {
207 labels.push(Archetype::MatchOnValue);
208 }
209 }
210
211 if !fd.effects.is_empty() {
212 let total_calls = facts.calls_to.len() + facts.builtin_calls.len();
213 if total_calls >= 2 {
214 labels.push(Archetype::Orchestration);
215 } else {
216 labels.push(Archetype::EffectfulLeaf);
217 }
218 }
219
220 let is_result_ret = type_is_result(&fd.return_type);
221 if is_result_ret && facts.has_error_prop {
222 labels.push(Archetype::PipelineResult);
223 } else if is_result_ret && facts.has_match_with_err_arm {
224 labels.push(Archetype::ManualResultAdapter);
225 }
226
227 let is_string_ret = matches!(&fd.return_type, Type::Named { name, .. } if name == "String");
228 if is_string_ret
229 && fd.effects.is_empty()
230 && (facts.has_interp_str || facts.string_builtin_calls >= 1)
231 && (facts.body_stmt_count >= 2 || facts.has_interp_str)
232 {
233 labels.push(Archetype::RendererFormatter);
234 }
235
236 if facts.body_stmt_count == 1
237 && (!facts.ctor_constructs.is_empty() || facts.record_creates > 0)
238 && !facts.has_match
239 {
240 labels.push(Archetype::ConstructorWrapper);
241 }
242
243 if fd.params.is_empty()
244 && facts.calls_to.is_empty()
245 && facts.builtin_calls.is_empty()
246 && fd.effects.is_empty()
247 && facts.only_stmt_is_literal
248 {
249 labels.push(Archetype::DataAsFunction);
250 }
251
252 if facts.body_stmt_count == 1
253 && !facts.has_match
254 && !self_call
255 && fd.effects.is_empty()
256 && (!facts.builtin_calls.is_empty() || !facts.calls_to.is_empty())
257 {
258 labels.push(Archetype::TrivialHelper);
259 }
260
261 if facts.body_stmt_count == 1
262 && !facts.has_match
263 && !self_call
264 && fd.effects.is_empty()
265 && facts.builtin_calls.is_empty()
266 && facts.calls_to.is_empty()
267 && facts.ctor_constructs.is_empty()
268 && !fd.params.is_empty()
269 {
270 labels.push(Archetype::PureExpression);
271 }
272
273 if facts.body_stmt_count >= 2
274 && !facts.last_stmt_is_match
275 && fd.effects.is_empty()
276 && (!facts.builtin_calls.is_empty() || !facts.calls_to.is_empty())
277 && !self_call
278 {
279 labels.push(Archetype::LetPipeline);
280 }
281
282 labels
283}
284
285pub fn type_is_result(t: &Type) -> bool {
286 matches!(t, Type::Named { name, .. } if name == "Result")
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
299pub enum Archetype {
300 SccMutual,
301 StructuralRecursion,
302 MatchDispatcher,
303 PipelineResult,
304 ManualResultAdapter,
305 RendererFormatter,
306 MatchOnValue,
307 Orchestration,
308 EffectfulLeaf,
309 LetPipeline,
310 ConstructorWrapper,
311 DataAsFunction,
312 TrivialHelper,
313 PureExpression,
314 Unclassified,
317}
318
319impl Archetype {
320 pub fn all() -> &'static [Archetype] {
324 &[
325 Archetype::SccMutual,
326 Archetype::StructuralRecursion,
327 Archetype::MatchDispatcher,
328 Archetype::PipelineResult,
329 Archetype::ManualResultAdapter,
330 Archetype::RendererFormatter,
331 Archetype::MatchOnValue,
332 Archetype::Orchestration,
333 Archetype::EffectfulLeaf,
334 Archetype::LetPipeline,
335 Archetype::ConstructorWrapper,
336 Archetype::DataAsFunction,
337 Archetype::TrivialHelper,
338 Archetype::PureExpression,
339 ]
340 }
341
342 pub fn as_str(&self) -> &'static str {
344 match self {
345 Archetype::SccMutual => "scc-mutual",
346 Archetype::StructuralRecursion => "structural-recursion",
347 Archetype::MatchDispatcher => "match-dispatcher",
348 Archetype::PipelineResult => "pipeline-result",
349 Archetype::ManualResultAdapter => "manual-result-adapter",
350 Archetype::RendererFormatter => "renderer-formatter",
351 Archetype::MatchOnValue => "match-on-value",
352 Archetype::Orchestration => "orchestration",
353 Archetype::EffectfulLeaf => "effectful-leaf",
354 Archetype::LetPipeline => "let-pipeline",
355 Archetype::ConstructorWrapper => "constructor-wrapper",
356 Archetype::DataAsFunction => "data-as-function",
357 Archetype::TrivialHelper => "trivial-helper",
358 Archetype::PureExpression => "pure-expression",
359 Archetype::Unclassified => "unclassified",
360 }
361 }
362
363 pub fn parse(s: &str) -> Option<Archetype> {
367 Some(match s {
368 "scc-mutual" => Archetype::SccMutual,
369 "structural-recursion" => Archetype::StructuralRecursion,
370 "match-dispatcher" => Archetype::MatchDispatcher,
371 "pipeline-result" => Archetype::PipelineResult,
372 "manual-result-adapter" => Archetype::ManualResultAdapter,
373 "renderer-formatter" => Archetype::RendererFormatter,
374 "match-on-value" => Archetype::MatchOnValue,
375 "orchestration" => Archetype::Orchestration,
376 "effectful-leaf" => Archetype::EffectfulLeaf,
377 "let-pipeline" => Archetype::LetPipeline,
378 "constructor-wrapper" => Archetype::ConstructorWrapper,
379 "data-as-function" => Archetype::DataAsFunction,
380 "trivial-helper" => Archetype::TrivialHelper,
381 "pure-expression" => Archetype::PureExpression,
382 "unclassified" => Archetype::Unclassified,
383 _ => return None,
384 })
385 }
386}
387
388pub fn primary_label(labels: &[Archetype]) -> Archetype {
389 for &want in Archetype::all() {
390 if labels.contains(&want) {
391 return want;
392 }
393 }
394 Archetype::Unclassified
395}
396
397pub fn compute_sccs(fns: &[&ResolvedFnDef], facts_by_id: &HashMap<FnId, &Facts>) -> HashSet<FnId> {
400 let mut graph: HashMap<FnId, Vec<FnId>> = HashMap::new();
401 let fn_ids: HashSet<FnId> = fns.iter().map(|f| f.fn_id).collect();
402 for fd in fns {
403 if let Some(facts) = facts_by_id.get(&fd.fn_id) {
404 let edges: Vec<FnId> = facts
405 .calls_to
406 .iter()
407 .copied()
408 .filter(|c| fn_ids.contains(c) && *c != fd.fn_id)
409 .collect();
410 graph.insert(fd.fn_id, edges);
411 }
412 }
413 let nodes: Vec<FnId> = graph.keys().copied().collect();
418 crate::scc::mutually_recursive(&nodes, &graph)
419}
420
421#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct FnRecognition {
432 pub primary: Archetype,
433 pub labels: Vec<Archetype>,
434}
435
436#[derive(Debug, Clone, Default)]
451pub struct ProgramShape {
452 pub per_fn: std::collections::HashMap<FnId, FnRecognition>,
454 pub sccs: HashSet<FnId>,
458 pub patterns: Vec<ModulePattern>,
464 pub inductable_sum_types: HashSet<String>,
472}
473
474impl ProgramShape {
475 pub fn for_fn(&self, fn_id: FnId) -> Option<&FnRecognition> {
479 self.per_fn.get(&fn_id)
480 }
481}
482
483pub fn analyze_program(resolved_fns: &[&ResolvedFnDef]) -> ProgramShape {
494 let mut facts_by_id: std::collections::HashMap<FnId, Facts> =
495 std::collections::HashMap::with_capacity(resolved_fns.len());
496 for fd in resolved_fns {
497 facts_by_id.insert(fd.fn_id, extract_facts(fd));
498 }
499 let facts_refs: std::collections::HashMap<FnId, &Facts> =
500 facts_by_id.iter().map(|(k, v)| (*k, v)).collect();
501 let sccs = compute_sccs(resolved_fns, &facts_refs);
502
503 let mut per_fn = std::collections::HashMap::with_capacity(resolved_fns.len());
504 for fd in resolved_fns {
505 let facts = &facts_by_id[&fd.fn_id];
506 let labels = classify(fd, facts, &sccs);
507 let primary = primary_label(&labels);
508 per_fn.insert(fd.fn_id, FnRecognition { primary, labels });
509 }
510
511 ProgramShape {
512 per_fn,
513 sccs,
514 patterns: Vec::new(),
515 inductable_sum_types: HashSet::new(),
516 }
517}
518
519pub fn analyze_program_with_modules(
524 resolved_fns: &[&ResolvedFnDef],
525 entry_items: &[crate::ast::TopLevel],
526 dep_modules: &[crate::codegen::ModuleInfo],
527) -> ProgramShape {
528 let mut shape = analyze_program(resolved_fns);
529 shape.patterns = detect_module_patterns(entry_items, dep_modules);
530 shape.inductable_sum_types = collect_inductable_sum_types(entry_items, dep_modules);
531 shape
532}
533
534#[derive(Debug, Clone)]
559pub enum ModulePattern {
560 RefinementSmartConstructor {
568 scope: Option<String>,
575 type_name: String,
579 carrier_field: String,
583 carrier_type: String,
587 constructor_fn: String,
589 param_name: String,
594 predicate: crate::ast::Spanned<crate::ast::Expr>,
598 },
599 WrapperOverRecursion {
619 wrapper_scope: Option<String>,
623 wrapper_fn: String,
625 inner_scope: Option<String>,
628 inner_fn: String,
630 },
631 ResultPipelineChain {
648 scope: Option<String>,
649 fn_name: String,
650 step_count: usize,
651 step_fns: Vec<String>,
658 },
659 RendererFormatter {
676 scope: Option<String>,
677 fn_name: String,
678 },
679 MatchDispatcherFold {
698 scope: Option<String>,
699 fn_name: String,
700 list_param: String,
701 },
702}
703
704pub fn collect_inductable_sum_types(
713 entry_items: &[crate::ast::TopLevel],
714 dep_modules: &[crate::codegen::ModuleInfo],
715) -> HashSet<String> {
716 use crate::ast::{TopLevel, TypeDef};
717 let mut out = HashSet::new();
718 let mut consider = |td: &TypeDef| {
719 if let TypeDef::Sum { name, variants, .. } = td
720 && crate::codegen::common::is_recursive_sum(name, variants)
721 && !indirect_rec_variants(variants, name)
722 {
723 out.insert(name.clone());
724 }
725 };
726 for item in entry_items {
727 if let TopLevel::TypeDef(td) = item {
728 consider(td);
729 }
730 }
731 for m in dep_modules {
732 for td in &m.type_defs {
733 consider(td);
734 }
735 }
736 out
737}
738
739fn indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
743 for variant in variants {
744 for field in &variant.fields {
745 let f = field.trim();
746 if f == type_name {
747 continue;
748 }
749 let opens = f.matches('<').count();
750 if opens > 1 && f.contains(type_name) {
751 return true;
752 }
753 }
754 }
755 false
756}
757
758pub fn detect_module_patterns(
767 entry_items: &[crate::ast::TopLevel],
768 dep_modules: &[crate::codegen::ModuleInfo],
769) -> Vec<ModulePattern> {
770 use crate::ast::{Expr, Stmt, TopLevel, TypeDef};
771
772 let mut out = Vec::new();
773
774 struct CandidateRecord<'a> {
781 scope: Option<String>,
782 type_name: &'a str,
783 carrier_field: &'a str,
784 carrier_type: &'a str,
785 fns: Vec<&'a crate::ast::FnDef>,
786 }
787
788 let entry_fns: Vec<&crate::ast::FnDef> = entry_items
789 .iter()
790 .filter_map(|i| match i {
791 TopLevel::FnDef(fd) => Some(fd),
792 _ => None,
793 })
794 .collect();
795
796 let mut candidates: Vec<CandidateRecord<'_>> = Vec::new();
797 for td in entry_items.iter().filter_map(|i| match i {
798 TopLevel::TypeDef(td) => Some(td),
799 _ => None,
800 }) {
801 if let TypeDef::Product { name, fields, .. } = td
802 && fields.len() == 1
803 {
804 let (fname, ftype) = &fields[0];
805 candidates.push(CandidateRecord {
806 scope: None,
807 type_name: name.as_str(),
808 carrier_field: fname.as_str(),
809 carrier_type: ftype.as_str(),
810 fns: entry_fns.clone(),
811 });
812 }
813 }
814 for m in dep_modules {
815 let module_fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
816 for td in &m.type_defs {
817 if let TypeDef::Product { name, fields, .. } = td
818 && fields.len() == 1
819 {
820 let (fname, ftype) = &fields[0];
821 candidates.push(CandidateRecord {
822 scope: Some(m.prefix.clone()),
823 type_name: name.as_str(),
824 carrier_field: fname.as_str(),
825 carrier_type: ftype.as_str(),
826 fns: module_fns.clone(),
827 });
828 }
829 }
830 }
831
832 for candidate in &candidates {
839 let CandidateRecord {
840 scope,
841 type_name,
842 carrier_field,
843 carrier_type,
844 fns,
845 } = candidate;
846 for fd in fns {
847 if !fd.return_type.starts_with("Result<") {
848 continue;
849 }
850 if !fd.return_type[7..].starts_with(*type_name) {
851 continue;
852 }
853 if fd.params.len() != 1 {
854 continue;
855 }
856 let (param_name, _) = &fd.params[0];
857 let stmts = fd.body.stmts();
858 if stmts.len() != 1 {
859 continue;
860 }
861 let Stmt::Expr(body_expr) = &stmts[0] else {
862 continue;
863 };
864 let Expr::Match { subject, arms } = &body_expr.node else {
865 continue;
866 };
867 if !crate::codegen::common::is_refinement_bool_ok_err_match(
868 arms,
869 type_name,
870 carrier_field,
871 param_name,
872 ) {
873 continue;
874 }
875 out.push(ModulePattern::RefinementSmartConstructor {
876 scope: scope.clone(),
877 type_name: (*type_name).to_string(),
878 carrier_field: (*carrier_field).to_string(),
879 carrier_type: (*carrier_type).to_string(),
880 constructor_fn: fd.name.clone(),
881 param_name: param_name.clone(),
882 predicate: (**subject).clone(),
883 });
884 break;
885 }
886 }
887
888 detect_wrapper_over_recursion(None, &entry_fns, &mut out);
892 for m in dep_modules {
893 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
894 detect_wrapper_over_recursion(Some(m.prefix.clone()), &fns, &mut out);
895 }
896
897 detect_result_pipeline_chain(None, &entry_fns, &mut out);
899 for m in dep_modules {
900 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
901 detect_result_pipeline_chain(Some(m.prefix.clone()), &fns, &mut out);
902 }
903
904 detect_renderer_formatter(None, &entry_fns, &mut out);
906 for m in dep_modules {
907 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
908 detect_renderer_formatter(Some(m.prefix.clone()), &fns, &mut out);
909 }
910
911 detect_match_dispatcher_fold(None, &entry_fns, &mut out);
913 for m in dep_modules {
914 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
915 detect_match_dispatcher_fold(Some(m.prefix.clone()), &fns, &mut out);
916 }
917
918 out
919}
920
921fn detect_match_dispatcher_fold(
924 scope: Option<String>,
925 fns: &[&crate::ast::FnDef],
926 out: &mut Vec<ModulePattern>,
927) {
928 use crate::ast::{Expr, Pattern, Stmt};
929 for fd in fns {
930 let stmts = fd.body.stmts();
931 if stmts.len() != 1 {
932 continue;
933 }
934 let Stmt::Expr(body_expr) = &stmts[0] else {
935 continue;
936 };
937 let Expr::Match { subject, arms } = &body_expr.node else {
938 continue;
939 };
940 let subj_name = match &subject.node {
945 Expr::Ident(n) => n.as_str(),
946 Expr::Resolved { name, .. } => name.as_str(),
947 _ => continue,
948 };
949 if !fd.params.iter().any(|(n, _)| n == subj_name) {
950 continue;
951 }
952 let has_nil = arms.iter().any(|a| matches!(a.pattern, Pattern::EmptyList));
953 let has_cons = arms
954 .iter()
955 .any(|a| matches!(a.pattern, Pattern::Cons(_, _)));
956 if !(has_nil && has_cons) {
957 continue;
958 }
959 if !body_calls_name(&fd.body, &fd.name) {
960 continue;
961 }
962 out.push(ModulePattern::MatchDispatcherFold {
963 scope: scope.clone(),
964 fn_name: fd.name.clone(),
965 list_param: subj_name.to_string(),
966 });
967 }
968}
969
970fn detect_renderer_formatter(
973 scope: Option<String>,
974 fns: &[&crate::ast::FnDef],
975 out: &mut Vec<ModulePattern>,
976) {
977 for fd in fns {
978 if fd.return_type != "String" {
979 continue;
980 }
981 if !fd.effects.is_empty() {
982 continue;
983 }
984 if body_calls_name(&fd.body, &fd.name) {
985 continue;
986 }
987 if !body_has_string_building(&fd.body) {
988 continue;
989 }
990 out.push(ModulePattern::RendererFormatter {
991 scope: scope.clone(),
992 fn_name: fd.name.clone(),
993 });
994 }
995}
996
997fn body_has_string_building(body: &crate::ast::FnBody) -> bool {
1005 for stmt in body.stmts() {
1006 let expr = match stmt {
1007 crate::ast::Stmt::Binding(_, _, e) => e,
1008 crate::ast::Stmt::Expr(e) => e,
1009 };
1010 if expr_has_string_building(expr) {
1011 return true;
1012 }
1013 }
1014 false
1015}
1016
1017fn expr_has_string_building(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
1018 use crate::ast::Expr;
1019 match &expr.node {
1020 Expr::InterpolatedStr(_) => true,
1021 Expr::BinOp(crate::ast::BinOp::Add, _, _) => true,
1022 Expr::FnCall(callee, args) => {
1023 expr_has_string_building(callee) || args.iter().any(expr_has_string_building)
1024 }
1025 Expr::TailCall(td) => td.args.iter().any(expr_has_string_building),
1026 Expr::Match { subject, arms } => {
1027 expr_has_string_building(subject)
1028 || arms.iter().any(|a| expr_has_string_building(&a.body))
1029 }
1030 Expr::BinOp(_, l, r) => expr_has_string_building(l) || expr_has_string_building(r),
1031 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_has_string_building(e),
1032 Expr::Constructor(_, Some(e)) => expr_has_string_building(e),
1033 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1034 xs.iter().any(expr_has_string_building)
1035 }
1036 Expr::MapLiteral(pairs) => pairs
1037 .iter()
1038 .any(|(k, v)| expr_has_string_building(k) || expr_has_string_building(v)),
1039 Expr::RecordCreate { fields, .. } => {
1040 fields.iter().any(|(_, e)| expr_has_string_building(e))
1041 }
1042 Expr::RecordUpdate { base, updates, .. } => {
1043 expr_has_string_building(base)
1044 || updates.iter().any(|(_, e)| expr_has_string_building(e))
1045 }
1046 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1047 false
1048 }
1049 }
1050}
1051
1052fn detect_result_pipeline_chain(
1058 scope: Option<String>,
1059 fns: &[&crate::ast::FnDef],
1060 out: &mut Vec<ModulePattern>,
1061) {
1062 use crate::ast::{Expr, Stmt};
1063 for fd in fns {
1064 if !fd.return_type.starts_with("Result<") {
1065 continue;
1066 }
1067 let stmts = fd.body.stmts();
1068 if stmts.len() < 2 {
1069 continue;
1070 }
1071 if !matches!(stmts.last(), Some(Stmt::Expr(_))) {
1072 continue;
1073 }
1074 let mut step_fns: Vec<String> = Vec::new();
1075 for stmt in stmts {
1076 if let Stmt::Binding(_, _, value) = stmt
1077 && let Expr::ErrorProp(inner) = &value.node
1078 && let Expr::FnCall(callee, _) = &inner.node
1079 && let Expr::Ident(name) = &callee.node
1080 {
1081 step_fns.push(name.clone());
1082 }
1083 }
1084 if step_fns.len() < 2 {
1085 continue;
1086 }
1087 let step_count = step_fns.len();
1088 out.push(ModulePattern::ResultPipelineChain {
1089 scope: scope.clone(),
1090 fn_name: fd.name.clone(),
1091 step_count,
1092 step_fns,
1093 });
1094 }
1095}
1096
1097fn detect_wrapper_over_recursion(
1103 scope: Option<String>,
1104 fns: &[&crate::ast::FnDef],
1105 out: &mut Vec<ModulePattern>,
1106) {
1107 if fns.is_empty() {
1108 return;
1109 }
1110
1111 let mut recursive: HashSet<String> = HashSet::new();
1112 for fd in fns {
1113 if body_calls_name(&fd.body, &fd.name) {
1114 recursive.insert(fd.name.clone());
1115 }
1116 }
1117 if recursive.is_empty() {
1118 return;
1119 }
1120
1121 for fd in fns {
1122 if recursive.contains(&fd.name) {
1123 continue;
1124 }
1125 if fd.params.is_empty() {
1126 continue;
1127 }
1128 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1129 let mut hits: Vec<String> = Vec::new();
1130 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1131 hits.sort();
1132 hits.dedup();
1133 if hits.len() != 1 {
1134 continue;
1135 }
1136 let inner = hits.into_iter().next().unwrap();
1137 out.push(ModulePattern::WrapperOverRecursion {
1138 wrapper_scope: scope.clone(),
1139 wrapper_fn: fd.name.clone(),
1140 inner_scope: scope.clone(),
1141 inner_fn: inner,
1142 });
1143 }
1144}
1145
1146fn body_calls_name(body: &crate::ast::FnBody, name: &str) -> bool {
1150 for stmt in body.stmts() {
1151 let expr = match stmt {
1152 crate::ast::Stmt::Binding(_, _, e) => e,
1153 crate::ast::Stmt::Expr(e) => e,
1154 };
1155 if expr_calls_name(expr, name) {
1156 return true;
1157 }
1158 }
1159 false
1160}
1161
1162fn expr_calls_name(expr: &crate::ast::Spanned<crate::ast::Expr>, name: &str) -> bool {
1163 use crate::ast::Expr;
1164 match &expr.node {
1165 Expr::FnCall(callee, args) => {
1166 if let Expr::Ident(n) = &callee.node
1167 && n == name
1168 {
1169 return true;
1170 }
1171 if expr_calls_name(callee, name) {
1172 return true;
1173 }
1174 args.iter().any(|a| expr_calls_name(a, name))
1175 }
1176 Expr::TailCall(td) => td.target == name || td.args.iter().any(|a| expr_calls_name(a, name)),
1177 Expr::Match { subject, arms } => {
1178 if expr_calls_name(subject, name) {
1179 return true;
1180 }
1181 arms.iter().any(|a| expr_calls_name(&a.body, name))
1182 }
1183 Expr::BinOp(_, l, r) => expr_calls_name(l, name) || expr_calls_name(r, name),
1184 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_calls_name(e, name),
1185 Expr::Constructor(_, Some(e)) => expr_calls_name(e, name),
1186 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1187 xs.iter().any(|x| expr_calls_name(x, name))
1188 }
1189 Expr::MapLiteral(pairs) => pairs
1190 .iter()
1191 .any(|(k, v)| expr_calls_name(k, name) || expr_calls_name(v, name)),
1192 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_name(e, name)),
1193 Expr::RecordUpdate { base, updates, .. } => {
1194 expr_calls_name(base, name) || updates.iter().any(|(_, e)| expr_calls_name(e, name))
1195 }
1196 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1197 crate::ast::StrPart::Parsed(e) => expr_calls_name(e, name),
1198 crate::ast::StrPart::Literal(_) => false,
1199 }),
1200 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1201 false
1202 }
1203 }
1204}
1205
1206fn collect_qualifying_inner_calls(
1212 body: &crate::ast::FnBody,
1213 outer_params: &[&str],
1214 recursive: &HashSet<String>,
1215 out: &mut Vec<String>,
1216) {
1217 for stmt in body.stmts() {
1218 let expr = match stmt {
1219 crate::ast::Stmt::Binding(_, _, e) => e,
1220 crate::ast::Stmt::Expr(e) => e,
1221 };
1222 collect_qualifying_in_expr(expr, outer_params, recursive, out);
1223 }
1224}
1225
1226fn collect_qualifying_in_expr(
1227 expr: &crate::ast::Spanned<crate::ast::Expr>,
1228 outer_params: &[&str],
1229 recursive: &HashSet<String>,
1230 out: &mut Vec<String>,
1231) {
1232 use crate::ast::Expr;
1233 let try_qualify = |callee: &str, args: &[crate::ast::Spanned<Expr>], out: &mut Vec<String>| {
1234 if !recursive.contains(callee) {
1235 return;
1236 }
1237 if args.len() <= outer_params.len() {
1238 return;
1239 }
1240 let mut arg_idents: HashSet<&str> = HashSet::new();
1244 for a in args {
1245 match &a.node {
1246 Expr::Ident(n) => {
1247 arg_idents.insert(n.as_str());
1248 }
1249 Expr::Resolved { name, .. } => {
1250 arg_idents.insert(name.as_str());
1251 }
1252 _ => {}
1253 }
1254 }
1255 if outer_params.iter().all(|p| arg_idents.contains(*p)) {
1256 out.push(callee.to_string());
1257 }
1258 };
1259 if let Expr::FnCall(callee, args) = &expr.node
1260 && let Expr::Ident(name) = &callee.node
1261 {
1262 try_qualify(name, args, out);
1263 }
1264 if let Expr::TailCall(td) = &expr.node {
1270 try_qualify(&td.target, &td.args, out);
1271 }
1272 match &expr.node {
1273 Expr::FnCall(callee, args) => {
1274 collect_qualifying_in_expr(callee, outer_params, recursive, out);
1275 for a in args {
1276 collect_qualifying_in_expr(a, outer_params, recursive, out);
1277 }
1278 }
1279 Expr::Match { subject, arms } => {
1280 collect_qualifying_in_expr(subject, outer_params, recursive, out);
1281 for a in arms {
1282 collect_qualifying_in_expr(&a.body, outer_params, recursive, out);
1283 }
1284 }
1285 Expr::BinOp(_, l, r) => {
1286 collect_qualifying_in_expr(l, outer_params, recursive, out);
1287 collect_qualifying_in_expr(r, outer_params, recursive, out);
1288 }
1289 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => {
1290 collect_qualifying_in_expr(e, outer_params, recursive, out);
1291 }
1292 Expr::Constructor(_, Some(e)) => {
1293 collect_qualifying_in_expr(e, outer_params, recursive, out);
1294 }
1295 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1296 for x in xs {
1297 collect_qualifying_in_expr(x, outer_params, recursive, out);
1298 }
1299 }
1300 Expr::MapLiteral(pairs) => {
1301 for (k, v) in pairs {
1302 collect_qualifying_in_expr(k, outer_params, recursive, out);
1303 collect_qualifying_in_expr(v, outer_params, recursive, out);
1304 }
1305 }
1306 Expr::RecordCreate { fields, .. } => {
1307 for (_, e) in fields {
1308 collect_qualifying_in_expr(e, outer_params, recursive, out);
1309 }
1310 }
1311 Expr::RecordUpdate { base, updates, .. } => {
1312 collect_qualifying_in_expr(base, outer_params, recursive, out);
1313 for (_, e) in updates {
1314 collect_qualifying_in_expr(e, outer_params, recursive, out);
1315 }
1316 }
1317 Expr::InterpolatedStr(parts) => {
1318 for p in parts {
1319 if let crate::ast::StrPart::Parsed(e) = p {
1320 collect_qualifying_in_expr(e, outer_params, recursive, out);
1321 }
1322 }
1323 }
1324 Expr::TailCall(td) => {
1325 for a in &td.args {
1326 collect_qualifying_in_expr(a, outer_params, recursive, out);
1327 }
1328 }
1329 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {}
1330 }
1331}