use std::collections::{HashMap, HashSet};
use crate::ir::FnId;
use crate::ir::hir::{
ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnDef, ResolvedMatchArm, ResolvedPattern,
ResolvedStmt, ResolvedStrPart,
};
use crate::types::Type;
#[derive(Debug, Default)]
pub struct Facts {
pub calls_to: HashSet<FnId>,
pub tail_calls: HashSet<FnId>,
pub builtin_calls: Vec<String>,
pub ctor_constructs: Vec<ResolvedCtor>,
pub ctor_match_patterns: usize,
pub other_match_patterns: usize,
pub has_match: bool,
pub has_error_prop: bool,
pub record_creates: usize,
pub last_stmt_is_match: bool,
pub only_stmt_is_literal: bool,
pub body_stmt_count: usize,
pub has_interp_str: bool,
pub string_builtin_calls: usize,
pub has_match_with_err_arm: bool,
}
fn walk_expr(e: &ResolvedExpr, facts: &mut Facts) {
match e {
ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {}
ResolvedExpr::Attr(inner, _) => walk_expr(&inner.node, facts),
ResolvedExpr::Call(callee, args) => {
match callee {
ResolvedCallee::Fn(id) => {
facts.calls_to.insert(*id);
}
ResolvedCallee::Builtin(name) => {
if name.starts_with("String.") {
facts.string_builtin_calls += 1;
}
facts.builtin_calls.push(name.clone());
}
ResolvedCallee::Intrinsic(_) => {}
ResolvedCallee::LocalSlot { .. } => {}
ResolvedCallee::Unresolved { callee } => walk_expr(&callee.node, facts),
}
for a in args {
walk_expr(&a.node, facts);
}
}
ResolvedExpr::BinOp(_, a, b) => {
walk_expr(&a.node, facts);
walk_expr(&b.node, facts);
}
ResolvedExpr::Neg(inner) => walk_expr(&inner.node, facts),
ResolvedExpr::Match { subject, arms } => {
facts.has_match = true;
walk_expr(&subject.node, facts);
for arm in arms {
count_arm_pattern(&arm.pattern, facts);
walk_match_arm(arm, facts);
}
}
ResolvedExpr::Ctor(c, args) => {
facts.ctor_constructs.push(c.clone());
for a in args {
walk_expr(&a.node, facts);
}
}
ResolvedExpr::ErrorProp(inner) => {
facts.has_error_prop = true;
walk_expr(&inner.node, facts);
}
ResolvedExpr::InterpolatedStr(parts) => {
facts.has_interp_str = true;
for p in parts {
if let ResolvedStrPart::Parsed(e) = p {
walk_expr(&e.node, facts);
}
}
}
ResolvedExpr::List(xs) | ResolvedExpr::Tuple(xs) => {
for x in xs {
walk_expr(&x.node, facts);
}
}
ResolvedExpr::MapLiteral(pairs) => {
for (k, v) in pairs {
walk_expr(&k.node, facts);
walk_expr(&v.node, facts);
}
}
ResolvedExpr::RecordCreate { fields, .. } => {
facts.record_creates += 1;
for (_, v) in fields {
walk_expr(&v.node, facts);
}
}
ResolvedExpr::RecordUpdate { base, updates, .. } => {
walk_expr(&base.node, facts);
for (_, v) in updates {
walk_expr(&v.node, facts);
}
}
ResolvedExpr::TailCall { target, args } => {
facts.tail_calls.insert(*target);
facts.calls_to.insert(*target);
for a in args {
walk_expr(&a.node, facts);
}
}
ResolvedExpr::IndependentProduct(xs, _) => {
for x in xs {
walk_expr(&x.node, facts);
}
}
}
}
fn count_arm_pattern(p: &ResolvedPattern, facts: &mut Facts) {
match p {
ResolvedPattern::Ctor(ctor, _) => {
facts.ctor_match_patterns += 1;
if matches!(
ctor,
ResolvedCtor::Builtin(crate::ir::hir::BuiltinCtor::ResultErr)
) {
facts.has_match_with_err_arm = true;
}
}
_ => facts.other_match_patterns += 1,
}
}
fn walk_match_arm(arm: &ResolvedMatchArm, facts: &mut Facts) {
walk_expr(&arm.body.node, facts);
}
pub fn extract_facts(fd: &ResolvedFnDef) -> Facts {
let mut facts = Facts::default();
let stmts = fd.body.stmts();
facts.body_stmt_count = stmts.len();
for stmt in stmts {
match stmt {
ResolvedStmt::Binding { value, .. } => walk_expr(&value.node, &mut facts),
ResolvedStmt::Expr(value) => walk_expr(&value.node, &mut facts),
}
}
if let Some(last) = stmts.last() {
let expr = match last {
ResolvedStmt::Binding { value, .. } => &value.node,
ResolvedStmt::Expr(value) => &value.node,
};
facts.last_stmt_is_match = matches!(expr, ResolvedExpr::Match { .. });
}
facts.only_stmt_is_literal = stmts.len() == 1 && {
let expr = match &stmts[0] {
ResolvedStmt::Binding { value, .. } => &value.node,
ResolvedStmt::Expr(value) => &value.node,
};
matches!(
expr,
ResolvedExpr::Literal(_)
| ResolvedExpr::List(_)
| ResolvedExpr::Tuple(_)
| ResolvedExpr::MapLiteral(_)
| ResolvedExpr::RecordCreate { .. }
| ResolvedExpr::Ctor(_, _)
)
};
facts
}
pub fn classify(fd: &ResolvedFnDef, facts: &Facts, scc: &HashSet<FnId>) -> Vec<Archetype> {
let mut labels = Vec::new();
let self_call = facts.calls_to.contains(&fd.fn_id) || facts.tail_calls.contains(&fd.fn_id);
if self_call {
labels.push(Archetype::StructuralRecursion);
}
if scc.contains(&fd.fn_id) {
labels.push(Archetype::SccMutual);
}
if facts.last_stmt_is_match {
if facts.ctor_match_patterns >= facts.other_match_patterns && facts.ctor_match_patterns > 0
{
labels.push(Archetype::MatchDispatcher);
} else {
labels.push(Archetype::MatchOnValue);
}
}
if !fd.effects.is_empty() {
let total_calls = facts.calls_to.len() + facts.builtin_calls.len();
if total_calls >= 2 {
labels.push(Archetype::Orchestration);
} else {
labels.push(Archetype::EffectfulLeaf);
}
}
let is_result_ret = type_is_result(&fd.return_type);
if is_result_ret && facts.has_error_prop {
labels.push(Archetype::PipelineResult);
} else if is_result_ret && facts.has_match_with_err_arm {
labels.push(Archetype::ManualResultAdapter);
}
let is_string_ret = matches!(&fd.return_type, Type::Named { name, .. } if name == "String");
if is_string_ret
&& fd.effects.is_empty()
&& (facts.has_interp_str || facts.string_builtin_calls >= 1)
&& (facts.body_stmt_count >= 2 || facts.has_interp_str)
{
labels.push(Archetype::RendererFormatter);
}
if facts.body_stmt_count == 1
&& (!facts.ctor_constructs.is_empty() || facts.record_creates > 0)
&& !facts.has_match
{
labels.push(Archetype::ConstructorWrapper);
}
if fd.params.is_empty()
&& facts.calls_to.is_empty()
&& facts.builtin_calls.is_empty()
&& fd.effects.is_empty()
&& facts.only_stmt_is_literal
{
labels.push(Archetype::DataAsFunction);
}
if facts.body_stmt_count == 1
&& !facts.has_match
&& !self_call
&& fd.effects.is_empty()
&& (!facts.builtin_calls.is_empty() || !facts.calls_to.is_empty())
{
labels.push(Archetype::TrivialHelper);
}
if facts.body_stmt_count == 1
&& !facts.has_match
&& !self_call
&& fd.effects.is_empty()
&& facts.builtin_calls.is_empty()
&& facts.calls_to.is_empty()
&& facts.ctor_constructs.is_empty()
&& !fd.params.is_empty()
{
labels.push(Archetype::PureExpression);
}
if facts.body_stmt_count >= 2
&& !facts.last_stmt_is_match
&& fd.effects.is_empty()
&& (!facts.builtin_calls.is_empty() || !facts.calls_to.is_empty())
&& !self_call
{
labels.push(Archetype::LetPipeline);
}
labels
}
pub fn type_is_result(t: &Type) -> bool {
matches!(t, Type::Named { name, .. } if name == "Result")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Archetype {
SccMutual,
StructuralRecursion,
MatchDispatcher,
PipelineResult,
ManualResultAdapter,
RendererFormatter,
MatchOnValue,
Orchestration,
EffectfulLeaf,
LetPipeline,
ConstructorWrapper,
DataAsFunction,
TrivialHelper,
PureExpression,
Unclassified,
}
impl Archetype {
pub fn all() -> &'static [Archetype] {
&[
Archetype::SccMutual,
Archetype::StructuralRecursion,
Archetype::MatchDispatcher,
Archetype::PipelineResult,
Archetype::ManualResultAdapter,
Archetype::RendererFormatter,
Archetype::MatchOnValue,
Archetype::Orchestration,
Archetype::EffectfulLeaf,
Archetype::LetPipeline,
Archetype::ConstructorWrapper,
Archetype::DataAsFunction,
Archetype::TrivialHelper,
Archetype::PureExpression,
]
}
pub fn as_str(&self) -> &'static str {
match self {
Archetype::SccMutual => "scc-mutual",
Archetype::StructuralRecursion => "structural-recursion",
Archetype::MatchDispatcher => "match-dispatcher",
Archetype::PipelineResult => "pipeline-result",
Archetype::ManualResultAdapter => "manual-result-adapter",
Archetype::RendererFormatter => "renderer-formatter",
Archetype::MatchOnValue => "match-on-value",
Archetype::Orchestration => "orchestration",
Archetype::EffectfulLeaf => "effectful-leaf",
Archetype::LetPipeline => "let-pipeline",
Archetype::ConstructorWrapper => "constructor-wrapper",
Archetype::DataAsFunction => "data-as-function",
Archetype::TrivialHelper => "trivial-helper",
Archetype::PureExpression => "pure-expression",
Archetype::Unclassified => "unclassified",
}
}
pub fn parse(s: &str) -> Option<Archetype> {
Some(match s {
"scc-mutual" => Archetype::SccMutual,
"structural-recursion" => Archetype::StructuralRecursion,
"match-dispatcher" => Archetype::MatchDispatcher,
"pipeline-result" => Archetype::PipelineResult,
"manual-result-adapter" => Archetype::ManualResultAdapter,
"renderer-formatter" => Archetype::RendererFormatter,
"match-on-value" => Archetype::MatchOnValue,
"orchestration" => Archetype::Orchestration,
"effectful-leaf" => Archetype::EffectfulLeaf,
"let-pipeline" => Archetype::LetPipeline,
"constructor-wrapper" => Archetype::ConstructorWrapper,
"data-as-function" => Archetype::DataAsFunction,
"trivial-helper" => Archetype::TrivialHelper,
"pure-expression" => Archetype::PureExpression,
"unclassified" => Archetype::Unclassified,
_ => return None,
})
}
}
pub fn primary_label(labels: &[Archetype]) -> Archetype {
for &want in Archetype::all() {
if labels.contains(&want) {
return want;
}
}
Archetype::Unclassified
}
pub fn compute_sccs(fns: &[&ResolvedFnDef], facts_by_id: &HashMap<FnId, &Facts>) -> HashSet<FnId> {
let mut graph: HashMap<FnId, Vec<FnId>> = HashMap::new();
let fn_ids: HashSet<FnId> = fns.iter().map(|f| f.fn_id).collect();
for fd in fns {
if let Some(facts) = facts_by_id.get(&fd.fn_id) {
let edges: Vec<FnId> = facts
.calls_to
.iter()
.copied()
.filter(|c| fn_ids.contains(c) && *c != fd.fn_id)
.collect();
graph.insert(fd.fn_id, edges);
}
}
let nodes: Vec<FnId> = graph.keys().copied().collect();
crate::scc::mutually_recursive(&nodes, &graph)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FnRecognition {
pub primary: Archetype,
pub labels: Vec<Archetype>,
}
#[derive(Debug, Clone, Default)]
pub struct ProgramShape {
pub per_fn: std::collections::HashMap<FnId, FnRecognition>,
pub sccs: HashSet<FnId>,
pub patterns: Vec<ModulePattern>,
pub inductable_sum_types: HashSet<String>,
}
impl ProgramShape {
pub fn for_fn(&self, fn_id: FnId) -> Option<&FnRecognition> {
self.per_fn.get(&fn_id)
}
}
pub fn analyze_program(resolved_fns: &[&ResolvedFnDef]) -> ProgramShape {
let mut facts_by_id: std::collections::HashMap<FnId, Facts> =
std::collections::HashMap::with_capacity(resolved_fns.len());
for fd in resolved_fns {
facts_by_id.insert(fd.fn_id, extract_facts(fd));
}
let facts_refs: std::collections::HashMap<FnId, &Facts> =
facts_by_id.iter().map(|(k, v)| (*k, v)).collect();
let sccs = compute_sccs(resolved_fns, &facts_refs);
let mut per_fn = std::collections::HashMap::with_capacity(resolved_fns.len());
for fd in resolved_fns {
let facts = &facts_by_id[&fd.fn_id];
let labels = classify(fd, facts, &sccs);
let primary = primary_label(&labels);
per_fn.insert(fd.fn_id, FnRecognition { primary, labels });
}
ProgramShape {
per_fn,
sccs,
patterns: Vec::new(),
inductable_sum_types: HashSet::new(),
}
}
pub fn analyze_program_with_modules(
resolved_fns: &[&ResolvedFnDef],
entry_items: &[crate::ast::TopLevel],
dep_modules: &[crate::codegen::ModuleInfo],
) -> ProgramShape {
let mut shape = analyze_program(resolved_fns);
shape.patterns = detect_module_patterns(entry_items, dep_modules);
shape.inductable_sum_types = collect_inductable_sum_types(entry_items, dep_modules);
shape
}
#[derive(Debug, Clone)]
pub enum ModulePattern {
RefinementSmartConstructor {
scope: Option<String>,
type_name: String,
carrier_field: String,
carrier_type: String,
constructor_fn: String,
param_name: String,
predicate: crate::ast::Spanned<crate::ast::Expr>,
},
WrapperOverRecursion {
wrapper_scope: Option<String>,
wrapper_fn: String,
inner_scope: Option<String>,
inner_fn: String,
},
ResultPipelineChain {
scope: Option<String>,
fn_name: String,
step_count: usize,
step_fns: Vec<String>,
},
RendererFormatter {
scope: Option<String>,
fn_name: String,
},
MatchDispatcherFold {
scope: Option<String>,
fn_name: String,
list_param: String,
},
AccumulatorFold {
scope: Option<String>,
wrapper_fn: String,
loop_fn: String,
list_param: String,
acc_param: String,
step_fn: Option<String>,
step_op: Option<crate::ast::BinOp>,
finish_fn: Option<String>,
driver_type: Option<String>,
step_value_first: bool,
},
}
pub fn collect_inductable_sum_types(
entry_items: &[crate::ast::TopLevel],
dep_modules: &[crate::codegen::ModuleInfo],
) -> HashSet<String> {
use crate::ast::{TopLevel, TypeDef};
let mut out = HashSet::new();
let mut consider = |td: &TypeDef| {
if let TypeDef::Sum { name, variants, .. } = td
&& crate::codegen::common::is_recursive_sum(name, variants)
&& !indirect_rec_variants(variants, name)
{
out.insert(name.clone());
}
};
for item in entry_items {
if let TopLevel::TypeDef(td) = item {
consider(td);
}
}
for m in dep_modules {
for td in &m.type_defs {
consider(td);
}
}
out
}
fn indirect_rec_variants(variants: &[crate::ast::TypeVariant], type_name: &str) -> bool {
for variant in variants {
for field in &variant.fields {
let f = field.trim();
if f == type_name {
continue;
}
let opens = f.matches('<').count();
if opens > 1 && f.contains(type_name) {
return true;
}
}
}
false
}
pub fn detect_module_patterns(
entry_items: &[crate::ast::TopLevel],
dep_modules: &[crate::codegen::ModuleInfo],
) -> Vec<ModulePattern> {
use crate::ast::{Expr, Stmt, TopLevel, TypeDef};
let mut out = Vec::new();
struct CandidateRecord<'a> {
scope: Option<String>,
type_name: &'a str,
carrier_field: &'a str,
carrier_type: &'a str,
fns: Vec<&'a crate::ast::FnDef>,
}
let entry_fns: Vec<&crate::ast::FnDef> = entry_items
.iter()
.filter_map(|i| match i {
TopLevel::FnDef(fd) => Some(fd),
_ => None,
})
.collect();
let mut candidates: Vec<CandidateRecord<'_>> = Vec::new();
for td in entry_items.iter().filter_map(|i| match i {
TopLevel::TypeDef(td) => Some(td),
_ => None,
}) {
if let TypeDef::Product { name, fields, .. } = td
&& fields.len() == 1
{
let (fname, ftype) = &fields[0];
candidates.push(CandidateRecord {
scope: None,
type_name: name.as_str(),
carrier_field: fname.as_str(),
carrier_type: ftype.as_str(),
fns: entry_fns.clone(),
});
}
}
for m in dep_modules {
let module_fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
for td in &m.type_defs {
if let TypeDef::Product { name, fields, .. } = td
&& fields.len() == 1
{
let (fname, ftype) = &fields[0];
candidates.push(CandidateRecord {
scope: Some(m.prefix.clone()),
type_name: name.as_str(),
carrier_field: fname.as_str(),
carrier_type: ftype.as_str(),
fns: module_fns.clone(),
});
}
}
}
for candidate in &candidates {
let CandidateRecord {
scope,
type_name,
carrier_field,
carrier_type,
fns,
} = candidate;
for fd in fns {
if !fd.return_type.starts_with("Result<") {
continue;
}
if !fd.return_type[7..].starts_with(*type_name) {
continue;
}
if fd.params.len() != 1 {
continue;
}
let (param_name, _) = &fd.params[0];
let stmts = fd.body.stmts();
if stmts.len() != 1 {
continue;
}
let Stmt::Expr(body_expr) = &stmts[0] else {
continue;
};
let Expr::Match { subject, arms } = &body_expr.node else {
continue;
};
if !crate::codegen::common::is_refinement_bool_ok_err_match(
arms,
type_name,
carrier_field,
param_name,
) {
continue;
}
out.push(ModulePattern::RefinementSmartConstructor {
scope: scope.clone(),
type_name: (*type_name).to_string(),
carrier_field: (*carrier_field).to_string(),
carrier_type: (*carrier_type).to_string(),
constructor_fn: fd.name.clone(),
param_name: param_name.clone(),
predicate: (**subject).clone(),
});
break;
}
}
detect_wrapper_over_recursion(None, &entry_fns, &mut out);
for m in dep_modules {
let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
detect_wrapper_over_recursion(Some(m.prefix.clone()), &fns, &mut out);
}
detect_accumulator_fold(None, &entry_fns, &mut out);
for m in dep_modules {
let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
detect_accumulator_fold(Some(m.prefix.clone()), &fns, &mut out);
}
detect_result_pipeline_chain(None, &entry_fns, &mut out);
for m in dep_modules {
let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
detect_result_pipeline_chain(Some(m.prefix.clone()), &fns, &mut out);
}
detect_renderer_formatter(None, &entry_fns, &mut out);
for m in dep_modules {
let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
detect_renderer_formatter(Some(m.prefix.clone()), &fns, &mut out);
}
detect_match_dispatcher_fold(None, &entry_fns, &mut out);
for m in dep_modules {
let fns: Vec<&crate::ast::FnDef> = m.fn_defs.iter().collect();
detect_match_dispatcher_fold(Some(m.prefix.clone()), &fns, &mut out);
}
out
}
fn detect_match_dispatcher_fold(
scope: Option<String>,
fns: &[&crate::ast::FnDef],
out: &mut Vec<ModulePattern>,
) {
use crate::ast::{Expr, Pattern, Stmt};
for fd in fns {
let stmts = fd.body.stmts();
if stmts.len() != 1 {
continue;
}
let Stmt::Expr(body_expr) = &stmts[0] else {
continue;
};
let Expr::Match { subject, arms } = &body_expr.node else {
continue;
};
let subj_name = match &subject.node {
Expr::Ident(n) => n.as_str(),
Expr::Resolved { name, .. } => name.as_str(),
_ => continue,
};
if !fd.params.iter().any(|(n, _)| n == subj_name) {
continue;
}
let has_nil = arms.iter().any(|a| matches!(a.pattern, Pattern::EmptyList));
let has_cons = arms
.iter()
.any(|a| matches!(a.pattern, Pattern::Cons(_, _)));
if !(has_nil && has_cons) {
continue;
}
if !body_calls_name(&fd.body, &fd.name) {
continue;
}
out.push(ModulePattern::MatchDispatcherFold {
scope: scope.clone(),
fn_name: fd.name.clone(),
list_param: subj_name.to_string(),
});
}
}
fn detect_renderer_formatter(
scope: Option<String>,
fns: &[&crate::ast::FnDef],
out: &mut Vec<ModulePattern>,
) {
for fd in fns {
if fd.return_type != "String" {
continue;
}
if !fd.effects.is_empty() {
continue;
}
if body_calls_name(&fd.body, &fd.name) {
continue;
}
if !body_has_string_building(&fd.body) {
continue;
}
out.push(ModulePattern::RendererFormatter {
scope: scope.clone(),
fn_name: fd.name.clone(),
});
}
}
fn body_has_string_building(body: &crate::ast::FnBody) -> bool {
for stmt in body.stmts() {
let expr = match stmt {
crate::ast::Stmt::Binding(_, _, e) => e,
crate::ast::Stmt::Expr(e) => e,
};
if expr_has_string_building(expr) {
return true;
}
}
false
}
fn expr_has_string_building(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::Expr;
match &expr.node {
Expr::InterpolatedStr(_) => true,
Expr::BinOp(crate::ast::BinOp::Add, _, _) => true,
Expr::FnCall(callee, args) => {
expr_has_string_building(callee) || args.iter().any(expr_has_string_building)
}
Expr::TailCall(td) => td.args.iter().any(expr_has_string_building),
Expr::Match { subject, arms } => {
expr_has_string_building(subject)
|| arms.iter().any(|a| expr_has_string_building(&a.body))
}
Expr::BinOp(_, l, r) => expr_has_string_building(l) || expr_has_string_building(r),
Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_has_string_building(e),
Expr::Constructor(_, Some(e)) => expr_has_string_building(e),
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
xs.iter().any(expr_has_string_building)
}
Expr::MapLiteral(pairs) => pairs
.iter()
.any(|(k, v)| expr_has_string_building(k) || expr_has_string_building(v)),
Expr::RecordCreate { fields, .. } => {
fields.iter().any(|(_, e)| expr_has_string_building(e))
}
Expr::RecordUpdate { base, updates, .. } => {
expr_has_string_building(base)
|| updates.iter().any(|(_, e)| expr_has_string_building(e))
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
false
}
}
}
fn detect_result_pipeline_chain(
scope: Option<String>,
fns: &[&crate::ast::FnDef],
out: &mut Vec<ModulePattern>,
) {
use crate::ast::{Expr, Stmt};
for fd in fns {
if !fd.return_type.starts_with("Result<") {
continue;
}
let stmts = fd.body.stmts();
if stmts.len() < 2 {
continue;
}
if !matches!(stmts.last(), Some(Stmt::Expr(_))) {
continue;
}
let mut step_fns: Vec<String> = Vec::new();
for stmt in stmts {
if let Stmt::Binding(_, _, value) = stmt
&& let Expr::ErrorProp(inner) = &value.node
&& let Expr::FnCall(callee, _) = &inner.node
&& let Expr::Ident(name) = &callee.node
{
step_fns.push(name.clone());
}
}
if step_fns.len() < 2 {
continue;
}
let step_count = step_fns.len();
out.push(ModulePattern::ResultPipelineChain {
scope: scope.clone(),
fn_name: fd.name.clone(),
step_count,
step_fns,
});
}
}
fn detect_wrapper_over_recursion(
scope: Option<String>,
fns: &[&crate::ast::FnDef],
out: &mut Vec<ModulePattern>,
) {
if fns.is_empty() {
return;
}
let mut recursive: HashSet<String> = HashSet::new();
for fd in fns {
if body_calls_name(&fd.body, &fd.name) {
recursive.insert(fd.name.clone());
}
}
if recursive.is_empty() {
return;
}
for fd in fns {
if recursive.contains(&fd.name) {
continue;
}
if fd.params.is_empty() {
continue;
}
let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
let mut hits: Vec<String> = Vec::new();
collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
hits.sort();
hits.dedup();
if hits.len() != 1 {
continue;
}
let inner = hits.into_iter().next().unwrap();
out.push(ModulePattern::WrapperOverRecursion {
wrapper_scope: scope.clone(),
wrapper_fn: fd.name.clone(),
inner_scope: scope.clone(),
inner_fn: inner,
});
}
}
fn call_target(
e: &crate::ast::Spanned<crate::ast::Expr>,
) -> Option<(&str, &[crate::ast::Spanned<crate::ast::Expr>])> {
use crate::ast::Expr;
match &e.node {
Expr::FnCall(callee, args) => match &callee.node {
Expr::Ident(n) => Some((n.as_str(), args.as_slice())),
_ => None,
},
Expr::TailCall(td) => Some((td.target.as_str(), td.args.as_slice())),
_ => None,
}
}
fn plain_ident(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<&str> {
match &e.node {
crate::ast::Expr::Ident(n) => Some(n.as_str()),
crate::ast::Expr::Resolved { name, .. } => Some(name.as_str()),
_ => None,
}
}
fn detect_accumulator_fold(
scope: Option<String>,
fns: &[&crate::ast::FnDef],
out: &mut Vec<ModulePattern>,
) {
use crate::ast::{Expr, Pattern, Stmt};
let mut recursive: HashSet<String> = HashSet::new();
for fd in fns {
if body_calls_name(&fd.body, &fd.name) {
recursive.insert(fd.name.clone());
}
}
if recursive.is_empty() {
return;
}
for fd in fns {
if recursive.contains(&fd.name) || fd.params.is_empty() {
continue;
}
let outer_params: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
let mut hits: Vec<String> = Vec::new();
collect_qualifying_inner_calls(&fd.body, &outer_params, &recursive, &mut hits);
hits.sort();
hits.dedup();
if hits.len() != 1 {
continue;
}
let loop_fn = hits.into_iter().next().unwrap();
let Some(lf) = fns.iter().find(|f| f.name == loop_fn) else {
continue;
};
if lf.params.len() != 2 {
continue;
}
let list_param = lf.params[0].0.clone();
let acc_param = lf.params[1].0.clone();
let Some(Stmt::Expr(body)) = lf.body.stmts().last() else {
continue;
};
let Expr::Match { subject, arms } = &body.node else {
continue;
};
if plain_ident(subject) != Some(list_param.as_str()) || arms.len() != 2 {
continue;
}
let list_shaped = arms
.iter()
.any(|a| matches!(a.pattern, Pattern::EmptyList | Pattern::Cons(_, _)));
if !list_shaped {
let mut base_seen = false;
let mut combine_fn: Option<String> = None;
let mut value_first = false;
let mut step_seen = false;
let mut ok = true;
for arm in arms {
let Pattern::Constructor(_, binders) = &arm.pattern else {
ok = false;
continue;
};
match binders.len() {
0 => {
if plain_ident(&arm.body) == Some(acc_param.as_str()) {
base_seen = true;
} else {
ok = false;
}
}
1 => {
let bind = &binders[0];
let Some((callee, cargs)) = call_target(&arm.body) else {
ok = false;
continue;
};
if callee != loop_fn
|| cargs.len() != 2
|| plain_ident(&cargs[0]) != Some(bind.as_str())
{
ok = false;
continue;
}
let Expr::FnCall(sc, sargs) = &cargs[1].node else {
ok = false;
continue;
};
let Expr::Ident(sname) = &sc.node else {
ok = false;
continue;
};
if sargs.len() != 2 {
ok = false;
continue;
}
let a0 = plain_ident(&sargs[0]);
let a1 = plain_ident(&sargs[1]);
if a0 == Some(list_param.as_str()) && a1 == Some(acc_param.as_str()) {
value_first = true;
} else if a0 == Some(acc_param.as_str()) && a1 == Some(list_param.as_str())
{
value_first = false;
} else {
ok = false;
continue;
}
combine_fn = Some(sname.clone());
step_seen = true;
}
_ => ok = false,
}
}
if base_seen && step_seen && ok && combine_fn.is_some() {
out.push(ModulePattern::AccumulatorFold {
scope: scope.clone(),
wrapper_fn: fd.name.clone(),
loop_fn,
driver_type: Some(lf.params[0].1.clone()),
list_param,
acc_param,
step_fn: combine_fn,
step_op: None,
finish_fn: None,
step_value_first: value_first,
});
}
continue;
}
let mut finish_fn: Option<Option<String>> = None; let mut step_fn: Option<String> = None;
let mut step_op: Option<crate::ast::BinOp> = None;
let mut step_seen = false;
let mut ok = true;
for arm in arms {
match &arm.pattern {
Pattern::EmptyList => {
if let Some((name, fargs)) = call_target(&arm.body) {
if fargs.len() == 1 && plain_ident(&fargs[0]) == Some(acc_param.as_str()) {
finish_fn = Some(Some(name.to_string()));
} else {
ok = false;
}
} else if plain_ident(&arm.body) == Some(acc_param.as_str()) {
finish_fn = Some(None);
} else {
ok = false;
}
}
Pattern::Cons(h, t) => {
let Some((callee, cargs)) = call_target(&arm.body) else {
ok = false;
continue;
};
if callee != loop_fn
|| cargs.len() != 2
|| plain_ident(&cargs[0]) != Some(t.as_str())
{
ok = false;
continue;
}
match &cargs[1].node {
Expr::FnCall(sc, sargs) => {
if let Expr::Ident(sname) = &sc.node
&& sargs.len() == 2
&& plain_ident(&sargs[0]) == Some(acc_param.as_str())
&& plain_ident(&sargs[1]) == Some(h.as_str())
{
step_fn = Some(sname.clone());
step_seen = true;
} else {
ok = false;
}
}
Expr::BinOp(op, l, r) => {
let ln = plain_ident(l);
let rn = plain_ident(r);
let acc_h = ln == Some(acc_param.as_str()) && rn == Some(h.as_str());
let h_acc = ln == Some(h.as_str()) && rn == Some(acc_param.as_str());
if acc_h || h_acc {
step_op = Some(*op);
step_seen = true;
} else {
ok = false;
}
}
_ => ok = false,
}
}
_ => ok = false,
}
}
let (Some(finish_fn), true, true) = (finish_fn, step_seen, ok) else {
continue;
};
out.push(ModulePattern::AccumulatorFold {
scope: scope.clone(),
wrapper_fn: fd.name.clone(),
loop_fn,
list_param,
acc_param,
step_fn,
step_op,
finish_fn,
driver_type: None,
step_value_first: false,
});
}
}
fn body_calls_name(body: &crate::ast::FnBody, name: &str) -> bool {
for stmt in body.stmts() {
let expr = match stmt {
crate::ast::Stmt::Binding(_, _, e) => e,
crate::ast::Stmt::Expr(e) => e,
};
if expr_calls_name(expr, name) {
return true;
}
}
false
}
fn expr_calls_name(expr: &crate::ast::Spanned<crate::ast::Expr>, name: &str) -> bool {
use crate::ast::Expr;
match &expr.node {
Expr::FnCall(callee, args) => {
if let Expr::Ident(n) = &callee.node
&& n == name
{
return true;
}
if expr_calls_name(callee, name) {
return true;
}
args.iter().any(|a| expr_calls_name(a, name))
}
Expr::TailCall(td) => td.target == name || td.args.iter().any(|a| expr_calls_name(a, name)),
Expr::Match { subject, arms } => {
if expr_calls_name(subject, name) {
return true;
}
arms.iter().any(|a| expr_calls_name(&a.body, name))
}
Expr::BinOp(_, l, r) => expr_calls_name(l, name) || expr_calls_name(r, name),
Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => expr_calls_name(e, name),
Expr::Constructor(_, Some(e)) => expr_calls_name(e, name),
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
xs.iter().any(|x| expr_calls_name(x, name))
}
Expr::MapLiteral(pairs) => pairs
.iter()
.any(|(k, v)| expr_calls_name(k, name) || expr_calls_name(v, name)),
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_name(e, name)),
Expr::RecordUpdate { base, updates, .. } => {
expr_calls_name(base, name) || updates.iter().any(|(_, e)| expr_calls_name(e, name))
}
Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
crate::ast::StrPart::Parsed(e) => expr_calls_name(e, name),
crate::ast::StrPart::Literal(_) => false,
}),
Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {
false
}
}
}
fn collect_qualifying_inner_calls(
body: &crate::ast::FnBody,
outer_params: &[&str],
recursive: &HashSet<String>,
out: &mut Vec<String>,
) {
for stmt in body.stmts() {
let expr = match stmt {
crate::ast::Stmt::Binding(_, _, e) => e,
crate::ast::Stmt::Expr(e) => e,
};
collect_qualifying_in_expr(expr, outer_params, recursive, out);
}
}
fn collect_qualifying_in_expr(
expr: &crate::ast::Spanned<crate::ast::Expr>,
outer_params: &[&str],
recursive: &HashSet<String>,
out: &mut Vec<String>,
) {
use crate::ast::Expr;
let try_qualify = |callee: &str, args: &[crate::ast::Spanned<Expr>], out: &mut Vec<String>| {
if !recursive.contains(callee) {
return;
}
if args.len() <= outer_params.len() {
return;
}
let mut arg_idents: HashSet<&str> = HashSet::new();
for a in args {
match &a.node {
Expr::Ident(n) => {
arg_idents.insert(n.as_str());
}
Expr::Resolved { name, .. } => {
arg_idents.insert(name.as_str());
}
_ => {}
}
}
if outer_params.iter().all(|p| arg_idents.contains(*p)) {
out.push(callee.to_string());
}
};
if let Expr::FnCall(callee, args) = &expr.node
&& let Expr::Ident(name) = &callee.node
{
try_qualify(name, args, out);
}
if let Expr::TailCall(td) = &expr.node {
try_qualify(&td.target, &td.args, out);
}
match &expr.node {
Expr::FnCall(callee, args) => {
collect_qualifying_in_expr(callee, outer_params, recursive, out);
for a in args {
collect_qualifying_in_expr(a, outer_params, recursive, out);
}
}
Expr::Match { subject, arms } => {
collect_qualifying_in_expr(subject, outer_params, recursive, out);
for a in arms {
collect_qualifying_in_expr(&a.body, outer_params, recursive, out);
}
}
Expr::BinOp(_, l, r) => {
collect_qualifying_in_expr(l, outer_params, recursive, out);
collect_qualifying_in_expr(r, outer_params, recursive, out);
}
Expr::Neg(e) | Expr::Attr(e, _) | Expr::ErrorProp(e) => {
collect_qualifying_in_expr(e, outer_params, recursive, out);
}
Expr::Constructor(_, Some(e)) => {
collect_qualifying_in_expr(e, outer_params, recursive, out);
}
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
for x in xs {
collect_qualifying_in_expr(x, outer_params, recursive, out);
}
}
Expr::MapLiteral(pairs) => {
for (k, v) in pairs {
collect_qualifying_in_expr(k, outer_params, recursive, out);
collect_qualifying_in_expr(v, outer_params, recursive, out);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, e) in fields {
collect_qualifying_in_expr(e, outer_params, recursive, out);
}
}
Expr::RecordUpdate { base, updates, .. } => {
collect_qualifying_in_expr(base, outer_params, recursive, out);
for (_, e) in updates {
collect_qualifying_in_expr(e, outer_params, recursive, out);
}
}
Expr::InterpolatedStr(parts) => {
for p in parts {
if let crate::ast::StrPart::Parsed(e) = p {
collect_qualifying_in_expr(e, outer_params, recursive, out);
}
}
}
Expr::TailCall(td) => {
for a in &td.args {
collect_qualifying_in_expr(a, outer_params, recursive, out);
}
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Constructor(_, None) | Expr::Resolved { .. } => {}
}
}