use crate::ast::{Expr, FnBody, FnDef, Pattern, Spanned, Stmt, TailCallData, TypeDef, VerifyLaw};
use crate::codegen::CodegenContext;
#[derive(Clone, Debug)]
pub(crate) struct PeanoType {
pub type_name: String,
pub base_ctor: String,
pub succ_ctor: String,
}
pub(crate) fn detect_canonical_peano(td: &TypeDef) -> Option<PeanoType> {
let TypeDef::Sum { name, variants, .. } = td else {
return None;
};
if variants.len() != 2 {
return None;
}
let mut base: Option<String> = None;
let mut succ: Option<String> = None;
for v in variants {
match v.fields.len() {
0 => {
if base.replace(v.name.clone()).is_some() {
return None; }
}
1 if v.fields[0].trim() == name => {
if succ.replace(v.name.clone()).is_some() {
return None; }
}
_ => return None,
}
}
Some(PeanoType {
type_name: name.clone(),
base_ctor: base?,
succ_ctor: succ?,
})
}
pub(crate) fn collect_peano_types(ctx: &CodegenContext) -> Vec<PeanoType> {
ctx.type_defs
.iter()
.chain(ctx.modules.iter().flat_map(|m| m.type_defs.iter()))
.filter_map(detect_canonical_peano)
.collect()
}
pub(crate) enum PeanoCtor {
Zero,
Succ,
}
pub(crate) fn peano_type_named(ctx: &CodegenContext, type_name: &str) -> Option<PeanoType> {
collect_peano_types(ctx)
.into_iter()
.find(|p| p.type_name == type_name)
}
pub(crate) fn peano_ctor_role(
ctx: &CodegenContext,
type_name: &str,
ctor_short: &str,
) -> Option<PeanoCtor> {
let p = peano_type_named(ctx, type_name)?;
if ctor_short == p.base_ctor {
Some(PeanoCtor::Zero)
} else if ctor_short == p.succ_ctor {
Some(PeanoCtor::Succ)
} else {
None
}
}
pub(crate) fn collect_called_fns(
expr: &Spanned<Expr>,
out: &mut std::collections::BTreeSet<String>,
) {
match &expr.node {
Expr::FnCall(f, args) => {
if let Some(name) = crate::codegen::common::expr_to_dotted_name(&f.node) {
if !name.contains('.') {
out.insert(name);
}
}
collect_called_fns(f, out);
for a in args {
collect_called_fns(a, out);
}
}
Expr::BinOp(_, l, r) => {
collect_called_fns(l, out);
collect_called_fns(r, out);
}
Expr::Match { subject, arms, .. } => {
collect_called_fns(subject, out);
for arm in arms {
collect_called_fns(&arm.body, out);
}
}
Expr::ErrorProp(inner) => collect_called_fns(inner, out),
Expr::Constructor(_, Some(arg)) => collect_called_fns(arg, out),
Expr::RecordCreate { fields, .. } => {
for (_, e) in fields {
collect_called_fns(e, out);
}
}
Expr::List(elems) => {
for e in elems {
collect_called_fns(e, out);
}
}
Expr::TailCall(tc) => {
let TailCallData { target, args, .. } = tc.as_ref();
if !target.contains('.') {
out.insert(target.clone());
}
for a in args {
collect_called_fns(a, out);
}
}
Expr::Tuple(elems) | Expr::IndependentProduct(elems, _) => {
for e in elems {
collect_called_fns(e, out);
}
}
Expr::Attr(obj, _) => collect_called_fns(obj, out),
Expr::Neg(inner) => collect_called_fns(inner, out),
_ => {}
}
}
pub(crate) fn collect_called_fns_in_body(
body: &FnBody,
out: &mut std::collections::BTreeSet<String>,
) {
match body {
FnBody::Block(stmts) => {
for stmt in stmts {
match stmt {
Stmt::Binding(_, _, expr) => collect_called_fns(expr, out),
Stmt::Expr(expr) => collect_called_fns(expr, out),
}
}
}
}
}
pub(crate) fn short_ctor(name: &str) -> &str {
name.rsplit('.').next().unwrap_or(name)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum NatArithKind {
Add,
Sub,
Mul,
}
#[derive(Clone, Debug)]
pub(crate) struct NatArithOp {
pub fn_name: String,
pub kind: NatArithKind,
}
fn call_or_ctor(e: &Spanned<Expr>) -> Option<(String, Vec<&Spanned<Expr>>)> {
match &e.node {
Expr::FnCall(callee, args) => {
let name = crate::codegen::common::expr_to_dotted_name(&callee.node)?;
Some((short_ctor(&name).to_string(), args.iter().collect()))
}
Expr::Constructor(name, arg) => Some((
short_ctor(name).to_string(),
arg.iter().map(|b| b.as_ref()).collect(),
)),
Expr::Attr(..) => crate::codegen::common::expr_to_dotted_name(&e.node)
.map(|name| (short_ctor(&name).to_string(), Vec::new())),
Expr::TailCall(tc) => Some((short_ctor(&tc.target).to_string(), tc.args.iter().collect())),
_ => None,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum BinaryLawShape {
Commutativity,
Associativity,
}
pub(crate) fn recognize_binary_law_shape(
law: &VerifyLaw,
fn_name: &str,
given_names: &[String],
) -> Option<BinaryLawShape> {
if law.when.is_some() {
return None;
}
let as_self_call = |e: &Spanned<Expr>| -> Option<(Spanned<Expr>, Spanned<Expr>)> {
let (callee, args) = match &e.node {
Expr::FnCall(c, a) => (crate::codegen::common::expr_to_dotted_name(&c.node)?, a),
_ => return None,
};
if short_ctor(&callee) != short_ctor(fn_name) || args.len() != 2 {
return None;
}
Some((args[0].clone(), args[1].clone()))
};
let given_name_of = |e: &Spanned<Expr>| -> Option<String> {
crate::codegen::recursion::detect::local_name_of(e)
.filter(|id| given_names.iter().any(|g| g == id))
.map(str::to_string)
};
if given_names.len() == 2
&& let Some((la, lb)) = as_self_call(&law.lhs)
&& let Some((ra, rb)) = as_self_call(&law.rhs)
&& let (Some(la), Some(lb), Some(ra), Some(rb)) = (
given_name_of(&la),
given_name_of(&lb),
given_name_of(&ra),
given_name_of(&rb),
)
&& la != lb
&& la == rb
&& lb == ra
{
return Some(BinaryLawShape::Commutativity);
}
if given_names.len() == 3
&& let Some((l_inner, lc)) = as_self_call(&law.lhs)
&& let Some((la_inner, lb_inner)) = as_self_call(&l_inner)
&& let Some((ra, r_inner)) = as_self_call(&law.rhs)
&& let Some((rb_inner, rc_inner)) = as_self_call(&r_inner)
&& let (Some(la), Some(lb), Some(lc), Some(ra), Some(rb), Some(rc)) = (
given_name_of(&la_inner),
given_name_of(&lb_inner),
given_name_of(&lc),
given_name_of(&ra),
given_name_of(&rb_inner),
given_name_of(&rc_inner),
)
&& la == ra
&& lb == rb
&& lc == rc
&& la != lb
&& lb != lc
&& la != lc
{
return Some(BinaryLawShape::Associativity);
}
None
}
type PeanoSplit<'a> = (
&'a str,
&'a str,
&'a Spanned<Expr>,
&'a str,
&'a Spanned<Expr>,
);
fn peano_outer_split<'a>(fd: &'a FnDef, peano: &PeanoType) -> Option<PeanoSplit<'a>> {
if fd.params.len() != 2 {
return None;
}
let (p0, t0) = &fd.params[0];
let (p1, t1) = &fd.params[1];
if t0 != t1 || &fd.return_type != t0 || t0.trim() != peano.type_name {
return None;
}
let ln = crate::codegen::recursion::detect::local_name_of;
let tail = fd.body.tail_expr()?;
let Expr::Match { subject, arms, .. } = &tail.node else {
return None;
};
if ln(subject) != Some(p0.as_str()) || arms.len() != 2 {
return None;
}
let mut base_body: Option<&Spanned<Expr>> = None;
let mut succ_q: Option<&String> = None;
let mut succ_body: Option<&Spanned<Expr>> = None;
for arm in arms {
let Pattern::Constructor(cname, binders) = &arm.pattern else {
return None;
};
let short = short_ctor(cname);
if short == peano.base_ctor && binders.is_empty() {
base_body = Some(&arm.body);
} else if short == peano.succ_ctor && binders.len() == 1 {
succ_q = Some(&binders[0]);
succ_body = Some(&arm.body);
} else {
return None;
}
}
Some((
p0.as_str(),
p1.as_str(),
base_body?,
succ_q?.as_str(),
succ_body?,
))
}
pub(crate) fn is_canonical_add(fd: &FnDef, peano: &PeanoType) -> bool {
let Some((_p0, p1, base_body, q, succ_body)) = peano_outer_split(fd, peano) else {
return false;
};
let ln = crate::codegen::recursion::detect::local_name_of;
let add_succ_ok = call_or_ctor(succ_body).is_some_and(|(c, a)| {
c == peano.succ_ctor
&& a.len() == 1
&& call_or_ctor(a[0]).is_some_and(|(rc, ra)| {
rc == fd.name && ra.len() == 2 && ln(ra[0]) == Some(q) && ln(ra[1]) == Some(p1)
})
});
ln(base_body) == Some(p1) && add_succ_ok
}
pub(crate) fn both_args_peeling_is_commutative(fd: &FnDef, ctx: &CodegenContext) -> Option<()> {
if !crate::codegen::recursion::detect::recurses_decrementing_both_args(fd) {
return None;
}
let peano = peano_type_named(ctx, &fd.params.first()?.1)?;
let (p0, p1, base_body, q, succ_body) = peano_outer_split(fd, &peano)?;
let ln = crate::codegen::recursion::detect::local_name_of;
let is_base = |e: &Spanned<Expr>| {
call_or_ctor(e).is_some_and(|(c, a)| c == peano.base_ctor && a.is_empty())
};
let Expr::Match {
subject: inner_subj,
arms: inner_arms,
..
} = &succ_body.node
else {
return None;
};
if ln(inner_subj) != Some(p1) {
return None;
}
let (inner_base, w, inner_succ) = split_peano_match(inner_arms, &peano)?;
let rec_ok = call_or_ctor(inner_succ).is_some_and(|(c, a)| {
c == peano.succ_ctor
&& a.len() == 1
&& call_or_ctor(a[0]).is_some_and(|(rc, ra)| {
rc == fd.name && ra.len() == 2 && ln(ra[0]) == Some(q) && ln(ra[1]) == Some(w)
})
});
if !rec_ok {
return None;
}
let max_shape = ln(base_body) == Some(p1) && ln(inner_base) == Some(p0);
let min_shape = is_base(base_body) && is_base(inner_base);
(max_shape || min_shape).then_some(())
}
fn detect_nat_arith_op(fd: &FnDef, ctx: &CodegenContext) -> Option<NatArithKind> {
let peano = peano_type_named(ctx, &fd.params.first()?.1)?;
if is_canonical_add(fd, &peano) {
return Some(NatArithKind::Add);
}
let (p0, p1, base_body, q, succ_body) = peano_outer_split(fd, &peano)?;
let ln = crate::codegen::recursion::detect::local_name_of;
let base_is_base =
call_or_ctor(base_body).is_some_and(|(c, a)| c == peano.base_ctor && a.is_empty());
if base_is_base
&& let Expr::Match {
subject: inner_subj,
arms: inner_arms,
..
} = &succ_body.node
&& ln(inner_subj) == Some(p1)
&& inner_arms.len() == 2
{
let mut inner_base_ok = false;
let mut inner_succ_ok = false;
for arm in inner_arms {
let Pattern::Constructor(cname, binders) = &arm.pattern else {
return None;
};
let short = short_ctor(cname);
if short == peano.base_ctor && binders.is_empty() {
inner_base_ok = ln(&arm.body) == Some(p0);
} else if short == peano.succ_ctor && binders.len() == 1 {
let r = binders[0].as_str();
inner_succ_ok = call_or_ctor(&arm.body).is_some_and(|(rc, ra)| {
rc == fd.name && ra.len() == 2 && ln(ra[0]) == Some(q) && ln(ra[1]) == Some(r)
});
} else {
return None;
}
}
if inner_base_ok && inner_succ_ok {
return Some(NatArithKind::Sub);
}
}
if base_is_base
&& let Some((add_fn, args)) = call_or_ctor(succ_body)
&& args.len() == 2
&& ln(args[0]) == Some(p1)
&& call_or_ctor(args[1]).is_some_and(|(rc, ra)| {
rc == fd.name && ra.len() == 2 && ln(ra[0]) == Some(q) && ln(ra[1]) == Some(p1)
})
&& ctx
.fn_def_by_name(&add_fn, ctx.active_module_scope().as_deref())
.is_some_and(|afd| afd.name != fd.name && is_canonical_add(afd, &peano))
{
return Some(NatArithKind::Mul);
}
None
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum NatCompareKind {
Le,
Lt,
Eq,
}
impl NatCompareKind {
pub(crate) fn prop_op(self) -> &'static str {
match self {
NatCompareKind::Le => "≤",
NatCompareKind::Lt => "<",
NatCompareKind::Eq => "=",
}
}
pub(crate) fn false_prop(self) -> &'static str {
match self {
NatCompareKind::Le => "b < a",
NatCompareKind::Lt => "b ≤ a",
NatCompareKind::Eq => "a ≠ b",
}
}
pub(crate) fn bridge_suffix(self) -> &'static str {
match self {
NatCompareKind::Le => "isNatLe",
NatCompareKind::Lt => "isNatLt",
NatCompareKind::Eq => "isNatEq",
}
}
pub(crate) fn induct_on_second(self) -> bool {
matches!(self, NatCompareKind::Lt)
}
}
#[derive(Clone, Debug)]
pub(crate) struct NatCompareOp {
pub fn_name: String,
pub kind: NatCompareKind,
}
fn split_peano_match<'a>(
arms: &'a [crate::ast::MatchArm],
peano: &PeanoType,
) -> Option<(&'a Spanned<Expr>, &'a str, &'a Spanned<Expr>)> {
if arms.len() != 2 {
return None;
}
let mut base: Option<&Spanned<Expr>> = None;
let mut succ_q: Option<&str> = None;
let mut succ_b: Option<&Spanned<Expr>> = None;
for arm in arms {
let Pattern::Constructor(cname, binders) = &arm.pattern else {
return None;
};
let short = short_ctor(cname);
if short == peano.base_ctor && binders.is_empty() {
base = Some(&arm.body);
} else if short == peano.succ_ctor && binders.len() == 1 {
succ_q = Some(binders[0].as_str());
succ_b = Some(&arm.body);
} else {
return None;
}
}
Some((base?, succ_q?, succ_b?))
}
fn as_bool_lit(e: &Spanned<Expr>) -> Option<bool> {
match &e.node {
Expr::Literal(crate::ast::Literal::Bool(b)) => Some(*b),
_ => None,
}
}
fn detect_nat_compare_op(fd: &FnDef, ctx: &CodegenContext) -> Option<NatCompareKind> {
if fd.params.len() != 2 || fd.return_type.trim() != "Bool" {
return None;
}
let (p0, t0) = &fd.params[0];
let (p1, t1) = &fd.params[1];
if t0 != t1 {
return None;
}
let peano = peano_type_named(ctx, t0)?;
let ln = crate::codegen::recursion::detect::local_name_of;
let tail = fd.body.tail_expr()?;
let Expr::Match { subject, arms, .. } = &tail.node else {
return None;
};
let outer_on = ln(subject)?;
let (base_body, q, succ_body) = split_peano_match(arms, &peano)?;
let Expr::Match {
subject: inner_subj,
arms: inner_arms,
..
} = &succ_body.node
else {
return None;
};
let inner_on = ln(inner_subj)?;
let (inner_base, r, inner_succ) = split_peano_match(inner_arms, &peano)?;
let rec_ok = |first: &str, second: &str| {
call_or_ctor(inner_succ).is_some_and(|(rc, ra)| {
rc == fd.name && ra.len() == 2 && ln(ra[0]) == Some(first) && ln(ra[1]) == Some(second)
})
};
if outer_on == p0.as_str()
&& inner_on == p1.as_str()
&& as_bool_lit(base_body) == Some(true)
&& as_bool_lit(inner_base) == Some(false)
&& rec_ok(q, r)
{
return Some(NatCompareKind::Le);
}
if outer_on == p1.as_str()
&& inner_on == p0.as_str()
&& as_bool_lit(base_body) == Some(false)
&& as_bool_lit(inner_base) == Some(true)
&& rec_ok(r, q)
{
return Some(NatCompareKind::Lt);
}
if outer_on == p0.as_str()
&& inner_on == p1.as_str()
&& as_bool_lit(inner_base) == Some(false)
&& rec_ok(q, r)
&& let Expr::Match {
subject: base_subj,
arms: base_arms,
..
} = &base_body.node
&& ln(base_subj) == Some(p1.as_str())
&& let Some((bb_base, _z, bb_succ)) = split_peano_match(base_arms, &peano)
&& as_bool_lit(bb_base) == Some(true)
&& as_bool_lit(bb_succ) == Some(false)
{
return Some(NatCompareKind::Eq);
}
None
}
fn law_called_fn_names(
law: &VerifyLaw,
ctx: &CodegenContext,
) -> std::collections::BTreeSet<String> {
let mut names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
collect_called_fns(&law.lhs, &mut names);
collect_called_fns(&law.rhs, &mut names);
let mut transitive: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for f in &names {
if let Some(fd) = ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref()) {
collect_called_fns_in_body(&fd.body, &mut transitive);
}
}
names.extend(transitive);
names
}
pub(crate) fn collect_nat_compare_ops_in_law(
law: &VerifyLaw,
ctx: &CodegenContext,
) -> Vec<NatCompareOp> {
collect_nat_compare_ops_for_names(&law_called_fn_names(law, ctx), ctx)
}
pub(crate) fn collect_nat_compare_ops_for_names(
names: &std::collections::BTreeSet<String>,
ctx: &CodegenContext,
) -> Vec<NatCompareOp> {
let mut seen = std::collections::BTreeSet::new();
names
.iter()
.filter_map(|f| ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref()))
.filter_map(|fd| detect_nat_compare_op(fd, ctx).map(|kind| (fd, kind)))
.filter(|(fd, _)| seen.insert(fd.name.clone()))
.map(|(fd, kind)| NatCompareOp {
fn_name: fd.name.clone(),
kind,
})
.collect()
}
pub(crate) fn collect_nat_arith_ops_in_law(
law: &VerifyLaw,
ctx: &CodegenContext,
) -> Vec<NatArithOp> {
collect_nat_arith_ops_for_names(&law_called_fn_names(law, ctx), ctx)
}
pub(crate) fn collect_nat_arith_ops_for_names(
names: &std::collections::BTreeSet<String>,
ctx: &CodegenContext,
) -> Vec<NatArithOp> {
let mut seen = std::collections::BTreeSet::new();
names
.iter()
.filter_map(|f| ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref()))
.filter_map(|fd| detect_nat_arith_op(fd, ctx).map(|kind| (fd, kind)))
.filter(|(fd, _)| seen.insert(fd.name.clone()))
.map(|(fd, kind)| NatArithOp {
fn_name: fd.name.clone(),
kind,
})
.collect()
}
#[derive(Clone, Debug)]
pub(crate) struct StringPosScanShape {
pub predicate_fn: String,
pub param_pins: Vec<Option<bool>>,
pub exit_expr: Spanned<Expr>,
}
const SCAN_TEMPLATE_RESERVED: &[&str] = &[
"fuel",
"ih",
"h0",
"h1",
"h2",
"h3",
"hch",
"hdrop",
"hdig",
"hstep",
"hrec",
"hlt",
"hpos",
"ch",
"AVERSCANLEN",
];
fn ident_name(e: &Spanned<Expr>) -> Option<&str> {
match &e.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.as_str()),
_ => None,
}
}
pub(crate) fn detect_string_pos_scan(fd: &FnDef) -> Option<StringPosScanShape> {
let dotted = |e: &Spanned<Expr>| crate::codegen::common::expr_to_dotted_name(&e.node);
if fd.params.len() < 2 || fd.params[0].1 != "String" || fd.params[1].1 != "Int" {
return None;
}
if fd
.params
.iter()
.any(|(n, _)| SCAN_TEMPLATE_RESERVED.contains(&n.as_str()))
{
return None;
}
let s_name = fd.params[0].0.as_str();
let pos_name = fd.params[1].0.as_str();
let [Stmt::Expr(body)] = fd.body.stmts() else {
return None;
};
let Expr::Match { subject, arms } = &body.node else {
return None;
};
let Expr::FnCall(callee, args) = &subject.node else {
return None;
};
if dotted(callee).as_deref() != Some("String.charAt") || args.len() != 2 {
return None;
}
if ident_name(&args[0]) != Some(s_name) || ident_name(&args[1]) != Some(pos_name) {
return None;
}
if arms.len() != 2 {
return None;
}
let none_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.None" && b.is_empty()),
)?;
let some_arm = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Constructor(n, b) if n == "Option.Some" && b.len() == 1),
)?;
let Pattern::Constructor(_, some_binders) = &some_arm.pattern else {
return None;
};
let c_name = some_binders[0].as_str();
let Expr::Match {
subject: pred_subject,
arms: pred_arms,
} = &some_arm.body.node
else {
return None;
};
let Expr::FnCall(pred_callee, pred_args) = &pred_subject.node else {
return None;
};
let predicate_fn = dotted(pred_callee)?;
if predicate_fn.contains('.') {
return None; }
if SCAN_TEMPLATE_RESERVED.contains(&predicate_fn.as_str()) {
return None;
}
let fuel_wrapper = format!("{}__fuel", fd.name);
if fd
.params
.iter()
.any(|(n, _)| *n == predicate_fn || *n == fuel_wrapper)
{
return None;
}
if pred_args.len() != 1 || ident_name(&pred_args[0]) != Some(c_name) {
return None;
}
if pred_arms.len() != 2 {
return None;
}
let true_arm = pred_arms.iter().find(|a| {
matches!(
&a.pattern,
Pattern::Literal(crate::ast::Literal::Bool(true))
)
})?;
pred_arms.iter().find(|a| {
matches!(
&a.pattern,
Pattern::Literal(crate::ast::Literal::Bool(false))
)
})?;
if count_calls_to(body, &fd.name) != 1 || count_calls_to(&true_arm.body, &fd.name) != 1 {
return None;
}
let self_args = find_self_call_args(&true_arm.body, &fd.name)?;
if self_args.len() != fd.params.len() {
return None;
}
if ident_name(&self_args[0]) != Some(s_name) {
return None;
}
let Expr::BinOp(crate::ast::BinOp::Add, l, r) = &self_args[1].node else {
return None;
};
if ident_name(l) != Some(pos_name)
|| !matches!(&r.node, Expr::Literal(crate::ast::Literal::Int(1)))
{
return None;
}
let mut param_pins: Vec<Option<bool>> = Vec::new();
for (i, arg) in self_args.iter().enumerate().skip(2) {
match (&arg.node, ident_name(arg)) {
(_, Some(n)) if n == fd.params[i].0 => param_pins.push(None),
(Expr::Literal(crate::ast::Literal::Bool(b)), _) => param_pins.push(Some(*b)),
_ => return None,
}
}
if !reduces_to_self_call(&true_arm.body, fd, ¶m_pins) {
return None;
}
let allowed: Vec<&str> = fd.params.iter().map(|(n, _)| n.as_str()).collect();
if !exit_expr_substitution_safe(&none_arm.body, &allowed) {
return None;
}
if !expr_mentions_ident(&none_arm.body, pos_name) {
return None;
}
Some(StringPosScanShape {
predicate_fn,
param_pins,
exit_expr: (*none_arm.body).clone(),
})
}
pub(crate) fn scan_predicate_fn_ok(fd: &FnDef) -> bool {
fd.effects.is_empty()
&& fd.params.len() == 1
&& fd.params[0].1 == "String"
&& fd.return_type == "Bool"
}
fn count_calls_to(expr: &Spanned<Expr>, target: &str) -> usize {
let mut count = 0;
walk_calls(expr, &mut |name| {
if name == target {
count += 1;
}
});
count
}
fn walk_calls(expr: &Spanned<Expr>, on_call: &mut impl FnMut(&str)) {
match &expr.node {
Expr::FnCall(callee, args) => {
if let Some(name) = crate::codegen::common::expr_to_dotted_name(&callee.node) {
on_call(&name);
}
walk_calls(callee, on_call);
for a in args {
walk_calls(a, on_call);
}
}
Expr::TailCall(data) => {
on_call(&data.target);
for a in &data.args {
walk_calls(a, on_call);
}
}
Expr::BinOp(_, l, r) => {
walk_calls(l, on_call);
walk_calls(r, on_call);
}
Expr::Neg(inner) | Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
walk_calls(inner, on_call);
}
Expr::Match { subject, arms } => {
walk_calls(subject, on_call);
for arm in arms {
walk_calls(&arm.body, on_call);
}
}
Expr::Constructor(_, Some(inner)) => walk_calls(inner, on_call),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
for i in items {
walk_calls(i, on_call);
}
}
Expr::MapLiteral(pairs) => {
for (k, v) in pairs {
walk_calls(k, on_call);
walk_calls(v, on_call);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, v) in fields {
walk_calls(v, on_call);
}
}
Expr::RecordUpdate { base, updates, .. } => {
walk_calls(base, on_call);
for (_, v) in updates {
walk_calls(v, on_call);
}
}
Expr::InterpolatedStr(parts) => {
for p in parts {
if let crate::ast::StrPart::Parsed(inner) = p {
walk_calls(inner, on_call);
}
}
}
_ => {}
}
}
fn find_self_call_args<'a>(expr: &'a Spanned<Expr>, target: &str) -> Option<&'a [Spanned<Expr>]> {
match &expr.node {
Expr::FnCall(callee, args)
if crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref()
== Some(target) =>
{
Some(args)
}
Expr::TailCall(data) if data.target == target => Some(&data.args),
Expr::Match { arms, .. } => arms
.iter()
.find_map(|arm| find_self_call_args(&arm.body, target)),
_ => None,
}
}
fn reduces_to_self_call(expr: &Spanned<Expr>, fd: &FnDef, pins: &[Option<bool>]) -> bool {
match &expr.node {
Expr::FnCall(callee, _)
if crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref()
== Some(fd.name.as_str()) =>
{
true
}
Expr::TailCall(data) if data.target == fd.name => true,
Expr::Match { subject, arms } => {
let Some(subj) = ident_name(subject) else {
return false;
};
let Some(idx) = fd.params.iter().position(|(n, _)| n.as_str() == subj) else {
return false;
};
if idx < 2 {
return false;
}
let Some(Some(pin)) = pins.get(idx - 2) else {
return false;
};
if arms.len() != 2 {
return false;
}
let selected = arms.iter().find(
|a| matches!(&a.pattern, Pattern::Literal(crate::ast::Literal::Bool(b)) if b == pin),
);
let other = arms.iter().any(
|a| matches!(&a.pattern, Pattern::Literal(crate::ast::Literal::Bool(b)) if b != pin),
);
match selected {
Some(arm) if other => reduces_to_self_call(&arm.body, fd, pins),
_ => false,
}
}
_ => false,
}
}
fn expr_mentions_ident(expr: &Spanned<Expr>, name: &str) -> bool {
match &expr.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => n == name,
Expr::Neg(inner) | Expr::ErrorProp(inner) => expr_mentions_ident(inner, name),
Expr::BinOp(_, l, r) => expr_mentions_ident(l, name) || expr_mentions_ident(r, name),
Expr::FnCall(callee, args) => {
expr_mentions_ident(callee, name) || args.iter().any(|a| expr_mentions_ident(a, name))
}
Expr::Constructor(_, payload) => payload
.as_deref()
.is_some_and(|p| expr_mentions_ident(p, name)),
Expr::Tuple(items) | Expr::List(items) => {
items.iter().any(|i| expr_mentions_ident(i, name))
}
_ => false,
}
}
fn exit_expr_substitution_safe(expr: &Spanned<Expr>, allowed: &[&str]) -> bool {
match &expr.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => allowed.contains(&n.as_str()),
Expr::Literal(_) => true,
Expr::Neg(inner) | Expr::ErrorProp(inner) => exit_expr_substitution_safe(inner, allowed),
Expr::BinOp(_, l, r) => {
exit_expr_substitution_safe(l, allowed) && exit_expr_substitution_safe(r, allowed)
}
Expr::FnCall(callee, args) => {
let Some(name) = crate::codegen::common::expr_to_dotted_name(&callee.node) else {
return false;
};
!SCAN_TEMPLATE_RESERVED.contains(&name.as_str())
&& args.iter().all(|a| exit_expr_substitution_safe(a, allowed))
}
Expr::Constructor(_, payload) => payload
.as_deref()
.is_none_or(|p| exit_expr_substitution_safe(p, allowed)),
Expr::Tuple(items) | Expr::List(items) => items
.iter()
.all(|i| exit_expr_substitution_safe(i, allowed)),
_ => false,
}
}
pub(crate) fn substitute_idents_in_expr(
expr: &Spanned<Expr>,
subst: &std::collections::HashMap<String, Expr>,
) -> Spanned<Expr> {
let node = match &expr.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => match subst.get(n) {
Some(replacement) => replacement.clone(),
None => expr.node.clone(),
},
Expr::Neg(inner) => Expr::Neg(Box::new(substitute_idents_in_expr(inner, subst))),
Expr::ErrorProp(inner) => {
Expr::ErrorProp(Box::new(substitute_idents_in_expr(inner, subst)))
}
Expr::BinOp(op, l, r) => Expr::BinOp(
*op,
Box::new(substitute_idents_in_expr(l, subst)),
Box::new(substitute_idents_in_expr(r, subst)),
),
Expr::FnCall(callee, args) => Expr::FnCall(
callee.clone(),
args.iter()
.map(|a| substitute_idents_in_expr(a, subst))
.collect(),
),
Expr::Constructor(name, payload) => Expr::Constructor(
name.clone(),
payload
.as_ref()
.map(|p| Box::new(substitute_idents_in_expr(p, subst))),
),
Expr::Tuple(items) => Expr::Tuple(
items
.iter()
.map(|i| substitute_idents_in_expr(i, subst))
.collect(),
),
Expr::List(items) => Expr::List(
items
.iter()
.map(|i| substitute_idents_in_expr(i, subst))
.collect(),
),
other => other.clone(),
};
Spanned {
node,
line: expr.line,
ty: std::sync::OnceLock::new(),
}
}