use std::collections::{HashMap, HashSet};
use crate::arena::Arena;
use crate::ast::stmt::{BinaryOpKind, Block, Expr, Literal, Stmt, TypeExpr};
use crate::intern::{Interner, Symbol};
const DEFAULT_DEPTH: usize = 8;
const DEFAULT_RUN_DEPTH: usize = 4;
const RUN_LOOP_DEPTH_CAP: usize = 2;
const DEFAULT_BUDGET: usize = 20_000;
struct RecCand<'a> {
params: Vec<Symbol>,
body: Block<'a>,
loop_interleaved: bool,
}
fn expr_ok(e: &Expr) -> bool {
match e {
Expr::Literal(_) | Expr::Identifier(_) => true,
Expr::BinaryOp { left, right, .. } => expr_ok(left) && expr_ok(right),
Expr::Not { operand } => expr_ok(operand),
Expr::Index { collection, index } => expr_ok(collection) && expr_ok(index),
Expr::Length { collection } => expr_ok(collection),
Expr::Call { args, .. } => args.iter().all(|a| expr_ok(a)),
_ => false,
}
}
fn stmt_ok(s: &Stmt) -> bool {
match s {
Stmt::Let { value, .. } => expr_ok(value),
Stmt::Set { value, .. } => expr_ok(value),
Stmt::Return { value } => value.map_or(true, |e| expr_ok(e)),
Stmt::If { cond, then_block, else_block } => {
expr_ok(cond)
&& then_block.iter().all(stmt_ok)
&& else_block.map_or(true, |b| b.iter().all(stmt_ok))
}
Stmt::While { cond, body, decreasing } => {
expr_ok(cond)
&& decreasing.map_or(true, |d| expr_ok(d))
&& body.iter().all(stmt_ok)
&& !block_has_return(body)
}
_ => false,
}
}
fn block_has_return(block: Block) -> bool {
block.iter().any(|s| match s {
Stmt::Return { .. } => true,
Stmt::If { then_block, else_block, .. } => {
block_has_return(then_block) || else_block.map_or(false, block_has_return)
}
Stmt::While { body, .. } => block_has_return(body),
_ => false,
})
}
fn definitely_returns(block: Block) -> bool {
match block.last() {
Some(Stmt::Return { .. }) => true,
Some(Stmt::If { then_block, else_block: Some(e), .. }) => {
definitely_returns(then_block) && definitely_returns(e)
}
_ => false,
}
}
fn returns_well_placed(block: Block) -> bool {
let n = block.len();
for (i, s) in block.iter().enumerate() {
let last = i + 1 == n;
match s {
Stmt::Return { .. } => {
if !last {
return false;
}
}
Stmt::If { then_block, else_block, .. } => {
if last {
if !returns_well_placed(then_block) {
return false;
}
if let Some(e) = else_block {
if !returns_well_placed(e) {
return false;
}
}
} else {
let guard = else_block.is_none() && definitely_returns(then_block);
if guard {
if !returns_well_placed(then_block) {
return false;
}
} else {
if block_has_return(then_block) {
return false;
}
if let Some(e) = else_block {
if block_has_return(e) {
return false;
}
}
}
}
}
Stmt::While { body, .. } => {
if block_has_return(body) {
return false;
}
}
_ => {}
}
}
true
}
fn has_self_call(block: Block, fname: Symbol) -> bool {
fn in_expr(e: &Expr, fname: Symbol) -> bool {
match e {
Expr::Call { function, args } => {
*function == fname || args.iter().any(|a| in_expr(a, fname))
}
Expr::BinaryOp { left, right, .. } => in_expr(left, fname) || in_expr(right, fname),
Expr::Not { operand } => in_expr(operand, fname),
Expr::Index { collection, index } => {
in_expr(collection, fname) || in_expr(index, fname)
}
Expr::Length { collection } => in_expr(collection, fname),
_ => false,
}
}
block.iter().any(|s| match s {
Stmt::Let { value, .. } | Stmt::Set { value, .. } => in_expr(value, fname),
Stmt::Return { value } => value.map_or(false, |e| in_expr(e, fname)),
Stmt::If { cond, then_block, else_block } => {
in_expr(cond, fname)
|| has_self_call(then_block, fname)
|| else_block.map_or(false, |b| has_self_call(b, fname))
}
Stmt::While { cond, body, .. } => in_expr(cond, fname) || has_self_call(body, fname),
_ => false,
})
}
fn count_self_calls(block: Block, fname: Symbol) -> usize {
fn in_expr(e: &Expr, f: Symbol) -> usize {
match e {
Expr::Call { function, args } => {
(if *function == f { 1 } else { 0 })
+ args.iter().map(|a| in_expr(a, f)).sum::<usize>()
}
Expr::BinaryOp { left, right, .. } => in_expr(left, f) + in_expr(right, f),
Expr::Not { operand } => in_expr(operand, f),
Expr::Index { collection, index } => in_expr(collection, f) + in_expr(index, f),
Expr::Length { collection } => in_expr(collection, f),
_ => 0,
}
}
block
.iter()
.map(|s| match s {
Stmt::Let { value, .. } | Stmt::Set { value, .. } => in_expr(value, fname),
Stmt::Return { value } => value.map_or(0, |e| in_expr(e, fname)),
Stmt::If { cond, then_block, else_block } => {
in_expr(cond, fname)
+ count_self_calls(then_block, fname)
+ else_block.map_or(0, |b| count_self_calls(b, fname))
}
Stmt::While { cond, body, .. } => in_expr(cond, fname) + count_self_calls(body, fname),
_ => 0,
})
.sum()
}
fn has_self_call_in_loop(block: Block, fname: Symbol) -> bool {
block.iter().any(|s| match s {
Stmt::While { body, .. } => has_self_call(body, fname),
Stmt::If { then_block, else_block, .. } => {
has_self_call_in_loop(then_block, fname)
|| else_block.map_or(false, |b| has_self_call_in_loop(b, fname))
}
_ => false,
})
}
fn has_tail_self_call(block: Block, fname: Symbol) -> bool {
block.iter().any(|s| match s {
Stmt::Return { value: Some(e) } => matches!(e, Expr::Call { function, .. } if *function == fname),
Stmt::If { then_block, else_block, .. } => {
has_tail_self_call(then_block, fname)
|| else_block.map_or(false, |b| has_tail_self_call(b, fname))
}
Stmt::While { body, .. } => has_tail_self_call(body, fname),
_ => false,
})
}
fn handled_by_loop_conversion(block: Block, fname: Symbol) -> bool {
has_tail_self_call(block, fname)
|| (count_self_calls(block, fname) == 1 && !has_self_call_in_loop(block, fname))
}
fn is_intish(ty: Option<&TypeExpr>, it: &Interner) -> bool {
matches!(ty, Some(TypeExpr::Primitive(s)) if {
let n = it.resolve(*s);
n == "Int" || n == "Nat"
})
}
fn collect_locals(block: Block, out: &mut HashSet<Symbol>) {
for s in block {
match s {
Stmt::Let { var, .. } => {
out.insert(*var);
}
Stmt::If { then_block, else_block, .. } => {
collect_locals(then_block, out);
if let Some(b) = else_block {
collect_locals(b, out);
}
}
Stmt::While { body, .. } => collect_locals(body, out),
_ => {}
}
}
}
fn bound_names_unique(params: &[Symbol], body: Block) -> bool {
fn walk(block: Block, seen: &mut HashSet<Symbol>) -> bool {
for s in block {
match s {
Stmt::Let { var, .. } => {
if !seen.insert(*var) {
return false;
}
}
Stmt::If { then_block, else_block, .. } => {
if !walk(then_block, seen) {
return false;
}
if let Some(b) = else_block {
if !walk(b, seen) {
return false;
}
}
}
Stmt::While { body, .. } => {
if !walk(body, seen) {
return false;
}
}
_ => {}
}
}
true
}
let mut seen = HashSet::new();
for p in params {
if !seen.insert(*p) {
return false;
}
}
walk(body, &mut seen)
}
fn is_tree_recursion(block: Block, fname: Symbol) -> bool {
count_self_calls(block, fname) >= 2
&& !has_self_call_in_loop(block, fname)
&& !has_tail_self_call(block, fname)
}
fn is_eligible(
params: &[Symbol],
body: Block,
return_type: Option<&TypeExpr>,
fname: Symbol,
run_path: bool,
it: &Interner,
) -> bool {
let shape_ok = has_self_call_in_loop(body, fname)
|| (run_path && is_tree_recursion(body, fname));
is_intish(return_type, it)
&& body.iter().all(stmt_ok)
&& returns_well_placed(body)
&& definitely_returns(body)
&& has_self_call(body, fname)
&& shape_ok
&& !handled_by_loop_conversion(body, fname)
&& bound_names_unique(params, body)
}
fn ren(sym: Symbol, map: &HashMap<Symbol, Symbol>) -> Symbol {
map.get(&sym).copied().unwrap_or(sym)
}
fn rename_expr<'a>(
e: &'a Expr<'a>,
map: &HashMap<Symbol, Symbol>,
ea: &'a Arena<Expr<'a>>,
) -> &'a Expr<'a> {
match e {
Expr::Identifier(s) => match map.get(s) {
Some(r) => ea.alloc(Expr::Identifier(*r)),
None => e,
},
Expr::Literal(_) => e,
Expr::BinaryOp { op, left, right } => ea.alloc(Expr::BinaryOp {
op: *op,
left: rename_expr(left, map, ea),
right: rename_expr(right, map, ea),
}),
Expr::Not { operand } => ea.alloc(Expr::Not { operand: rename_expr(operand, map, ea) }),
Expr::Index { collection, index } => ea.alloc(Expr::Index {
collection: rename_expr(collection, map, ea),
index: rename_expr(index, map, ea),
}),
Expr::Length { collection } => {
ea.alloc(Expr::Length { collection: rename_expr(collection, map, ea) })
}
Expr::Call { function, args } => ea.alloc(Expr::Call {
function: *function,
args: args.iter().map(|a| rename_expr(a, map, ea)).collect(),
}),
_ => e,
}
}
fn rename_block<'a>(
block: Block<'a>,
map: &HashMap<Symbol, Symbol>,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
) -> Block<'a> {
let out: Vec<Stmt<'a>> = block.iter().map(|s| rename_stmt(s, map, ea, sa)).collect();
sa.alloc_slice(out)
}
fn rename_stmt<'a>(
s: &Stmt<'a>,
map: &HashMap<Symbol, Symbol>,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
) -> Stmt<'a> {
match s {
Stmt::Let { var, ty, value, mutable } => Stmt::Let {
var: ren(*var, map),
ty: *ty,
value: rename_expr(value, map, ea),
mutable: *mutable,
},
Stmt::Set { target, value } => {
Stmt::Set { target: ren(*target, map), value: rename_expr(value, map, ea) }
}
Stmt::Return { value } => Stmt::Return { value: value.map(|e| rename_expr(e, map, ea)) },
Stmt::If { cond, then_block, else_block } => Stmt::If {
cond: rename_expr(cond, map, ea),
then_block: rename_block(then_block, map, ea, sa),
else_block: else_block.map(|b| rename_block(b, map, ea, sa)),
},
Stmt::While { cond, body, decreasing } => Stmt::While {
cond: rename_expr(cond, map, ea),
body: rename_block(body, map, ea, sa),
decreasing: decreasing.map(|d| rename_expr(d, map, ea)),
},
other => other.clone(),
}
}
fn restructure_seq<'a>(
block: Block<'a>,
result: Symbol,
map: &HashMap<Symbol, Symbol>,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
) -> Vec<Stmt<'a>> {
let mut out: Vec<Stmt<'a>> = Vec::new();
let n = block.len();
for (i, s) in block.iter().enumerate() {
let last = i + 1 == n;
match s {
Stmt::Return { value: Some(e) } => {
out.push(Stmt::Set { target: result, value: rename_expr(e, map, ea) });
break; }
Stmt::Return { value: None } => break,
Stmt::If { cond, then_block, else_block } => {
let guard = !last && else_block.is_none() && definitely_returns(then_block);
if guard {
let rest = &block[i + 1..];
out.push(Stmt::If {
cond: rename_expr(cond, map, ea),
then_block: sa.alloc_slice(restructure_seq(then_block, result, map, ea, sa)),
else_block: Some(sa.alloc_slice(restructure_seq(rest, result, map, ea, sa))),
});
break; } else if last
&& (definitely_returns(then_block)
|| else_block.map_or(false, definitely_returns))
{
out.push(Stmt::If {
cond: rename_expr(cond, map, ea),
then_block: sa.alloc_slice(restructure_seq(then_block, result, map, ea, sa)),
else_block: else_block
.map(|e| sa.alloc_slice(restructure_seq(e, result, map, ea, sa))),
});
break;
} else {
out.push(rename_stmt(s, map, ea, sa));
}
}
_ => out.push(rename_stmt(s, map, ea, sa)),
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn expand_call<'a>(
args: &[&'a Expr<'a>],
depth: usize,
cand: &RecCand<'a>,
fname: Symbol,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
counter: &mut usize,
emitted: &mut usize,
budget: usize,
prelude: &mut Vec<Stmt<'a>>,
) -> &'a Expr<'a> {
let id = *counter;
*counter += 1;
*emitted += cand.body.len() + cand.params.len() + 1;
let mut locals = HashSet::new();
collect_locals(cand.body, &mut locals);
let mut map: HashMap<Symbol, Symbol> = HashMap::new();
for p in &cand.params {
let name = format!("__r{id}_{}", it.resolve(*p));
map.insert(*p, it.intern(&name));
}
for l in &locals {
let name = format!("__r{id}_{}", it.resolve(*l));
map.insert(*l, it.intern(&name));
}
let result = it.intern(&format!("__r{id}_result"));
for (p, arg) in cand.params.iter().zip(args.iter()) {
prelude.push(Stmt::Let { var: ren(*p, &map), ty: None, value: arg, mutable: true });
}
prelude.push(Stmt::Let {
var: result,
ty: None,
value: ea.alloc(Expr::Literal(Literal::Number(0))),
mutable: true,
});
let restructured = restructure_seq(cand.body, result, &map, ea, sa);
let lowered = rewrite_block_inline(
&restructured,
depth - 1,
cand,
fname,
ea,
sa,
it,
counter,
emitted,
budget,
);
prelude.extend(lowered);
ea.alloc(Expr::Identifier(result))
}
#[allow(clippy::too_many_arguments)]
fn rewrite_expr_inline<'a>(
e: &'a Expr<'a>,
depth: usize,
cand: &RecCand<'a>,
fname: Symbol,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
counter: &mut usize,
emitted: &mut usize,
budget: usize,
prelude: &mut Vec<Stmt<'a>>,
) -> &'a Expr<'a> {
match e {
Expr::Call { function, args } => {
let new_args: Vec<&'a Expr<'a>> = args
.iter()
.map(|a| {
rewrite_expr_inline(
a, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
)
})
.collect();
if *function == fname
&& new_args.len() == cand.params.len()
&& depth > 0
&& *emitted < budget
{
return expand_call(
&new_args, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
}
ea.alloc(Expr::Call { function: *function, args: new_args })
}
Expr::BinaryOp { op, left, right } => {
let l = rewrite_expr_inline(
left, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
let r = if matches!(op, BinaryOpKind::And | BinaryOpKind::Or) {
*right
} else {
rewrite_expr_inline(
right, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
)
};
ea.alloc(Expr::BinaryOp { op: *op, left: l, right: r })
}
Expr::Not { operand } => {
let o = rewrite_expr_inline(
operand, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
ea.alloc(Expr::Not { operand: o })
}
Expr::Index { collection, index } => {
let c = rewrite_expr_inline(
collection, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
let i = rewrite_expr_inline(
index, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
ea.alloc(Expr::Index { collection: c, index: i })
}
Expr::Length { collection } => {
let c = rewrite_expr_inline(
collection, depth, cand, fname, ea, sa, it, counter, emitted, budget, prelude,
);
ea.alloc(Expr::Length { collection: c })
}
_ => e,
}
}
#[allow(clippy::too_many_arguments)]
fn rewrite_stmt_inline<'a>(
s: &Stmt<'a>,
depth: usize,
cand: &RecCand<'a>,
fname: Symbol,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
counter: &mut usize,
emitted: &mut usize,
budget: usize,
out: &mut Vec<Stmt<'a>>,
) {
match s {
Stmt::Let { var, ty, value, mutable } => {
let mut prelude = Vec::new();
let v = rewrite_expr_inline(
value, depth, cand, fname, ea, sa, it, counter, emitted, budget, &mut prelude,
);
out.extend(prelude);
out.push(Stmt::Let { var: *var, ty: *ty, value: v, mutable: *mutable });
}
Stmt::Set { target, value } => {
let mut prelude = Vec::new();
let v = rewrite_expr_inline(
value, depth, cand, fname, ea, sa, it, counter, emitted, budget, &mut prelude,
);
out.extend(prelude);
out.push(Stmt::Set { target: *target, value: v });
}
Stmt::Return { value } => {
let mut prelude = Vec::new();
let v = value.map(|e| {
rewrite_expr_inline(
e, depth, cand, fname, ea, sa, it, counter, emitted, budget, &mut prelude,
)
});
out.extend(prelude);
out.push(Stmt::Return { value: v });
}
Stmt::If { cond, then_block, else_block } => {
let mut prelude = Vec::new();
let c = rewrite_expr_inline(
cond, depth, cand, fname, ea, sa, it, counter, emitted, budget, &mut prelude,
);
out.extend(prelude);
let tb = rewrite_block_inline(
then_block, depth, cand, fname, ea, sa, it, counter, emitted, budget,
);
let eb = else_block.map(|b| {
rewrite_block_inline(
b, depth, cand, fname, ea, sa, it, counter, emitted, budget,
)
});
out.push(Stmt::If {
cond: c,
then_block: sa.alloc_slice(tb),
else_block: eb.map(|b| sa.alloc_slice(b)),
});
}
Stmt::While { cond, body, decreasing } => {
let b = rewrite_block_inline(
body, depth, cand, fname, ea, sa, it, counter, emitted, budget,
);
out.push(Stmt::While {
cond: *cond,
body: sa.alloc_slice(b),
decreasing: *decreasing,
});
}
other => out.push(other.clone()),
}
}
#[allow(clippy::too_many_arguments)]
fn rewrite_block_inline<'a>(
block: &[Stmt<'a>],
depth: usize,
cand: &RecCand<'a>,
fname: Symbol,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
counter: &mut usize,
emitted: &mut usize,
budget: usize,
) -> Vec<Stmt<'a>> {
let mut out: Vec<Stmt<'a>> = Vec::with_capacity(block.len());
for s in block {
rewrite_stmt_inline(
s, depth, cand, fname, ea, sa, it, counter, emitted, budget, &mut out,
);
}
out
}
pub fn inline_recursive_fns<'a>(
stmts: Vec<Stmt<'a>>,
expr_arena: &'a Arena<Expr<'a>>,
stmt_arena: &'a Arena<Stmt<'a>>,
interner: &mut Interner,
) -> Vec<Stmt<'a>> {
inline_recursive_dispatch(stmts, expr_arena, stmt_arena, interner, false)
}
pub fn inline_recursive_fns_run<'a>(
stmts: Vec<Stmt<'a>>,
expr_arena: &'a Arena<Expr<'a>>,
stmt_arena: &'a Arena<Stmt<'a>>,
interner: &mut Interner,
) -> Vec<Stmt<'a>> {
inline_recursive_dispatch(stmts, expr_arena, stmt_arena, interner, true)
}
fn inline_recursive_dispatch<'a>(
stmts: Vec<Stmt<'a>>,
expr_arena: &'a Arena<Expr<'a>>,
stmt_arena: &'a Arena<Stmt<'a>>,
interner: &mut Interner,
run_path: bool,
) -> Vec<Stmt<'a>> {
let default_depth = if run_path { DEFAULT_RUN_DEPTH } else { DEFAULT_DEPTH };
let depth: usize = std::env::var("LOGOS_RECURSE_DEPTH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default_depth);
let budget: usize = std::env::var("LOGOS_RECURSE_BUDGET")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_BUDGET);
inline_recursive_with(stmts, expr_arena, stmt_arena, interner, depth, budget, run_path)
}
fn inline_recursive_with<'a>(
stmts: Vec<Stmt<'a>>,
expr_arena: &'a Arena<Expr<'a>>,
stmt_arena: &'a Arena<Stmt<'a>>,
interner: &mut Interner,
depth: usize,
budget: usize,
run_path: bool,
) -> Vec<Stmt<'a>> {
if depth == 0 {
return stmts;
}
let mut cands: HashMap<Symbol, RecCand<'a>> = HashMap::new();
for s in &stmts {
if let Stmt::FunctionDef {
name,
generics,
params,
body,
return_type,
is_native: false,
is_exported: false,
opt_flags,
..
} = s
{
if !generics.is_empty() || !opt_flags.is_on(crate::optimization::Opt::Unfold) {
continue;
}
let param_syms: Vec<Symbol> = params.iter().map(|(p, _)| *p).collect();
if is_eligible(¶m_syms, body, *return_type, *name, run_path, interner) {
let loop_interleaved = has_self_call_in_loop(body, *name);
cands.insert(*name, RecCand { params: param_syms, body, loop_interleaved });
}
}
}
if cands.is_empty() {
return stmts;
}
let mut counter: usize = 0;
let mut out: Vec<Stmt<'a>> = Vec::with_capacity(stmts.len());
for s in &stmts {
if let Stmt::FunctionDef {
name,
generics,
params,
body,
return_type,
is_native,
native_path,
is_exported,
export_target,
opt_flags,
} = s
{
if let Some(cand) = cands.get(name) {
let eff_depth = if run_path && cand.loop_interleaved {
depth.min(RUN_LOOP_DEPTH_CAP)
} else {
depth
};
let mut emitted: usize = 0;
let new_body = rewrite_block_inline(
body,
eff_depth,
cand,
*name,
expr_arena,
stmt_arena,
interner,
&mut counter,
&mut emitted,
budget,
);
out.push(Stmt::FunctionDef {
name: *name,
generics: generics.clone(),
params: params.clone(),
body: stmt_arena.alloc_slice(new_body),
return_type: *return_type,
is_native: *is_native,
native_path: *native_path,
is_exported: *is_exported,
export_target: *export_target,
opt_flags: opt_flags.clone(),
});
continue;
}
}
out.push(s.clone());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::stmt::TypeExpr;
fn count_calls(block: Block, target: Symbol) -> usize {
fn in_expr(e: &Expr, t: Symbol) -> usize {
match e {
Expr::Call { function, args } => {
(if *function == t { 1 } else { 0 })
+ args.iter().map(|a| in_expr(a, t)).sum::<usize>()
}
Expr::BinaryOp { left, right, .. } => in_expr(left, t) + in_expr(right, t),
Expr::Not { operand } => in_expr(operand, t),
Expr::Index { collection, index } => in_expr(collection, t) + in_expr(index, t),
Expr::Length { collection } => in_expr(collection, t),
_ => 0,
}
}
fn in_block(b: Block, t: Symbol) -> usize {
b.iter()
.map(|s| match s {
Stmt::Let { value, .. } | Stmt::Set { value, .. } => in_expr(value, t),
Stmt::Return { value } => value.map_or(0, |e| in_expr(e, t)),
Stmt::If { cond, then_block, else_block } => {
in_expr(cond, t)
+ in_block(then_block, t)
+ else_block.map_or(0, |x| in_block(x, t))
}
Stmt::While { cond, body, .. } => in_expr(cond, t) + in_block(body, t),
_ => 0,
})
.sum()
}
in_block(block, target)
}
fn count_result_temps(block: Block, it: &Interner) -> usize {
fn in_block(b: Block, it: &Interner) -> usize {
b.iter()
.map(|s| match s {
Stmt::Let { var, .. } => {
if it.resolve(*var).ends_with("_result") {
1
} else {
0
}
}
Stmt::If { then_block, else_block, .. } => {
in_block(then_block, it) + else_block.map_or(0, |x| in_block(x, it))
}
Stmt::While { body, .. } => in_block(body, it),
_ => 0,
})
.sum()
}
in_block(block, it)
}
fn body_of<'a>(stmts: &'a [Stmt<'a>], name: Symbol) -> Block<'a> {
for s in stmts {
if let Stmt::FunctionDef { name: n, body, .. } = s {
if *n == name {
return body;
}
}
}
panic!("function not found");
}
fn build_f<'a>(
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
ta: &'a Arena<TypeExpr<'a>>,
it: &mut Interner,
) -> (Stmt<'a>, Symbol) {
let f = it.intern("f");
let row = it.intern("row");
let n = it.intern("n");
let count = it.intern("count");
let int_ty: &TypeExpr = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
let guard = Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Eq,
left: ea.alloc(Expr::Identifier(row)),
right: ea.alloc(Expr::Identifier(n)),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Literal(Literal::Number(1)))),
}]),
else_block: None,
};
let self_call = ea.alloc(Expr::Call {
function: f,
args: vec![
ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Add,
left: ea.alloc(Expr::Identifier(row)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
}),
ea.alloc(Expr::Identifier(n)),
],
});
let loop_stmt = Stmt::While {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Lt,
left: ea.alloc(Expr::Identifier(count)),
right: ea.alloc(Expr::Identifier(n)),
}),
body: sa.alloc_slice(vec![Stmt::Set {
target: count,
value: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Add,
left: ea.alloc(Expr::Identifier(count)),
right: self_call,
}),
}]),
decreasing: None,
};
let body = sa.alloc_slice(vec![
guard,
Stmt::Let {
var: count,
ty: None,
value: ea.alloc(Expr::Literal(Literal::Number(0))),
mutable: true,
},
loop_stmt,
Stmt::Return { value: Some(ea.alloc(Expr::Identifier(count))) },
]);
let func = Stmt::FunctionDef {
name: f,
generics: vec![],
params: vec![(row, int_ty), (n, int_ty)],
body,
return_type: Some(int_ty),
is_native: false,
native_path: None,
is_exported: false,
export_target: None,
opt_flags: Default::default(),
};
(func, f)
}
#[test]
fn unrolls_self_recursion_to_depth_and_bottoms_out() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, f) = build_f(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 3, DEFAULT_BUDGET, false);
let body = body_of(&out, f);
assert_eq!(count_calls(body, f), 1, "exactly one residual call bottoms out");
assert_eq!(count_result_temps(body, &it), 3, "one result temp per inlined level");
let inlined_has_else = {
fn any_guard_else(b: Block) -> bool {
b.iter().any(|s| match s {
Stmt::If { else_block: Some(_), .. } => true,
Stmt::If { then_block, else_block: None, .. } => any_guard_else(then_block),
Stmt::While { body, .. } => any_guard_else(body),
_ => false,
})
}
any_guard_else(body)
};
assert!(inlined_has_else, "guard restructuring produced an else branch");
}
#[test]
fn depth_zero_is_a_noop() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, f) = build_f(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 0, DEFAULT_BUDGET, false);
let body = body_of(&out, f);
assert_eq!(count_calls(body, f), 1, "depth 0 leaves the single original call");
assert_eq!(count_result_temps(body, &it), 0, "no inlining happened");
}
#[test]
fn budget_clamps_runaway_unrolling() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, f) = build_f(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 50, 5, false);
let body = body_of(&out, f);
assert_eq!(count_calls(body, f), 1, "a real call still bottoms out");
assert!(count_result_temps(body, &it) < 50, "budget clamped the depth");
assert!(count_result_temps(body, &it) >= 1, "at least one level inlined");
}
#[test]
fn return_inside_loop_is_ineligible() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let g = it.intern("g");
let n = it.intern("n");
let int_ty: &TypeExpr = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
let loop_stmt = Stmt::While {
cond: ea.alloc(Expr::Literal(Literal::Number(1))),
body: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Call {
function: g,
args: vec![ea.alloc(Expr::Identifier(n))],
})),
}]),
decreasing: None,
};
let body = sa.alloc_slice(vec![
loop_stmt,
Stmt::Return { value: Some(ea.alloc(Expr::Literal(Literal::Number(0)))) },
]);
let func = Stmt::FunctionDef {
name: g,
generics: vec![],
params: vec![(n, int_ty)],
body,
return_type: Some(int_ty),
is_native: false,
native_path: None,
is_exported: false,
export_target: None,
opt_flags: Default::default(),
};
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 3, DEFAULT_BUDGET, false);
let body = body_of(&out, g);
assert_eq!(count_result_temps(body, &it), 0, "ineligible: no inlining");
}
fn make_fn<'a>(
name: Symbol,
params: Vec<Symbol>,
body: Block<'a>,
ta: &'a Arena<TypeExpr<'a>>,
it: &mut Interner,
) -> Stmt<'a> {
let int_ty: &TypeExpr = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
Stmt::FunctionDef {
name,
generics: vec![],
params: params.into_iter().map(|p| (p, int_ty)).collect(),
body,
return_type: Some(int_ty),
is_native: false,
native_path: None,
is_exported: false,
export_target: None,
opt_flags: Default::default(),
}
}
#[test]
fn tail_recursion_is_deferred_to_tce() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let cd = it.intern("cd");
let n = it.intern("n");
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Eq,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(0))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Literal(Literal::Number(0)))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::Call {
function: cd,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
})],
})),
},
]);
let func = make_fn(cd, vec![n], body, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 4, DEFAULT_BUDGET, false);
assert_eq!(count_result_temps(body_of(&out, cd), &it), 0, "tail recursion deferred");
}
#[test]
fn linear_recursion_is_deferred_to_accumulator() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let fac = it.intern("fac");
let n = it.intern("n");
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::LtEq,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Literal(Literal::Number(1)))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Multiply,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Call {
function: fac,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
})],
}),
})),
},
]);
let func = make_fn(fac, vec![n], body, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 4, DEFAULT_BUDGET, false);
assert_eq!(count_result_temps(body_of(&out, fac), &it), 0, "linear recursion deferred");
}
#[test]
fn tree_recursion_without_loop_is_deferred() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let fib = it.intern("fib");
let n = it.intern("n");
let fib_minus = |sub: i64| -> &Expr {
ea.alloc(Expr::Call {
function: fib,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(sub))),
})],
})
};
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Lt,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(2))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Identifier(n))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Add,
left: fib_minus(1),
right: fib_minus(2),
})),
},
]);
let func = make_fn(fib, vec![n], body, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 2, DEFAULT_BUDGET, false);
let body = body_of(&out, fib);
assert_eq!(count_result_temps(body, &it), 0, "return-position recursion deferred");
assert_eq!(count_calls(body, fib), 2, "the two original self-calls are untouched");
}
fn build_fib<'a>(
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
ta: &'a Arena<TypeExpr<'a>>,
it: &mut Interner,
) -> (Stmt<'a>, Symbol) {
let fib = it.intern("fib");
let n = it.intern("n");
let fib_minus = |sub: i64| -> &Expr {
ea.alloc(Expr::Call {
function: fib,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(sub))),
})],
})
};
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Lt,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(2))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Identifier(n))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Add,
left: fib_minus(1),
right: fib_minus(2),
})),
},
]);
(make_fn(fib, vec![n], body, ta, it), fib)
}
#[test]
fn tree_recursion_is_unrolled_on_run_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, fib) = build_fib(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 1, DEFAULT_BUDGET, true);
let body = body_of(&out, fib);
assert_eq!(count_result_temps(body, &it), 2, "both self-call sites inlined once");
assert_eq!(count_calls(body, fib), 4, "each inlined copy bottoms out with real calls");
}
#[test]
fn tree_recursion_still_deferred_on_aot_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, fib) = build_fib(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 2, DEFAULT_BUDGET, false);
let body = body_of(&out, fib);
assert_eq!(count_result_temps(body, &it), 0, "AOT defers tree recursion");
assert_eq!(count_calls(body, fib), 2, "the two original self-calls are untouched");
}
#[test]
fn tail_recursion_still_deferred_on_run_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let cd = it.intern("cd");
let n = it.intern("n");
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Eq,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(0))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Literal(Literal::Number(0)))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::Call {
function: cd,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
})],
})),
},
]);
let func = make_fn(cd, vec![n], body, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 4, DEFAULT_BUDGET, true);
assert_eq!(
count_result_temps(body_of(&out, cd), &it),
0,
"tail recursion deferred to tail_call even on the run path"
);
}
#[test]
fn linear_recursion_still_deferred_on_run_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let fac = it.intern("fac");
let n = it.intern("n");
let body = sa.alloc_slice(vec![
Stmt::If {
cond: ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::LtEq,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
}),
then_block: sa.alloc_slice(vec![Stmt::Return {
value: Some(ea.alloc(Expr::Literal(Literal::Number(1)))),
}]),
else_block: None,
},
Stmt::Return {
value: Some(ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Multiply,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Call {
function: fac,
args: vec![ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Subtract,
left: ea.alloc(Expr::Identifier(n)),
right: ea.alloc(Expr::Literal(Literal::Number(1))),
})],
}),
})),
},
]);
let func = make_fn(fac, vec![n], body, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 4, DEFAULT_BUDGET, true);
assert_eq!(
count_result_temps(body_of(&out, fac), &it),
0,
"single-linear recursion deferred to the accumulator transform"
);
}
#[test]
fn loop_interleaved_recursion_is_depth_capped_on_run_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, f) = build_f(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 8, DEFAULT_BUDGET, true);
let body = body_of(&out, f);
assert_eq!(
count_result_temps(body, &it),
RUN_LOOP_DEPTH_CAP,
"loop-interleaved recursion capped at RUN_LOOP_DEPTH_CAP on the run path"
);
assert_eq!(count_calls(body, f), 1, "a single real call still bottoms out");
}
#[test]
fn loop_interleaved_recursion_is_not_capped_on_aot_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, f) = build_f(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 5, DEFAULT_BUDGET, false);
let body = body_of(&out, f);
assert_eq!(
count_result_temps(body, &it),
5,
"AOT loop-interleaved recursion unrolls to the full requested depth"
);
}
#[test]
fn tree_recursion_is_not_depth_capped_on_run_path() {
let ea: Arena<Expr> = Arena::new();
let sa: Arena<Stmt> = Arena::new();
let ta: Arena<TypeExpr> = Arena::new();
let mut it = Interner::new();
let (func, fib) = build_fib(&ea, &sa, &ta, &mut it);
let out = inline_recursive_with(vec![func], &ea, &sa, &mut it, 3, DEFAULT_BUDGET, true);
let body = body_of(&out, fib);
assert_eq!(count_result_temps(body, &it), 14, "tree recursion unrolls uncapped");
}
}