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 AccumulatorFold {
720 scope: Option<String>,
721 wrapper_fn: String,
722 loop_fn: String,
723 list_param: String,
724 acc_param: String,
725 step_fn: Option<String>,
728 step_op: Option<crate::ast::BinOp>,
731 finish_fn: Option<String>,
734 },
735}
736
737pub fn collect_inductable_sum_types(
746 entry_items: &[crate::ast::TopLevel],
747 dep_modules: &[crate::codegen::ModuleInfo],
748) -> HashSet<String> {
749 use crate::ast::{TopLevel, TypeDef};
750 let mut out = HashSet::new();
751 let mut consider = |td: &TypeDef| {
752 if let TypeDef::Sum { name, variants, .. } = td
753 && crate::codegen::common::is_recursive_sum(name, variants)
754 && !indirect_rec_variants(variants, name)
755 {
756 out.insert(name.clone());
757 }
758 };
759 for item in entry_items {
760 if let TopLevel::TypeDef(td) = item {
761 consider(td);
762 }
763 }
764 for m in dep_modules {
765 for td in &m.type_defs {
766 consider(td);
767 }
768 }
769 out
770}
771
772fn indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
776 for variant in variants {
777 for field in &variant.fields {
778 let f = field.trim();
779 if f == type_name {
780 continue;
781 }
782 let opens = f.matches('<').count();
783 if opens > 1 && f.contains(type_name) {
784 return true;
785 }
786 }
787 }
788 false
789}
790
791pub fn detect_module_patterns(
800 entry_items: &[crate::ast::TopLevel],
801 dep_modules: &[crate::codegen::ModuleInfo],
802) -> Vec<ModulePattern> {
803 use crate::ast::{Expr, Stmt, TopLevel, TypeDef};
804
805 let mut out = Vec::new();
806
807 struct CandidateRecord<'a> {
814 scope: Option<String>,
815 type_name: &'a str,
816 carrier_field: &'a str,
817 carrier_type: &'a str,
818 fns: Vec<&'a crate::ast::FnDef>,
819 }
820
821 let entry_fns: Vec<&crate::ast::FnDef> = entry_items
822 .iter()
823 .filter_map(|i| match i {
824 TopLevel::FnDef(fd) => Some(fd),
825 _ => None,
826 })
827 .collect();
828
829 let mut candidates: Vec<CandidateRecord<'_>> = Vec::new();
830 for td in entry_items.iter().filter_map(|i| match i {
831 TopLevel::TypeDef(td) => Some(td),
832 _ => None,
833 }) {
834 if let TypeDef::Product { name, fields, .. } = td
835 && fields.len() == 1
836 {
837 let (fname, ftype) = &fields[0];
838 candidates.push(CandidateRecord {
839 scope: None,
840 type_name: name.as_str(),
841 carrier_field: fname.as_str(),
842 carrier_type: ftype.as_str(),
843 fns: entry_fns.clone(),
844 });
845 }
846 }
847 for m in dep_modules {
848 let module_fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
849 for td in &m.type_defs {
850 if let TypeDef::Product { name, fields, .. } = td
851 && fields.len() == 1
852 {
853 let (fname, ftype) = &fields[0];
854 candidates.push(CandidateRecord {
855 scope: Some(m.prefix.clone()),
856 type_name: name.as_str(),
857 carrier_field: fname.as_str(),
858 carrier_type: ftype.as_str(),
859 fns: module_fns.clone(),
860 });
861 }
862 }
863 }
864
865 for candidate in &candidates {
872 let CandidateRecord {
873 scope,
874 type_name,
875 carrier_field,
876 carrier_type,
877 fns,
878 } = candidate;
879 for fd in fns {
880 if !fd.return_type.starts_with("Result<") {
881 continue;
882 }
883 if !fd.return_type[7..].starts_with(*type_name) {
884 continue;
885 }
886 if fd.params.len() != 1 {
887 continue;
888 }
889 let (param_name, _) = &fd.params[0];
890 let stmts = fd.body.stmts();
891 if stmts.len() != 1 {
892 continue;
893 }
894 let Stmt::Expr(body_expr) = &stmts[0] else {
895 continue;
896 };
897 let Expr::Match { subject, arms } = &body_expr.node else {
898 continue;
899 };
900 if !crate::codegen::common::is_refinement_bool_ok_err_match(
901 arms,
902 type_name,
903 carrier_field,
904 param_name,
905 ) {
906 continue;
907 }
908 out.push(ModulePattern::RefinementSmartConstructor {
909 scope: scope.clone(),
910 type_name: (*type_name).to_string(),
911 carrier_field: (*carrier_field).to_string(),
912 carrier_type: (*carrier_type).to_string(),
913 constructor_fn: fd.name.clone(),
914 param_name: param_name.clone(),
915 predicate: (**subject).clone(),
916 });
917 break;
918 }
919 }
920
921 detect_wrapper_over_recursion(None, &entry_fns, &mut out);
925 for m in dep_modules {
926 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
927 detect_wrapper_over_recursion(Some(m.prefix.clone()), &fns, &mut out);
928 }
929
930 detect_accumulator_fold(None, &entry_fns, &mut out);
933 for m in dep_modules {
934 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
935 detect_accumulator_fold(Some(m.prefix.clone()), &fns, &mut out);
936 }
937
938 detect_result_pipeline_chain(None, &entry_fns, &mut out);
940 for m in dep_modules {
941 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
942 detect_result_pipeline_chain(Some(m.prefix.clone()), &fns, &mut out);
943 }
944
945 detect_renderer_formatter(None, &entry_fns, &mut out);
947 for m in dep_modules {
948 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
949 detect_renderer_formatter(Some(m.prefix.clone()), &fns, &mut out);
950 }
951
952 detect_match_dispatcher_fold(None, &entry_fns, &mut out);
954 for m in dep_modules {
955 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
956 detect_match_dispatcher_fold(Some(m.prefix.clone()), &fns, &mut out);
957 }
958
959 out
960}
961
962fn detect_match_dispatcher_fold(
965 scope: Option<String>,
966 fns: &[&crate::ast::FnDef],
967 out: &mut Vec<ModulePattern>,
968) {
969 use crate::ast::{Expr, Pattern, Stmt};
970 for fd in fns {
971 let stmts = fd.body.stmts();
972 if stmts.len() != 1 {
973 continue;
974 }
975 let Stmt::Expr(body_expr) = &stmts[0] else {
976 continue;
977 };
978 let Expr::Match { subject, arms } = &body_expr.node else {
979 continue;
980 };
981 let subj_name = match &subject.node {
986 Expr::Ident(n) => n.as_str(),
987 Expr::Resolved { name, .. } => name.as_str(),
988 _ => continue,
989 };
990 if !fd.params.iter().any(|(n, _)| n == subj_name) {
991 continue;
992 }
993 let has_nil = arms.iter().any(|a| matches!(a.pattern, Pattern::EmptyList));
994 let has_cons = arms
995 .iter()
996 .any(|a| matches!(a.pattern, Pattern::Cons(_, _)));
997 if !(has_nil && has_cons) {
998 continue;
999 }
1000 if !body_calls_name(&fd.body, &fd.name) {
1001 continue;
1002 }
1003 out.push(ModulePattern::MatchDispatcherFold {
1004 scope: scope.clone(),
1005 fn_name: fd.name.clone(),
1006 list_param: subj_name.to_string(),
1007 });
1008 }
1009}
1010
1011fn detect_renderer_formatter(
1014 scope: Option<String>,
1015 fns: &[&crate::ast::FnDef],
1016 out: &mut Vec<ModulePattern>,
1017) {
1018 for fd in fns {
1019 if fd.return_type != "String" {
1020 continue;
1021 }
1022 if !fd.effects.is_empty() {
1023 continue;
1024 }
1025 if body_calls_name(&fd.body, &fd.name) {
1026 continue;
1027 }
1028 if !body_has_string_building(&fd.body) {
1029 continue;
1030 }
1031 out.push(ModulePattern::RendererFormatter {
1032 scope: scope.clone(),
1033 fn_name: fd.name.clone(),
1034 });
1035 }
1036}
1037
1038fn body_has_string_building(body: &crate::ast::FnBody) -> bool {
1046 for stmt in body.stmts() {
1047 let expr = match stmt {
1048 crate::ast::Stmt::Binding(_, _, e) => e,
1049 crate::ast::Stmt::Expr(e) => e,
1050 };
1051 if expr_has_string_building(expr) {
1052 return true;
1053 }
1054 }
1055 false
1056}
1057
1058fn expr_has_string_building(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
1059 use crate::ast::Expr;
1060 match &expr.node {
1061 Expr::InterpolatedStr(_) => true,
1062 Expr::BinOp(crate::ast::BinOp::Add, _, _) => true,
1063 Expr::FnCall(callee, args) => {
1064 expr_has_string_building(callee) || args.iter().any(expr_has_string_building)
1065 }
1066 Expr::TailCall(td) => td.args.iter().any(expr_has_string_building),
1067 Expr::Match { subject, arms } => {
1068 expr_has_string_building(subject)
1069 || arms.iter().any(|a| expr_has_string_building(&a.body))
1070 }
1071 Expr::BinOp(_, l, r) => expr_has_string_building(l) || expr_has_string_building(r),
1072 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_has_string_building(e),
1073 Expr::Constructor(_, Some(e)) => expr_has_string_building(e),
1074 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1075 xs.iter().any(expr_has_string_building)
1076 }
1077 Expr::MapLiteral(pairs) => pairs
1078 .iter()
1079 .any(|(k, v)| expr_has_string_building(k) || expr_has_string_building(v)),
1080 Expr::RecordCreate { fields, .. } => {
1081 fields.iter().any(|(_, e)| expr_has_string_building(e))
1082 }
1083 Expr::RecordUpdate { base, updates, .. } => {
1084 expr_has_string_building(base)
1085 || updates.iter().any(|(_, e)| expr_has_string_building(e))
1086 }
1087 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1088 false
1089 }
1090 }
1091}
1092
1093fn detect_result_pipeline_chain(
1099 scope: Option<String>,
1100 fns: &[&crate::ast::FnDef],
1101 out: &mut Vec<ModulePattern>,
1102) {
1103 use crate::ast::{Expr, Stmt};
1104 for fd in fns {
1105 if !fd.return_type.starts_with("Result<") {
1106 continue;
1107 }
1108 let stmts = fd.body.stmts();
1109 if stmts.len() < 2 {
1110 continue;
1111 }
1112 if !matches!(stmts.last(), Some(Stmt::Expr(_))) {
1113 continue;
1114 }
1115 let mut step_fns: Vec<String> = Vec::new();
1116 for stmt in stmts {
1117 if let Stmt::Binding(_, _, value) = stmt
1118 && let Expr::ErrorProp(inner) = &value.node
1119 && let Expr::FnCall(callee, _) = &inner.node
1120 && let Expr::Ident(name) = &callee.node
1121 {
1122 step_fns.push(name.clone());
1123 }
1124 }
1125 if step_fns.len() < 2 {
1126 continue;
1127 }
1128 let step_count = step_fns.len();
1129 out.push(ModulePattern::ResultPipelineChain {
1130 scope: scope.clone(),
1131 fn_name: fd.name.clone(),
1132 step_count,
1133 step_fns,
1134 });
1135 }
1136}
1137
1138fn detect_wrapper_over_recursion(
1144 scope: Option<String>,
1145 fns: &[&crate::ast::FnDef],
1146 out: &mut Vec<ModulePattern>,
1147) {
1148 if fns.is_empty() {
1149 return;
1150 }
1151
1152 let mut recursive: HashSet<String> = HashSet::new();
1153 for fd in fns {
1154 if body_calls_name(&fd.body, &fd.name) {
1155 recursive.insert(fd.name.clone());
1156 }
1157 }
1158 if recursive.is_empty() {
1159 return;
1160 }
1161
1162 for fd in fns {
1163 if recursive.contains(&fd.name) {
1164 continue;
1165 }
1166 if fd.params.is_empty() {
1167 continue;
1168 }
1169 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1170 let mut hits: Vec<String> = Vec::new();
1171 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1172 hits.sort();
1173 hits.dedup();
1174 if hits.len() != 1 {
1175 continue;
1176 }
1177 let inner = hits.into_iter().next().unwrap();
1178 out.push(ModulePattern::WrapperOverRecursion {
1179 wrapper_scope: scope.clone(),
1180 wrapper_fn: fd.name.clone(),
1181 inner_scope: scope.clone(),
1182 inner_fn: inner,
1183 });
1184 }
1185}
1186
1187fn call_target(
1190 e: &crate::ast::Spanned<crate::ast::Expr>,
1191) -> Option<(&str, &[crate::ast::Spanned<crate::ast::Expr>])> {
1192 use crate::ast::Expr;
1193 match &e.node {
1194 Expr::FnCall(callee, args) => match &callee.node {
1195 Expr::Ident(n) => Some((n.as_str(), args.as_slice())),
1196 _ => None,
1197 },
1198 Expr::TailCall(td) => Some((td.target.as_str(), td.args.as_slice())),
1199 _ => None,
1200 }
1201}
1202
1203fn plain_ident(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<&str> {
1205 match &e.node {
1206 crate::ast::Expr::Ident(n) => Some(n.as_str()),
1207 crate::ast::Expr::Resolved { name, .. } => Some(name.as_str()),
1208 _ => None,
1209 }
1210}
1211
1212fn detect_accumulator_fold(
1218 scope: Option<String>,
1219 fns: &[&crate::ast::FnDef],
1220 out: &mut Vec<ModulePattern>,
1221) {
1222 use crate::ast::{Expr, Pattern, Stmt};
1223
1224 let mut recursive: HashSet<String> = HashSet::new();
1225 for fd in fns {
1226 if body_calls_name(&fd.body, &fd.name) {
1227 recursive.insert(fd.name.clone());
1228 }
1229 }
1230 if recursive.is_empty() {
1231 return;
1232 }
1233
1234 for fd in fns {
1235 if recursive.contains(&fd.name) || fd.params.is_empty() {
1236 continue;
1237 }
1238 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1239 let mut hits: Vec<String> = Vec::new();
1240 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1241 hits.sort();
1242 hits.dedup();
1243 if hits.len() != 1 {
1244 continue;
1245 }
1246 let loop_fn = hits.into_iter().next().unwrap();
1247
1248 let Some(lf) = fns.iter().find(|f| f.name == loop_fn) else {
1250 continue;
1251 };
1252 if lf.params.len() != 2 {
1253 continue;
1254 }
1255 let list_param = lf.params[0].0.clone();
1256 let acc_param = lf.params[1].0.clone();
1257 let Some(Stmt::Expr(body)) = lf.body.stmts().last() else {
1258 continue;
1259 };
1260 let Expr::Match { subject, arms } = &body.node else {
1261 continue;
1262 };
1263 if plain_ident(subject) != Some(list_param.as_str()) || arms.len() != 2 {
1264 continue;
1265 }
1266
1267 let mut finish_fn: Option<Option<String>> = None; let mut step_fn: Option<String> = None;
1269 let mut step_op: Option<crate::ast::BinOp> = None;
1270 let mut step_seen = false;
1271 let mut ok = true;
1272 for arm in arms {
1273 match &arm.pattern {
1274 Pattern::EmptyList => {
1275 if let Some((name, fargs)) = call_target(&arm.body) {
1277 if fargs.len() == 1 && plain_ident(&fargs[0]) == Some(acc_param.as_str()) {
1278 finish_fn = Some(Some(name.to_string()));
1279 } else {
1280 ok = false;
1281 }
1282 } else if plain_ident(&arm.body) == Some(acc_param.as_str()) {
1283 finish_fn = Some(None);
1284 } else {
1285 ok = false;
1286 }
1287 }
1288 Pattern::Cons(h, t) => {
1289 let Some((callee, cargs)) = call_target(&arm.body) else {
1291 ok = false;
1292 continue;
1293 };
1294 if callee != loop_fn
1295 || cargs.len() != 2
1296 || plain_ident(&cargs[0]) != Some(t.as_str())
1297 {
1298 ok = false;
1299 continue;
1300 }
1301 match &cargs[1].node {
1302 Expr::FnCall(sc, sargs) => {
1303 if let Expr::Ident(sname) = &sc.node
1304 && sargs.len() == 2
1305 && plain_ident(&sargs[0]) == Some(acc_param.as_str())
1306 && plain_ident(&sargs[1]) == Some(h.as_str())
1307 {
1308 step_fn = Some(sname.clone());
1309 step_seen = true;
1310 } else {
1311 ok = false;
1312 }
1313 }
1314 Expr::BinOp(op, l, r) => {
1315 let ln = plain_ident(l);
1316 let rn = plain_ident(r);
1317 let acc_h = ln == Some(acc_param.as_str()) && rn == Some(h.as_str());
1318 let h_acc = ln == Some(h.as_str()) && rn == Some(acc_param.as_str());
1319 if acc_h || h_acc {
1320 step_op = Some(*op);
1321 step_seen = true;
1322 } else {
1323 ok = false;
1324 }
1325 }
1326 _ => ok = false,
1327 }
1328 }
1329 _ => ok = false,
1330 }
1331 }
1332
1333 let (Some(finish_fn), true, true) = (finish_fn, step_seen, ok) else {
1334 continue;
1335 };
1336 out.push(ModulePattern::AccumulatorFold {
1337 scope: scope.clone(),
1338 wrapper_fn: fd.name.clone(),
1339 loop_fn,
1340 list_param,
1341 acc_param,
1342 step_fn,
1343 step_op,
1344 finish_fn,
1345 });
1346 }
1347}
1348
1349fn body_calls_name(body: &crate::ast::FnBody, name: &str) -> bool {
1353 for stmt in body.stmts() {
1354 let expr = match stmt {
1355 crate::ast::Stmt::Binding(_, _, e) => e,
1356 crate::ast::Stmt::Expr(e) => e,
1357 };
1358 if expr_calls_name(expr, name) {
1359 return true;
1360 }
1361 }
1362 false
1363}
1364
1365fn expr_calls_name(expr: &crate::ast::Spanned<crate::ast::Expr>, name: &str) -> bool {
1366 use crate::ast::Expr;
1367 match &expr.node {
1368 Expr::FnCall(callee, args) => {
1369 if let Expr::Ident(n) = &callee.node
1370 && n == name
1371 {
1372 return true;
1373 }
1374 if expr_calls_name(callee, name) {
1375 return true;
1376 }
1377 args.iter().any(|a| expr_calls_name(a, name))
1378 }
1379 Expr::TailCall(td) => td.target == name || td.args.iter().any(|a| expr_calls_name(a, name)),
1380 Expr::Match { subject, arms } => {
1381 if expr_calls_name(subject, name) {
1382 return true;
1383 }
1384 arms.iter().any(|a| expr_calls_name(&a.body, name))
1385 }
1386 Expr::BinOp(_, l, r) => expr_calls_name(l, name) || expr_calls_name(r, name),
1387 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_calls_name(e, name),
1388 Expr::Constructor(_, Some(e)) => expr_calls_name(e, name),
1389 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1390 xs.iter().any(|x| expr_calls_name(x, name))
1391 }
1392 Expr::MapLiteral(pairs) => pairs
1393 .iter()
1394 .any(|(k, v)| expr_calls_name(k, name) || expr_calls_name(v, name)),
1395 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_name(e, name)),
1396 Expr::RecordUpdate { base, updates, .. } => {
1397 expr_calls_name(base, name) || updates.iter().any(|(_, e)| expr_calls_name(e, name))
1398 }
1399 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1400 crate::ast::StrPart::Parsed(e) => expr_calls_name(e, name),
1401 crate::ast::StrPart::Literal(_) => false,
1402 }),
1403 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1404 false
1405 }
1406 }
1407}
1408
1409fn collect_qualifying_inner_calls(
1415 body: &crate::ast::FnBody,
1416 outer_params: &[&str],
1417 recursive: &HashSet<String>,
1418 out: &mut Vec<String>,
1419) {
1420 for stmt in body.stmts() {
1421 let expr = match stmt {
1422 crate::ast::Stmt::Binding(_, _, e) => e,
1423 crate::ast::Stmt::Expr(e) => e,
1424 };
1425 collect_qualifying_in_expr(expr, outer_params, recursive, out);
1426 }
1427}
1428
1429fn collect_qualifying_in_expr(
1430 expr: &crate::ast::Spanned<crate::ast::Expr>,
1431 outer_params: &[&str],
1432 recursive: &HashSet<String>,
1433 out: &mut Vec<String>,
1434) {
1435 use crate::ast::Expr;
1436 let try_qualify = |callee: &str, args: &[crate::ast::Spanned<Expr>], out: &mut Vec<String>| {
1437 if !recursive.contains(callee) {
1438 return;
1439 }
1440 if args.len() <= outer_params.len() {
1441 return;
1442 }
1443 let mut arg_idents: HashSet<&str> = HashSet::new();
1447 for a in args {
1448 match &a.node {
1449 Expr::Ident(n) => {
1450 arg_idents.insert(n.as_str());
1451 }
1452 Expr::Resolved { name, .. } => {
1453 arg_idents.insert(name.as_str());
1454 }
1455 _ => {}
1456 }
1457 }
1458 if outer_params.iter().all(|p| arg_idents.contains(*p)) {
1459 out.push(callee.to_string());
1460 }
1461 };
1462 if let Expr::FnCall(callee, args) = &expr.node
1463 && let Expr::Ident(name) = &callee.node
1464 {
1465 try_qualify(name, args, out);
1466 }
1467 if let Expr::TailCall(td) = &expr.node {
1473 try_qualify(&td.target, &td.args, out);
1474 }
1475 match &expr.node {
1476 Expr::FnCall(callee, args) => {
1477 collect_qualifying_in_expr(callee, outer_params, recursive, out);
1478 for a in args {
1479 collect_qualifying_in_expr(a, outer_params, recursive, out);
1480 }
1481 }
1482 Expr::Match { subject, arms } => {
1483 collect_qualifying_in_expr(subject, outer_params, recursive, out);
1484 for a in arms {
1485 collect_qualifying_in_expr(&a.body, outer_params, recursive, out);
1486 }
1487 }
1488 Expr::BinOp(_, l, r) => {
1489 collect_qualifying_in_expr(l, outer_params, recursive, out);
1490 collect_qualifying_in_expr(r, outer_params, recursive, out);
1491 }
1492 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => {
1493 collect_qualifying_in_expr(e, outer_params, recursive, out);
1494 }
1495 Expr::Constructor(_, Some(e)) => {
1496 collect_qualifying_in_expr(e, outer_params, recursive, out);
1497 }
1498 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1499 for x in xs {
1500 collect_qualifying_in_expr(x, outer_params, recursive, out);
1501 }
1502 }
1503 Expr::MapLiteral(pairs) => {
1504 for (k, v) in pairs {
1505 collect_qualifying_in_expr(k, outer_params, recursive, out);
1506 collect_qualifying_in_expr(v, outer_params, recursive, out);
1507 }
1508 }
1509 Expr::RecordCreate { fields, .. } => {
1510 for (_, e) in fields {
1511 collect_qualifying_in_expr(e, outer_params, recursive, out);
1512 }
1513 }
1514 Expr::RecordUpdate { base, updates, .. } => {
1515 collect_qualifying_in_expr(base, outer_params, recursive, out);
1516 for (_, e) in updates {
1517 collect_qualifying_in_expr(e, outer_params, recursive, out);
1518 }
1519 }
1520 Expr::InterpolatedStr(parts) => {
1521 for p in parts {
1522 if let crate::ast::StrPart::Parsed(e) = p {
1523 collect_qualifying_in_expr(e, outer_params, recursive, out);
1524 }
1525 }
1526 }
1527 Expr::TailCall(td) => {
1528 for a in &td.args {
1529 collect_qualifying_in_expr(a, outer_params, recursive, out);
1530 }
1531 }
1532 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {}
1533 }
1534}