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 driver_type: Option<String>,
741 step_value_first: bool,
745 },
746}
747
748pub fn collect_inductable_sum_types(
757 entry_items: &[crate::ast::TopLevel],
758 dep_modules: &[crate::codegen::ModuleInfo],
759) -> HashSet<String> {
760 use crate::ast::{TopLevel, TypeDef};
761 let mut out = HashSet::new();
762 let mut consider = |td: &TypeDef| {
763 if let TypeDef::Sum { name, variants, .. } = td
764 && crate::codegen::common::is_recursive_sum(name, variants)
765 && !indirect_rec_variants(variants, name)
766 {
767 out.insert(name.clone());
768 }
769 };
770 for item in entry_items {
771 if let TopLevel::TypeDef(td) = item {
772 consider(td);
773 }
774 }
775 for m in dep_modules {
776 for td in &m.type_defs {
777 consider(td);
778 }
779 }
780 out
781}
782
783fn indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
787 for variant in variants {
788 for field in &variant.fields {
789 let f = field.trim();
790 if f == type_name {
791 continue;
792 }
793 let opens = f.matches('<').count();
794 if opens > 1 && f.contains(type_name) {
795 return true;
796 }
797 }
798 }
799 false
800}
801
802pub fn detect_module_patterns(
811 entry_items: &[crate::ast::TopLevel],
812 dep_modules: &[crate::codegen::ModuleInfo],
813) -> Vec<ModulePattern> {
814 use crate::ast::{Expr, Stmt, TopLevel, TypeDef};
815
816 let mut out = Vec::new();
817
818 struct CandidateRecord<'a> {
825 scope: Option<String>,
826 type_name: &'a str,
827 carrier_field: &'a str,
828 carrier_type: &'a str,
829 fns: Vec<&'a crate::ast::FnDef>,
830 }
831
832 let entry_fns: Vec<&crate::ast::FnDef> = entry_items
833 .iter()
834 .filter_map(|i| match i {
835 TopLevel::FnDef(fd) => Some(fd),
836 _ => None,
837 })
838 .collect();
839
840 let mut candidates: Vec<CandidateRecord<'_>> = Vec::new();
841 for td in entry_items.iter().filter_map(|i| match i {
842 TopLevel::TypeDef(td) => Some(td),
843 _ => None,
844 }) {
845 if let TypeDef::Product { name, fields, .. } = td
846 && fields.len() == 1
847 {
848 let (fname, ftype) = &fields[0];
849 candidates.push(CandidateRecord {
850 scope: None,
851 type_name: name.as_str(),
852 carrier_field: fname.as_str(),
853 carrier_type: ftype.as_str(),
854 fns: entry_fns.clone(),
855 });
856 }
857 }
858 for m in dep_modules {
859 let module_fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
860 for td in &m.type_defs {
861 if let TypeDef::Product { name, fields, .. } = td
862 && fields.len() == 1
863 {
864 let (fname, ftype) = &fields[0];
865 candidates.push(CandidateRecord {
866 scope: Some(m.prefix.clone()),
867 type_name: name.as_str(),
868 carrier_field: fname.as_str(),
869 carrier_type: ftype.as_str(),
870 fns: module_fns.clone(),
871 });
872 }
873 }
874 }
875
876 for candidate in &candidates {
883 let CandidateRecord {
884 scope,
885 type_name,
886 carrier_field,
887 carrier_type,
888 fns,
889 } = candidate;
890 for fd in fns {
891 if !fd.return_type.starts_with("Result<") {
892 continue;
893 }
894 if !fd.return_type[7..].starts_with(*type_name) {
895 continue;
896 }
897 if fd.params.len() != 1 {
898 continue;
899 }
900 let (param_name, _) = &fd.params[0];
901 let stmts = fd.body.stmts();
902 if stmts.len() != 1 {
903 continue;
904 }
905 let Stmt::Expr(body_expr) = &stmts[0] else {
906 continue;
907 };
908 let Expr::Match { subject, arms } = &body_expr.node else {
909 continue;
910 };
911 if !crate::codegen::common::is_refinement_bool_ok_err_match(
912 arms,
913 type_name,
914 carrier_field,
915 param_name,
916 ) {
917 continue;
918 }
919 out.push(ModulePattern::RefinementSmartConstructor {
920 scope: scope.clone(),
921 type_name: (*type_name).to_string(),
922 carrier_field: (*carrier_field).to_string(),
923 carrier_type: (*carrier_type).to_string(),
924 constructor_fn: fd.name.clone(),
925 param_name: param_name.clone(),
926 predicate: (**subject).clone(),
927 });
928 break;
929 }
930 }
931
932 detect_wrapper_over_recursion(None, &entry_fns, &mut out);
936 for m in dep_modules {
937 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
938 detect_wrapper_over_recursion(Some(m.prefix.clone()), &fns, &mut out);
939 }
940
941 detect_accumulator_fold(None, &entry_fns, &mut out);
944 for m in dep_modules {
945 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
946 detect_accumulator_fold(Some(m.prefix.clone()), &fns, &mut out);
947 }
948
949 detect_result_pipeline_chain(None, &entry_fns, &mut out);
951 for m in dep_modules {
952 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
953 detect_result_pipeline_chain(Some(m.prefix.clone()), &fns, &mut out);
954 }
955
956 detect_renderer_formatter(None, &entry_fns, &mut out);
958 for m in dep_modules {
959 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
960 detect_renderer_formatter(Some(m.prefix.clone()), &fns, &mut out);
961 }
962
963 detect_match_dispatcher_fold(None, &entry_fns, &mut out);
965 for m in dep_modules {
966 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
967 detect_match_dispatcher_fold(Some(m.prefix.clone()), &fns, &mut out);
968 }
969
970 out
971}
972
973fn detect_match_dispatcher_fold(
976 scope: Option<String>,
977 fns: &[&crate::ast::FnDef],
978 out: &mut Vec<ModulePattern>,
979) {
980 use crate::ast::{Expr, Pattern, Stmt};
981 for fd in fns {
982 let stmts = fd.body.stmts();
983 if stmts.len() != 1 {
984 continue;
985 }
986 let Stmt::Expr(body_expr) = &stmts[0] else {
987 continue;
988 };
989 let Expr::Match { subject, arms } = &body_expr.node else {
990 continue;
991 };
992 let subj_name = match &subject.node {
997 Expr::Ident(n) => n.as_str(),
998 Expr::Resolved { name, .. } => name.as_str(),
999 _ => continue,
1000 };
1001 if !fd.params.iter().any(|(n, _)| n == subj_name) {
1002 continue;
1003 }
1004 let has_nil = arms.iter().any(|a| matches!(a.pattern, Pattern::EmptyList));
1005 let has_cons = arms
1006 .iter()
1007 .any(|a| matches!(a.pattern, Pattern::Cons(_, _)));
1008 if !(has_nil && has_cons) {
1009 continue;
1010 }
1011 if !body_calls_name(&fd.body, &fd.name) {
1012 continue;
1013 }
1014 out.push(ModulePattern::MatchDispatcherFold {
1015 scope: scope.clone(),
1016 fn_name: fd.name.clone(),
1017 list_param: subj_name.to_string(),
1018 });
1019 }
1020}
1021
1022fn detect_renderer_formatter(
1025 scope: Option<String>,
1026 fns: &[&crate::ast::FnDef],
1027 out: &mut Vec<ModulePattern>,
1028) {
1029 for fd in fns {
1030 if fd.return_type != "String" {
1031 continue;
1032 }
1033 if !fd.effects.is_empty() {
1034 continue;
1035 }
1036 if body_calls_name(&fd.body, &fd.name) {
1037 continue;
1038 }
1039 if !body_has_string_building(&fd.body) {
1040 continue;
1041 }
1042 out.push(ModulePattern::RendererFormatter {
1043 scope: scope.clone(),
1044 fn_name: fd.name.clone(),
1045 });
1046 }
1047}
1048
1049fn body_has_string_building(body: &crate::ast::FnBody) -> bool {
1057 for stmt in body.stmts() {
1058 let expr = match stmt {
1059 crate::ast::Stmt::Binding(_, _, e) => e,
1060 crate::ast::Stmt::Expr(e) => e,
1061 };
1062 if expr_has_string_building(expr) {
1063 return true;
1064 }
1065 }
1066 false
1067}
1068
1069fn expr_has_string_building(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
1070 use crate::ast::Expr;
1071 match &expr.node {
1072 Expr::InterpolatedStr(_) => true,
1073 Expr::BinOp(crate::ast::BinOp::Add, _, _) => true,
1074 Expr::FnCall(callee, args) => {
1075 expr_has_string_building(callee) || args.iter().any(expr_has_string_building)
1076 }
1077 Expr::TailCall(td) => td.args.iter().any(expr_has_string_building),
1078 Expr::Match { subject, arms } => {
1079 expr_has_string_building(subject)
1080 || arms.iter().any(|a| expr_has_string_building(&a.body))
1081 }
1082 Expr::BinOp(_, l, r) => expr_has_string_building(l) || expr_has_string_building(r),
1083 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_has_string_building(e),
1084 Expr::Constructor(_, Some(e)) => expr_has_string_building(e),
1085 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1086 xs.iter().any(expr_has_string_building)
1087 }
1088 Expr::MapLiteral(pairs) => pairs
1089 .iter()
1090 .any(|(k, v)| expr_has_string_building(k) || expr_has_string_building(v)),
1091 Expr::RecordCreate { fields, .. } => {
1092 fields.iter().any(|(_, e)| expr_has_string_building(e))
1093 }
1094 Expr::RecordUpdate { base, updates, .. } => {
1095 expr_has_string_building(base)
1096 || updates.iter().any(|(_, e)| expr_has_string_building(e))
1097 }
1098 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1099 false
1100 }
1101 }
1102}
1103
1104fn detect_result_pipeline_chain(
1110 scope: Option<String>,
1111 fns: &[&crate::ast::FnDef],
1112 out: &mut Vec<ModulePattern>,
1113) {
1114 use crate::ast::{Expr, Stmt};
1115 for fd in fns {
1116 if !fd.return_type.starts_with("Result<") {
1117 continue;
1118 }
1119 let stmts = fd.body.stmts();
1120 if stmts.len() < 2 {
1121 continue;
1122 }
1123 if !matches!(stmts.last(), Some(Stmt::Expr(_))) {
1124 continue;
1125 }
1126 let mut step_fns: Vec<String> = Vec::new();
1127 for stmt in stmts {
1128 if let Stmt::Binding(_, _, value) = stmt
1129 && let Expr::ErrorProp(inner) = &value.node
1130 && let Expr::FnCall(callee, _) = &inner.node
1131 && let Expr::Ident(name) = &callee.node
1132 {
1133 step_fns.push(name.clone());
1134 }
1135 }
1136 if step_fns.len() < 2 {
1137 continue;
1138 }
1139 let step_count = step_fns.len();
1140 out.push(ModulePattern::ResultPipelineChain {
1141 scope: scope.clone(),
1142 fn_name: fd.name.clone(),
1143 step_count,
1144 step_fns,
1145 });
1146 }
1147}
1148
1149fn detect_wrapper_over_recursion(
1155 scope: Option<String>,
1156 fns: &[&crate::ast::FnDef],
1157 out: &mut Vec<ModulePattern>,
1158) {
1159 if fns.is_empty() {
1160 return;
1161 }
1162
1163 let mut recursive: HashSet<String> = HashSet::new();
1164 for fd in fns {
1165 if body_calls_name(&fd.body, &fd.name) {
1166 recursive.insert(fd.name.clone());
1167 }
1168 }
1169 if recursive.is_empty() {
1170 return;
1171 }
1172
1173 for fd in fns {
1174 if recursive.contains(&fd.name) {
1175 continue;
1176 }
1177 if fd.params.is_empty() {
1178 continue;
1179 }
1180 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1181 let mut hits: Vec<String> = Vec::new();
1182 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1183 hits.sort();
1184 hits.dedup();
1185 if hits.len() != 1 {
1186 continue;
1187 }
1188 let inner = hits.into_iter().next().unwrap();
1189 out.push(ModulePattern::WrapperOverRecursion {
1190 wrapper_scope: scope.clone(),
1191 wrapper_fn: fd.name.clone(),
1192 inner_scope: scope.clone(),
1193 inner_fn: inner,
1194 });
1195 }
1196}
1197
1198fn call_target(
1201 e: &crate::ast::Spanned<crate::ast::Expr>,
1202) -> Option<(&str, &[crate::ast::Spanned<crate::ast::Expr>])> {
1203 use crate::ast::Expr;
1204 match &e.node {
1205 Expr::FnCall(callee, args) => match &callee.node {
1206 Expr::Ident(n) => Some((n.as_str(), args.as_slice())),
1207 _ => None,
1208 },
1209 Expr::TailCall(td) => Some((td.target.as_str(), td.args.as_slice())),
1210 _ => None,
1211 }
1212}
1213
1214fn plain_ident(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<&str> {
1216 match &e.node {
1217 crate::ast::Expr::Ident(n) => Some(n.as_str()),
1218 crate::ast::Expr::Resolved { name, .. } => Some(name.as_str()),
1219 _ => None,
1220 }
1221}
1222
1223fn detect_accumulator_fold(
1229 scope: Option<String>,
1230 fns: &[&crate::ast::FnDef],
1231 out: &mut Vec<ModulePattern>,
1232) {
1233 use crate::ast::{Expr, Pattern, Stmt};
1234
1235 let mut recursive: HashSet<String> = HashSet::new();
1236 for fd in fns {
1237 if body_calls_name(&fd.body, &fd.name) {
1238 recursive.insert(fd.name.clone());
1239 }
1240 }
1241 if recursive.is_empty() {
1242 return;
1243 }
1244
1245 for fd in fns {
1246 if recursive.contains(&fd.name) || fd.params.is_empty() {
1247 continue;
1248 }
1249 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1250 let mut hits: Vec<String> = Vec::new();
1251 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1252 hits.sort();
1253 hits.dedup();
1254 if hits.len() != 1 {
1255 continue;
1256 }
1257 let loop_fn = hits.into_iter().next().unwrap();
1258
1259 let Some(lf) = fns.iter().find(|f| f.name == loop_fn) else {
1261 continue;
1262 };
1263 if lf.params.len() != 2 {
1264 continue;
1265 }
1266 let list_param = lf.params[0].0.clone();
1267 let acc_param = lf.params[1].0.clone();
1268 let Some(Stmt::Expr(body)) = lf.body.stmts().last() else {
1269 continue;
1270 };
1271 let Expr::Match { subject, arms } = &body.node else {
1272 continue;
1273 };
1274 if plain_ident(subject) != Some(list_param.as_str()) || arms.len() != 2 {
1275 continue;
1276 }
1277
1278 let list_shaped = arms
1282 .iter()
1283 .any(|a| matches!(a.pattern, Pattern::EmptyList | Pattern::Cons(_, _)));
1284 if !list_shaped {
1285 let mut base_seen = false;
1290 let mut combine_fn: Option<String> = None;
1291 let mut value_first = false;
1292 let mut step_seen = false;
1293 let mut ok = true;
1294 for arm in arms {
1295 let Pattern::Constructor(_, binders) = &arm.pattern else {
1296 ok = false;
1297 continue;
1298 };
1299 match binders.len() {
1300 0 => {
1301 if plain_ident(&arm.body) == Some(acc_param.as_str()) {
1302 base_seen = true;
1303 } else {
1304 ok = false;
1305 }
1306 }
1307 1 => {
1308 let bind = &binders[0];
1309 let Some((callee, cargs)) = call_target(&arm.body) else {
1310 ok = false;
1311 continue;
1312 };
1313 if callee != loop_fn
1314 || cargs.len() != 2
1315 || plain_ident(&cargs[0]) != Some(bind.as_str())
1316 {
1317 ok = false;
1318 continue;
1319 }
1320 let Expr::FnCall(sc, sargs) = &cargs[1].node else {
1323 ok = false;
1324 continue;
1325 };
1326 let Expr::Ident(sname) = &sc.node else {
1327 ok = false;
1328 continue;
1329 };
1330 if sargs.len() != 2 {
1331 ok = false;
1332 continue;
1333 }
1334 let a0 = plain_ident(&sargs[0]);
1335 let a1 = plain_ident(&sargs[1]);
1336 if a0 == Some(list_param.as_str()) && a1 == Some(acc_param.as_str()) {
1337 value_first = true;
1338 } else if a0 == Some(acc_param.as_str()) && a1 == Some(list_param.as_str())
1339 {
1340 value_first = false;
1341 } else {
1342 ok = false;
1343 continue;
1344 }
1345 combine_fn = Some(sname.clone());
1346 step_seen = true;
1347 }
1348 _ => ok = false,
1349 }
1350 }
1351 if base_seen && step_seen && ok && combine_fn.is_some() {
1352 out.push(ModulePattern::AccumulatorFold {
1353 scope: scope.clone(),
1354 wrapper_fn: fd.name.clone(),
1355 loop_fn,
1356 driver_type: Some(lf.params[0].1.clone()),
1357 list_param,
1358 acc_param,
1359 step_fn: combine_fn,
1360 step_op: None,
1361 finish_fn: None,
1362 step_value_first: value_first,
1363 });
1364 }
1365 continue;
1366 }
1367
1368 let mut finish_fn: Option<Option<String>> = None; let mut step_fn: Option<String> = None;
1370 let mut step_op: Option<crate::ast::BinOp> = None;
1371 let mut step_seen = false;
1372 let mut ok = true;
1373 for arm in arms {
1374 match &arm.pattern {
1375 Pattern::EmptyList => {
1376 if let Some((name, fargs)) = call_target(&arm.body) {
1378 if fargs.len() == 1 && plain_ident(&fargs[0]) == Some(acc_param.as_str()) {
1379 finish_fn = Some(Some(name.to_string()));
1380 } else {
1381 ok = false;
1382 }
1383 } else if plain_ident(&arm.body) == Some(acc_param.as_str()) {
1384 finish_fn = Some(None);
1385 } else {
1386 ok = false;
1387 }
1388 }
1389 Pattern::Cons(h, t) => {
1390 let Some((callee, cargs)) = call_target(&arm.body) else {
1392 ok = false;
1393 continue;
1394 };
1395 if callee != loop_fn
1396 || cargs.len() != 2
1397 || plain_ident(&cargs[0]) != Some(t.as_str())
1398 {
1399 ok = false;
1400 continue;
1401 }
1402 match &cargs[1].node {
1403 Expr::FnCall(sc, sargs) => {
1404 if let Expr::Ident(sname) = &sc.node
1405 && sargs.len() == 2
1406 && plain_ident(&sargs[0]) == Some(acc_param.as_str())
1407 && plain_ident(&sargs[1]) == Some(h.as_str())
1408 {
1409 step_fn = Some(sname.clone());
1410 step_seen = true;
1411 } else {
1412 ok = false;
1413 }
1414 }
1415 Expr::BinOp(op, l, r) => {
1416 let ln = plain_ident(l);
1417 let rn = plain_ident(r);
1418 let acc_h = ln == Some(acc_param.as_str()) && rn == Some(h.as_str());
1419 let h_acc = ln == Some(h.as_str()) && rn == Some(acc_param.as_str());
1420 if acc_h || h_acc {
1421 step_op = Some(*op);
1422 step_seen = true;
1423 } else {
1424 ok = false;
1425 }
1426 }
1427 _ => ok = false,
1428 }
1429 }
1430 _ => ok = false,
1431 }
1432 }
1433
1434 let (Some(finish_fn), true, true) = (finish_fn, step_seen, ok) else {
1435 continue;
1436 };
1437 out.push(ModulePattern::AccumulatorFold {
1438 scope: scope.clone(),
1439 wrapper_fn: fd.name.clone(),
1440 loop_fn,
1441 list_param,
1442 acc_param,
1443 step_fn,
1444 step_op,
1445 finish_fn,
1446 driver_type: None,
1447 step_value_first: false,
1448 });
1449 }
1450}
1451
1452fn body_calls_name(body: &crate::ast::FnBody, name: &str) -> bool {
1456 for stmt in body.stmts() {
1457 let expr = match stmt {
1458 crate::ast::Stmt::Binding(_, _, e) => e,
1459 crate::ast::Stmt::Expr(e) => e,
1460 };
1461 if expr_calls_name(expr, name) {
1462 return true;
1463 }
1464 }
1465 false
1466}
1467
1468fn expr_calls_name(expr: &crate::ast::Spanned<crate::ast::Expr>, name: &str) -> bool {
1469 use crate::ast::Expr;
1470 match &expr.node {
1471 Expr::FnCall(callee, args) => {
1472 if let Expr::Ident(n) = &callee.node
1473 && n == name
1474 {
1475 return true;
1476 }
1477 if expr_calls_name(callee, name) {
1478 return true;
1479 }
1480 args.iter().any(|a| expr_calls_name(a, name))
1481 }
1482 Expr::TailCall(td) => td.target == name || td.args.iter().any(|a| expr_calls_name(a, name)),
1483 Expr::Match { subject, arms } => {
1484 if expr_calls_name(subject, name) {
1485 return true;
1486 }
1487 arms.iter().any(|a| expr_calls_name(&a.body, name))
1488 }
1489 Expr::BinOp(_, l, r) => expr_calls_name(l, name) || expr_calls_name(r, name),
1490 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_calls_name(e, name),
1491 Expr::Constructor(_, Some(e)) => expr_calls_name(e, name),
1492 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1493 xs.iter().any(|x| expr_calls_name(x, name))
1494 }
1495 Expr::MapLiteral(pairs) => pairs
1496 .iter()
1497 .any(|(k, v)| expr_calls_name(k, name) || expr_calls_name(v, name)),
1498 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_name(e, name)),
1499 Expr::RecordUpdate { base, updates, .. } => {
1500 expr_calls_name(base, name) || updates.iter().any(|(_, e)| expr_calls_name(e, name))
1501 }
1502 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1503 crate::ast::StrPart::Parsed(e) => expr_calls_name(e, name),
1504 crate::ast::StrPart::Literal(_) => false,
1505 }),
1506 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1507 false
1508 }
1509 }
1510}
1511
1512fn collect_qualifying_inner_calls(
1518 body: &crate::ast::FnBody,
1519 outer_params: &[&str],
1520 recursive: &HashSet<String>,
1521 out: &mut Vec<String>,
1522) {
1523 for stmt in body.stmts() {
1524 let expr = match stmt {
1525 crate::ast::Stmt::Binding(_, _, e) => e,
1526 crate::ast::Stmt::Expr(e) => e,
1527 };
1528 collect_qualifying_in_expr(expr, outer_params, recursive, out);
1529 }
1530}
1531
1532fn collect_qualifying_in_expr(
1533 expr: &crate::ast::Spanned<crate::ast::Expr>,
1534 outer_params: &[&str],
1535 recursive: &HashSet<String>,
1536 out: &mut Vec<String>,
1537) {
1538 use crate::ast::Expr;
1539 let try_qualify = |callee: &str, args: &[crate::ast::Spanned<Expr>], out: &mut Vec<String>| {
1540 if !recursive.contains(callee) {
1541 return;
1542 }
1543 if args.len() <= outer_params.len() {
1544 return;
1545 }
1546 let mut arg_idents: HashSet<&str> = HashSet::new();
1550 for a in args {
1551 match &a.node {
1552 Expr::Ident(n) => {
1553 arg_idents.insert(n.as_str());
1554 }
1555 Expr::Resolved { name, .. } => {
1556 arg_idents.insert(name.as_str());
1557 }
1558 _ => {}
1559 }
1560 }
1561 if outer_params.iter().all(|p| arg_idents.contains(*p)) {
1562 out.push(callee.to_string());
1563 }
1564 };
1565 if let Expr::FnCall(callee, args) = &expr.node
1566 && let Expr::Ident(name) = &callee.node
1567 {
1568 try_qualify(name, args, out);
1569 }
1570 if let Expr::TailCall(td) = &expr.node {
1576 try_qualify(&td.target, &td.args, out);
1577 }
1578 match &expr.node {
1579 Expr::FnCall(callee, args) => {
1580 collect_qualifying_in_expr(callee, outer_params, recursive, out);
1581 for a in args {
1582 collect_qualifying_in_expr(a, outer_params, recursive, out);
1583 }
1584 }
1585 Expr::Match { subject, arms } => {
1586 collect_qualifying_in_expr(subject, outer_params, recursive, out);
1587 for a in arms {
1588 collect_qualifying_in_expr(&a.body, outer_params, recursive, out);
1589 }
1590 }
1591 Expr::BinOp(_, l, r) => {
1592 collect_qualifying_in_expr(l, outer_params, recursive, out);
1593 collect_qualifying_in_expr(r, outer_params, recursive, out);
1594 }
1595 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => {
1596 collect_qualifying_in_expr(e, outer_params, recursive, out);
1597 }
1598 Expr::Constructor(_, Some(e)) => {
1599 collect_qualifying_in_expr(e, outer_params, recursive, out);
1600 }
1601 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1602 for x in xs {
1603 collect_qualifying_in_expr(x, outer_params, recursive, out);
1604 }
1605 }
1606 Expr::MapLiteral(pairs) => {
1607 for (k, v) in pairs {
1608 collect_qualifying_in_expr(k, outer_params, recursive, out);
1609 collect_qualifying_in_expr(v, outer_params, recursive, out);
1610 }
1611 }
1612 Expr::RecordCreate { fields, .. } => {
1613 for (_, e) in fields {
1614 collect_qualifying_in_expr(e, outer_params, recursive, out);
1615 }
1616 }
1617 Expr::RecordUpdate { base, updates, .. } => {
1618 collect_qualifying_in_expr(base, outer_params, recursive, out);
1619 for (_, e) in updates {
1620 collect_qualifying_in_expr(e, outer_params, recursive, out);
1621 }
1622 }
1623 Expr::InterpolatedStr(parts) => {
1624 for p in parts {
1625 if let crate::ast::StrPart::Parsed(e) = p {
1626 collect_qualifying_in_expr(e, outer_params, recursive, out);
1627 }
1628 }
1629 }
1630 Expr::TailCall(td) => {
1631 for a in &td.args {
1632 collect_qualifying_in_expr(a, outer_params, recursive, out);
1633 }
1634 }
1635 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {}
1636 }
1637}