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 mut index = 0u32;
414 let mut stack: Vec<FnId> = Vec::new();
415 let mut on_stack: HashSet<FnId> = HashSet::new();
416 let mut indices: HashMap<FnId, u32> = HashMap::new();
417 let mut lowlinks: HashMap<FnId, u32> = HashMap::new();
418 let mut multi_scc: HashSet<FnId> = HashSet::new();
419
420 #[allow(clippy::too_many_arguments)]
421 fn strongconnect(
422 v: FnId,
423 graph: &HashMap<FnId, Vec<FnId>>,
424 index: &mut u32,
425 stack: &mut Vec<FnId>,
426 on_stack: &mut HashSet<FnId>,
427 indices: &mut HashMap<FnId, u32>,
428 lowlinks: &mut HashMap<FnId, u32>,
429 multi_scc: &mut HashSet<FnId>,
430 ) {
431 indices.insert(v, *index);
432 lowlinks.insert(v, *index);
433 *index += 1;
434 stack.push(v);
435 on_stack.insert(v);
436 if let Some(neighbors) = graph.get(&v).cloned() {
437 for w in neighbors {
438 if !indices.contains_key(&w) {
439 strongconnect(
440 w, graph, index, stack, on_stack, indices, lowlinks, multi_scc,
441 );
442 let lv = *lowlinks.get(&v).unwrap();
443 let lw = *lowlinks.get(&w).unwrap();
444 lowlinks.insert(v, lv.min(lw));
445 } else if on_stack.contains(&w) {
446 let lv = *lowlinks.get(&v).unwrap();
447 let iw = *indices.get(&w).unwrap();
448 lowlinks.insert(v, lv.min(iw));
449 }
450 }
451 }
452 if lowlinks.get(&v) == indices.get(&v) {
453 let mut scc: Vec<FnId> = Vec::new();
454 loop {
455 let w = stack.pop().unwrap();
456 on_stack.remove(&w);
457 scc.push(w);
458 if w == v {
459 break;
460 }
461 }
462 if scc.len() > 1 {
463 multi_scc.extend(scc);
464 }
465 }
466 }
467
468 let nodes: Vec<FnId> = graph.keys().copied().collect();
469 for v in nodes {
470 if !indices.contains_key(&v) {
471 strongconnect(
472 v,
473 &graph,
474 &mut index,
475 &mut stack,
476 &mut on_stack,
477 &mut indices,
478 &mut lowlinks,
479 &mut multi_scc,
480 );
481 }
482 }
483 multi_scc
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct FnRecognition {
497 pub primary: Archetype,
498 pub labels: Vec<Archetype>,
499}
500
501#[derive(Debug, Clone, Default)]
516pub struct ProgramShape {
517 pub per_fn: std::collections::HashMap<FnId, FnRecognition>,
519 pub sccs: HashSet<FnId>,
523 pub patterns: Vec<ModulePattern>,
529 pub inductable_sum_types: HashSet<String>,
537}
538
539impl ProgramShape {
540 pub fn for_fn(&self, fn_id: FnId) -> Option<&FnRecognition> {
544 self.per_fn.get(&fn_id)
545 }
546}
547
548pub fn analyze_program(resolved_fns: &[&ResolvedFnDef]) -> ProgramShape {
559 let mut facts_by_id: std::collections::HashMap<FnId, Facts> =
560 std::collections::HashMap::with_capacity(resolved_fns.len());
561 for fd in resolved_fns {
562 facts_by_id.insert(fd.fn_id, extract_facts(fd));
563 }
564 let facts_refs: std::collections::HashMap<FnId, &Facts> =
565 facts_by_id.iter().map(|(k, v)| (*k, v)).collect();
566 let sccs = compute_sccs(resolved_fns, &facts_refs);
567
568 let mut per_fn = std::collections::HashMap::with_capacity(resolved_fns.len());
569 for fd in resolved_fns {
570 let facts = &facts_by_id[&fd.fn_id];
571 let labels = classify(fd, facts, &sccs);
572 let primary = primary_label(&labels);
573 per_fn.insert(fd.fn_id, FnRecognition { primary, labels });
574 }
575
576 ProgramShape {
577 per_fn,
578 sccs,
579 patterns: Vec::new(),
580 inductable_sum_types: HashSet::new(),
581 }
582}
583
584pub fn analyze_program_with_modules(
589 resolved_fns: &[&ResolvedFnDef],
590 entry_items: &[crate::ast::TopLevel],
591 dep_modules: &[crate::codegen::ModuleInfo],
592) -> ProgramShape {
593 let mut shape = analyze_program(resolved_fns);
594 shape.patterns = detect_module_patterns(entry_items, dep_modules);
595 shape.inductable_sum_types = collect_inductable_sum_types(entry_items, dep_modules);
596 shape
597}
598
599#[derive(Debug, Clone)]
624pub enum ModulePattern {
625 RefinementSmartConstructor {
633 scope: Option<String>,
640 type_name: String,
644 carrier_field: String,
648 carrier_type: String,
652 constructor_fn: String,
654 param_name: String,
659 predicate: crate::ast::Spanned<crate::ast::Expr>,
663 },
664 WrapperOverRecursion {
684 wrapper_scope: Option<String>,
688 wrapper_fn: String,
690 inner_scope: Option<String>,
693 inner_fn: String,
695 },
696 ResultPipelineChain {
713 scope: Option<String>,
714 fn_name: String,
715 step_count: usize,
716 step_fns: Vec<String>,
723 },
724 RendererFormatter {
741 scope: Option<String>,
742 fn_name: String,
743 },
744 MatchDispatcherFold {
763 scope: Option<String>,
764 fn_name: String,
765 list_param: String,
766 },
767}
768
769pub fn collect_inductable_sum_types(
778 entry_items: &[crate::ast::TopLevel],
779 dep_modules: &[crate::codegen::ModuleInfo],
780) -> HashSet<String> {
781 use crate::ast::{TopLevel, TypeDef};
782 let mut out = HashSet::new();
783 let mut consider = |td: &TypeDef| {
784 if let TypeDef::Sum { name, variants, .. } = td
785 && crate::codegen::common::is_recursive_sum(name, variants)
786 && !indirect_rec_variants(variants, name)
787 {
788 out.insert(name.clone());
789 }
790 };
791 for item in entry_items {
792 if let TopLevel::TypeDef(td) = item {
793 consider(td);
794 }
795 }
796 for m in dep_modules {
797 for td in &m.type_defs {
798 consider(td);
799 }
800 }
801 out
802}
803
804fn indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
808 for variant in variants {
809 for field in &variant.fields {
810 let f = field.trim();
811 if f == type_name {
812 continue;
813 }
814 let opens = f.matches('<').count();
815 if opens > 1 && f.contains(type_name) {
816 return true;
817 }
818 }
819 }
820 false
821}
822
823pub fn detect_module_patterns(
832 entry_items: &[crate::ast::TopLevel],
833 dep_modules: &[crate::codegen::ModuleInfo],
834) -> Vec<ModulePattern> {
835 use crate::ast::{Expr, Stmt, TopLevel, TypeDef};
836
837 let mut out = Vec::new();
838
839 struct CandidateRecord<'a> {
846 scope: Option<String>,
847 type_name: &'a str,
848 carrier_field: &'a str,
849 carrier_type: &'a str,
850 fns: Vec<&'a crate::ast::FnDef>,
851 }
852
853 let entry_fns: Vec<&crate::ast::FnDef> = entry_items
854 .iter()
855 .filter_map(|i| match i {
856 TopLevel::FnDef(fd) => Some(fd),
857 _ => None,
858 })
859 .collect();
860
861 let mut candidates: Vec<CandidateRecord<'_>> = Vec::new();
862 for td in entry_items.iter().filter_map(|i| match i {
863 TopLevel::TypeDef(td) => Some(td),
864 _ => None,
865 }) {
866 if let TypeDef::Product { name, fields, .. } = td
867 && fields.len() == 1
868 {
869 let (fname, ftype) = &fields[0];
870 candidates.push(CandidateRecord {
871 scope: None,
872 type_name: name.as_str(),
873 carrier_field: fname.as_str(),
874 carrier_type: ftype.as_str(),
875 fns: entry_fns.clone(),
876 });
877 }
878 }
879 for m in dep_modules {
880 let module_fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
881 for td in &m.type_defs {
882 if let TypeDef::Product { name, fields, .. } = td
883 && fields.len() == 1
884 {
885 let (fname, ftype) = &fields[0];
886 candidates.push(CandidateRecord {
887 scope: Some(m.prefix.clone()),
888 type_name: name.as_str(),
889 carrier_field: fname.as_str(),
890 carrier_type: ftype.as_str(),
891 fns: module_fns.clone(),
892 });
893 }
894 }
895 }
896
897 for candidate in &candidates {
904 let CandidateRecord {
905 scope,
906 type_name,
907 carrier_field,
908 carrier_type,
909 fns,
910 } = candidate;
911 for fd in fns {
912 if !fd.return_type.starts_with("Result<") {
913 continue;
914 }
915 if !fd.return_type[7..].starts_with(*type_name) {
916 continue;
917 }
918 if fd.params.len() != 1 {
919 continue;
920 }
921 let (param_name, _) = &fd.params[0];
922 let stmts = fd.body.stmts();
923 if stmts.len() != 1 {
924 continue;
925 }
926 let Stmt::Expr(body_expr) = &stmts[0] else {
927 continue;
928 };
929 let Expr::Match { subject, arms } = &body_expr.node else {
930 continue;
931 };
932 if !crate::codegen::common::is_refinement_bool_ok_err_match(
933 arms,
934 type_name,
935 carrier_field,
936 param_name,
937 ) {
938 continue;
939 }
940 out.push(ModulePattern::RefinementSmartConstructor {
941 scope: scope.clone(),
942 type_name: (*type_name).to_string(),
943 carrier_field: (*carrier_field).to_string(),
944 carrier_type: (*carrier_type).to_string(),
945 constructor_fn: fd.name.clone(),
946 param_name: param_name.clone(),
947 predicate: (**subject).clone(),
948 });
949 break;
950 }
951 }
952
953 detect_wrapper_over_recursion(None, &entry_fns, &mut out);
957 for m in dep_modules {
958 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
959 detect_wrapper_over_recursion(Some(m.prefix.clone()), &fns, &mut out);
960 }
961
962 detect_result_pipeline_chain(None, &entry_fns, &mut out);
964 for m in dep_modules {
965 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
966 detect_result_pipeline_chain(Some(m.prefix.clone()), &fns, &mut out);
967 }
968
969 detect_renderer_formatter(None, &entry_fns, &mut out);
971 for m in dep_modules {
972 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
973 detect_renderer_formatter(Some(m.prefix.clone()), &fns, &mut out);
974 }
975
976 detect_match_dispatcher_fold(None, &entry_fns, &mut out);
978 for m in dep_modules {
979 let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
980 detect_match_dispatcher_fold(Some(m.prefix.clone()), &fns, &mut out);
981 }
982
983 out
984}
985
986fn detect_match_dispatcher_fold(
989 scope: Option<String>,
990 fns: &[&crate::ast::FnDef],
991 out: &mut Vec<ModulePattern>,
992) {
993 use crate::ast::{Expr, Pattern, Stmt};
994 for fd in fns {
995 let stmts = fd.body.stmts();
996 if stmts.len() != 1 {
997 continue;
998 }
999 let Stmt::Expr(body_expr) = &stmts[0] else {
1000 continue;
1001 };
1002 let Expr::Match { subject, arms } = &body_expr.node else {
1003 continue;
1004 };
1005 let subj_name = match &subject.node {
1010 Expr::Ident(n) => n.as_str(),
1011 Expr::Resolved { name, .. } => name.as_str(),
1012 _ => continue,
1013 };
1014 if !fd.params.iter().any(|(n, _)| n == subj_name) {
1015 continue;
1016 }
1017 let has_nil = arms.iter().any(|a| matches!(a.pattern, Pattern::EmptyList));
1018 let has_cons = arms
1019 .iter()
1020 .any(|a| matches!(a.pattern, Pattern::Cons(_, _)));
1021 if !(has_nil && has_cons) {
1022 continue;
1023 }
1024 if !body_calls_name(&fd.body, &fd.name) {
1025 continue;
1026 }
1027 out.push(ModulePattern::MatchDispatcherFold {
1028 scope: scope.clone(),
1029 fn_name: fd.name.clone(),
1030 list_param: subj_name.to_string(),
1031 });
1032 }
1033}
1034
1035fn detect_renderer_formatter(
1038 scope: Option<String>,
1039 fns: &[&crate::ast::FnDef],
1040 out: &mut Vec<ModulePattern>,
1041) {
1042 for fd in fns {
1043 if fd.return_type != "String" {
1044 continue;
1045 }
1046 if !fd.effects.is_empty() {
1047 continue;
1048 }
1049 if body_calls_name(&fd.body, &fd.name) {
1050 continue;
1051 }
1052 if !body_has_string_building(&fd.body) {
1053 continue;
1054 }
1055 out.push(ModulePattern::RendererFormatter {
1056 scope: scope.clone(),
1057 fn_name: fd.name.clone(),
1058 });
1059 }
1060}
1061
1062fn body_has_string_building(body: &crate::ast::FnBody) -> bool {
1070 for stmt in body.stmts() {
1071 let expr = match stmt {
1072 crate::ast::Stmt::Binding(_, _, e) => e,
1073 crate::ast::Stmt::Expr(e) => e,
1074 };
1075 if expr_has_string_building(expr) {
1076 return true;
1077 }
1078 }
1079 false
1080}
1081
1082fn expr_has_string_building(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
1083 use crate::ast::Expr;
1084 match &expr.node {
1085 Expr::InterpolatedStr(_) => true,
1086 Expr::BinOp(crate::ast::BinOp::Add, _, _) => true,
1087 Expr::FnCall(callee, args) => {
1088 expr_has_string_building(callee) || args.iter().any(expr_has_string_building)
1089 }
1090 Expr::TailCall(td) => td.args.iter().any(expr_has_string_building),
1091 Expr::Match { subject, arms } => {
1092 expr_has_string_building(subject)
1093 || arms.iter().any(|a| expr_has_string_building(&a.body))
1094 }
1095 Expr::BinOp(_, l, r) => expr_has_string_building(l) || expr_has_string_building(r),
1096 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_has_string_building(e),
1097 Expr::Constructor(_, Some(e)) => expr_has_string_building(e),
1098 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1099 xs.iter().any(expr_has_string_building)
1100 }
1101 Expr::MapLiteral(pairs) => pairs
1102 .iter()
1103 .any(|(k, v)| expr_has_string_building(k) || expr_has_string_building(v)),
1104 Expr::RecordCreate { fields, .. } => {
1105 fields.iter().any(|(_, e)| expr_has_string_building(e))
1106 }
1107 Expr::RecordUpdate { base, updates, .. } => {
1108 expr_has_string_building(base)
1109 || updates.iter().any(|(_, e)| expr_has_string_building(e))
1110 }
1111 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1112 false
1113 }
1114 }
1115}
1116
1117fn detect_result_pipeline_chain(
1123 scope: Option<String>,
1124 fns: &[&crate::ast::FnDef],
1125 out: &mut Vec<ModulePattern>,
1126) {
1127 use crate::ast::{Expr, Stmt};
1128 for fd in fns {
1129 if !fd.return_type.starts_with("Result<") {
1130 continue;
1131 }
1132 let stmts = fd.body.stmts();
1133 if stmts.len() < 2 {
1134 continue;
1135 }
1136 if !matches!(stmts.last(), Some(Stmt::Expr(_))) {
1137 continue;
1138 }
1139 let mut step_fns: Vec<String> = Vec::new();
1140 for stmt in stmts {
1141 if let Stmt::Binding(_, _, value) = stmt
1142 && let Expr::ErrorProp(inner) = &value.node
1143 && let Expr::FnCall(callee, _) = &inner.node
1144 && let Expr::Ident(name) = &callee.node
1145 {
1146 step_fns.push(name.clone());
1147 }
1148 }
1149 if step_fns.len() < 2 {
1150 continue;
1151 }
1152 let step_count = step_fns.len();
1153 out.push(ModulePattern::ResultPipelineChain {
1154 scope: scope.clone(),
1155 fn_name: fd.name.clone(),
1156 step_count,
1157 step_fns,
1158 });
1159 }
1160}
1161
1162fn detect_wrapper_over_recursion(
1168 scope: Option<String>,
1169 fns: &[&crate::ast::FnDef],
1170 out: &mut Vec<ModulePattern>,
1171) {
1172 if fns.is_empty() {
1173 return;
1174 }
1175
1176 let mut recursive: HashSet<String> = HashSet::new();
1177 for fd in fns {
1178 if body_calls_name(&fd.body, &fd.name) {
1179 recursive.insert(fd.name.clone());
1180 }
1181 }
1182 if recursive.is_empty() {
1183 return;
1184 }
1185
1186 for fd in fns {
1187 if recursive.contains(&fd.name) {
1188 continue;
1189 }
1190 if fd.params.is_empty() {
1191 continue;
1192 }
1193 let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
1194 let mut hits: Vec<String> = Vec::new();
1195 collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
1196 hits.sort();
1197 hits.dedup();
1198 if hits.len() != 1 {
1199 continue;
1200 }
1201 let inner = hits.into_iter().next().unwrap();
1202 out.push(ModulePattern::WrapperOverRecursion {
1203 wrapper_scope: scope.clone(),
1204 wrapper_fn: fd.name.clone(),
1205 inner_scope: scope.clone(),
1206 inner_fn: inner,
1207 });
1208 }
1209}
1210
1211fn body_calls_name(body: &crate::ast::FnBody, name: &str) -> bool {
1215 for stmt in body.stmts() {
1216 let expr = match stmt {
1217 crate::ast::Stmt::Binding(_, _, e) => e,
1218 crate::ast::Stmt::Expr(e) => e,
1219 };
1220 if expr_calls_name(expr, name) {
1221 return true;
1222 }
1223 }
1224 false
1225}
1226
1227fn expr_calls_name(expr: &crate::ast::Spanned<crate::ast::Expr>, name: &str) -> bool {
1228 use crate::ast::Expr;
1229 match &expr.node {
1230 Expr::FnCall(callee, args) => {
1231 if let Expr::Ident(n) = &callee.node
1232 && n == name
1233 {
1234 return true;
1235 }
1236 if expr_calls_name(callee, name) {
1237 return true;
1238 }
1239 args.iter().any(|a| expr_calls_name(a, name))
1240 }
1241 Expr::TailCall(td) => td.target == name || td.args.iter().any(|a| expr_calls_name(a, name)),
1242 Expr::Match { subject, arms } => {
1243 if expr_calls_name(subject, name) {
1244 return true;
1245 }
1246 arms.iter().any(|a| expr_calls_name(&a.body, name))
1247 }
1248 Expr::BinOp(_, l, r) => expr_calls_name(l, name) || expr_calls_name(r, name),
1249 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_calls_name(e, name),
1250 Expr::Constructor(_, Some(e)) => expr_calls_name(e, name),
1251 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1252 xs.iter().any(|x| expr_calls_name(x, name))
1253 }
1254 Expr::MapLiteral(pairs) => pairs
1255 .iter()
1256 .any(|(k, v)| expr_calls_name(k, name) || expr_calls_name(v, name)),
1257 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_name(e, name)),
1258 Expr::RecordUpdate { base, updates, .. } => {
1259 expr_calls_name(base, name) || updates.iter().any(|(_, e)| expr_calls_name(e, name))
1260 }
1261 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1262 crate::ast::StrPart::Parsed(e) => expr_calls_name(e, name),
1263 crate::ast::StrPart::Literal(_) => false,
1264 }),
1265 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
1266 false
1267 }
1268 }
1269}
1270
1271fn collect_qualifying_inner_calls(
1277 body: &crate::ast::FnBody,
1278 outer_params: &[&str],
1279 recursive: &HashSet<String>,
1280 out: &mut Vec<String>,
1281) {
1282 for stmt in body.stmts() {
1283 let expr = match stmt {
1284 crate::ast::Stmt::Binding(_, _, e) => e,
1285 crate::ast::Stmt::Expr(e) => e,
1286 };
1287 collect_qualifying_in_expr(expr, outer_params, recursive, out);
1288 }
1289}
1290
1291fn collect_qualifying_in_expr(
1292 expr: &crate::ast::Spanned<crate::ast::Expr>,
1293 outer_params: &[&str],
1294 recursive: &HashSet<String>,
1295 out: &mut Vec<String>,
1296) {
1297 use crate::ast::Expr;
1298 let try_qualify = |callee: &str, args: &[crate::ast::Spanned<Expr>], out: &mut Vec<String>| {
1299 if !recursive.contains(callee) {
1300 return;
1301 }
1302 if args.len() <= outer_params.len() {
1303 return;
1304 }
1305 let mut arg_idents: HashSet<&str> = HashSet::new();
1309 for a in args {
1310 match &a.node {
1311 Expr::Ident(n) => {
1312 arg_idents.insert(n.as_str());
1313 }
1314 Expr::Resolved { name, .. } => {
1315 arg_idents.insert(name.as_str());
1316 }
1317 _ => {}
1318 }
1319 }
1320 if outer_params.iter().all(|p| arg_idents.contains(*p)) {
1321 out.push(callee.to_string());
1322 }
1323 };
1324 if let Expr::FnCall(callee, args) = &expr.node
1325 && let Expr::Ident(name) = &callee.node
1326 {
1327 try_qualify(name, args, out);
1328 }
1329 if let Expr::TailCall(td) = &expr.node {
1335 try_qualify(&td.target, &td.args, out);
1336 }
1337 match &expr.node {
1338 Expr::FnCall(callee, args) => {
1339 collect_qualifying_in_expr(callee, outer_params, recursive, out);
1340 for a in args {
1341 collect_qualifying_in_expr(a, outer_params, recursive, out);
1342 }
1343 }
1344 Expr::Match { subject, arms } => {
1345 collect_qualifying_in_expr(subject, outer_params, recursive, out);
1346 for a in arms {
1347 collect_qualifying_in_expr(&a.body, outer_params, recursive, out);
1348 }
1349 }
1350 Expr::BinOp(_, l, r) => {
1351 collect_qualifying_in_expr(l, outer_params, recursive, out);
1352 collect_qualifying_in_expr(r, outer_params, recursive, out);
1353 }
1354 Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => {
1355 collect_qualifying_in_expr(e, outer_params, recursive, out);
1356 }
1357 Expr::Constructor(_, Some(e)) => {
1358 collect_qualifying_in_expr(e, outer_params, recursive, out);
1359 }
1360 Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
1361 for x in xs {
1362 collect_qualifying_in_expr(x, outer_params, recursive, out);
1363 }
1364 }
1365 Expr::MapLiteral(pairs) => {
1366 for (k, v) in pairs {
1367 collect_qualifying_in_expr(k, outer_params, recursive, out);
1368 collect_qualifying_in_expr(v, outer_params, recursive, out);
1369 }
1370 }
1371 Expr::RecordCreate { fields, .. } => {
1372 for (_, e) in fields {
1373 collect_qualifying_in_expr(e, outer_params, recursive, out);
1374 }
1375 }
1376 Expr::RecordUpdate { base, updates, .. } => {
1377 collect_qualifying_in_expr(base, outer_params, recursive, out);
1378 for (_, e) in updates {
1379 collect_qualifying_in_expr(e, outer_params, recursive, out);
1380 }
1381 }
1382 Expr::InterpolatedStr(parts) => {
1383 for p in parts {
1384 if let crate::ast::StrPart::Parsed(e) = p {
1385 collect_qualifying_in_expr(e, outer_params, recursive, out);
1386 }
1387 }
1388 }
1389 Expr::TailCall(td) => {
1390 for a in &td.args {
1391 collect_qualifying_in_expr(a, outer_params, recursive, out);
1392 }
1393 }
1394 Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {}
1395 }
1396}