use std::collections::HashMap;
use crate::arena::Arena;
use crate::ast::stmt::{BinaryOpKind, Expr, Literal, MatchArm, Stmt};
use crate::intern::{Interner, Symbol};
#[derive(Clone, Debug)]
struct CarriedSquare {
operand: Symbol,
temp: Symbol,
seed: Literal,
}
pub fn loop_carried_cse_stmts<'a>(
stmts: Vec<Stmt<'a>>,
expr_arena: &'a Arena<Expr<'a>>,
stmt_arena: &'a Arena<Stmt<'a>>,
interner: &mut Interner,
) -> (Vec<Stmt<'a>>, bool) {
let mut changed = false;
let out = rewrite_block(stmts, expr_arena, stmt_arena, interner, &mut changed);
(out, changed)
}
fn rewrite_block<'a>(
stmts: Vec<Stmt<'a>>,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
changed: &mut bool,
) -> Vec<Stmt<'a>> {
let mut out: Vec<Stmt<'a>> = Vec::with_capacity(stmts.len());
let mut idx = 0;
while idx < stmts.len() {
let prefix: &[Stmt<'a>] = &stmts[..idx];
let stmt = &stmts[idx];
if let Stmt::While { cond, body, decreasing } = stmt {
if let Some(squares) = detect_carried_squares(body, &out, prefix, it) {
let new_body =
build_body_with_temps(body, &squares, ea, sa, it, changed);
let new_cond = substitute_squares_expr(cond, &squares, ea);
for sq in &squares {
out.push(Stmt::Let {
var: sq.temp,
ty: None,
value: ea.alloc(Expr::Literal(sq.seed.clone())),
mutable: true,
});
}
out.push(Stmt::While {
cond: new_cond,
body: new_body,
decreasing: *decreasing,
});
*changed = true;
idx += 1;
continue;
}
}
let owned = stmts[idx].clone();
out.push(recurse_stmt(owned, ea, sa, it, changed));
idx += 1;
}
out
}
fn recurse_stmt<'a>(
stmt: Stmt<'a>,
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
changed: &mut bool,
) -> Stmt<'a> {
let recur = |block: &'a [Stmt<'a>], it: &mut Interner, changed: &mut bool| -> &'a [Stmt<'a>] {
let v = rewrite_block(block.to_vec(), ea, sa, it, changed);
sa.alloc_slice(v)
};
match stmt {
Stmt::If { cond, then_block, else_block } => Stmt::If {
cond,
then_block: recur(then_block, it, changed),
else_block: else_block.map(|b| recur(b, it, changed)),
},
Stmt::While { cond, body, decreasing } => Stmt::While {
cond,
body: recur(body, it, changed),
decreasing,
},
Stmt::Repeat { pattern, iterable, body } => Stmt::Repeat {
pattern,
iterable,
body: recur(body, it, changed),
},
Stmt::Inspect { target, arms, has_otherwise } => {
let arms = arms
.into_iter()
.map(|a| MatchArm {
enum_name: a.enum_name,
variant: a.variant,
bindings: a.bindings,
body: recur(a.body, it, changed),
})
.collect();
Stmt::Inspect { target, arms, has_otherwise }
}
Stmt::Zone { name, capacity, source_file, body } => Stmt::Zone {
name,
capacity,
source_file,
body: recur(body, it, changed),
},
Stmt::Concurrent { tasks } => Stmt::Concurrent { tasks: recur(tasks, it, changed) },
Stmt::Parallel { tasks } => Stmt::Parallel { tasks: recur(tasks, it, changed) },
other => other,
}
}
fn detect_carried_squares(
body: &[Stmt],
before: &[Stmt],
prefix: &[Stmt],
it: &mut Interner,
) -> Option<Vec<CarriedSquare>> {
let guard_expr = body.iter().find_map(|s| match s {
Stmt::If { cond, .. } => exit_guard_compared(cond),
_ => None,
})?;
let mut candidates: Vec<Symbol> = Vec::new();
collect_self_squares(guard_expr, &mut candidates);
candidates.dedup();
if candidates.is_empty() {
return None;
}
let mut squares: Vec<CarriedSquare> = Vec::new();
for o in candidates {
if let Some(sq) = qualify_square(o, body, before, prefix, it) {
squares.push(sq);
}
}
if squares.is_empty() {
None
} else {
Some(squares)
}
}
fn exit_guard_compared<'e>(cond: &'e Expr<'e>) -> Option<&'e Expr<'e>> {
match cond {
Expr::BinaryOp { op, left, .. }
if matches!(
op,
BinaryOpKind::Lt | BinaryOpKind::Gt | BinaryOpKind::LtEq | BinaryOpKind::GtEq
) =>
{
Some(left)
}
_ => None,
}
}
fn collect_self_squares(e: &Expr, out: &mut Vec<Symbol>) {
if let Expr::BinaryOp { op: BinaryOpKind::Multiply, left, right } = e {
if let (Expr::Identifier(a), Expr::Identifier(b)) = (left, right) {
if a == b {
out.push(*a);
}
}
}
match e {
Expr::BinaryOp { op, left, right }
if matches!(op, BinaryOpKind::Add | BinaryOpKind::Subtract | BinaryOpKind::Multiply) =>
{
collect_self_squares(left, out);
collect_self_squares(right, out);
}
Expr::Not { operand } => collect_self_squares(operand, out),
_ => {}
}
}
fn qualify_square(
o: Symbol,
body: &[Stmt],
before: &[Stmt],
prefix: &[Stmt],
it: &mut Interner,
) -> Option<CarriedSquare> {
let mut set_positions: Vec<usize> = Vec::new();
for (i, s) in body.iter().enumerate() {
match s {
Stmt::Set { target, value } if *target == o => {
if expr_reads(value, o) {
return None;
}
set_positions.push(i);
}
Stmt::Let { var, .. } if *var == o => return None,
_ => {}
}
}
if set_positions.len() != 1 {
return None;
}
let set_pos = set_positions[0];
if body.iter().any(|s| nested_assigns_var(s, o)) {
return None;
}
let recomputed_before = body[..set_pos]
.iter()
.any(|s| stmt_has_self_square(s, o));
if !recomputed_before {
return None;
}
let entry = entry_literal(o, before, prefix)?;
let seed = square_literal(&entry)?;
let temp = it.intern(&format!("__lcse_{}_sq", it.resolve(o)));
if temp == o || body.iter().any(|s| stmt_assigns(s, temp)) {
return None;
}
Some(CarriedSquare { operand: o, temp, seed })
}
fn entry_literal(o: Symbol, before: &[Stmt], prefix: &[Stmt]) -> Option<Literal> {
let scan = |stmts: &[Stmt]| -> Option<Literal> {
let mut found = None;
for s in stmts {
if let Stmt::Let { var, value, .. } = s {
if *var == o {
found = match value {
Expr::Literal(l @ (Literal::Number(_) | Literal::Float(_))) => {
Some(l.clone())
}
_ => None,
};
}
} else if stmt_assigns(s, o) {
found = None;
}
}
found
};
scan(prefix).or_else(|| scan(before))
}
fn square_literal(l: &Literal) -> Option<Literal> {
match l {
Literal::Number(n) => n.checked_mul(*n).map(Literal::Number),
Literal::Float(f) => Some(Literal::Float(f * f)),
_ => None,
}
}
fn build_body_with_temps<'a>(
body: &'a [Stmt<'a>],
squares: &[CarriedSquare],
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
it: &mut Interner,
changed: &mut bool,
) -> &'a [Stmt<'a>] {
let mut out: Vec<Stmt<'a>> = Vec::with_capacity(body.len() + squares.len());
for s in body {
let recursed = recurse_stmt(s.clone(), ea, sa, it, changed);
let substituted = substitute_squares_stmt(recursed, squares, ea, sa);
out.push(substituted);
if let Stmt::Set { target, .. } = s {
for sq in squares.iter().filter(|sq| sq.operand == *target) {
let prod = ea.alloc(Expr::BinaryOp {
op: BinaryOpKind::Multiply,
left: ea.alloc(Expr::Identifier(sq.operand)),
right: ea.alloc(Expr::Identifier(sq.operand)),
});
out.push(Stmt::Set { target: sq.temp, value: prod });
}
}
}
sa.alloc_slice(out)
}
fn substitute_squares_stmt<'a>(
stmt: Stmt<'a>,
squares: &[CarriedSquare],
ea: &'a Arena<Expr<'a>>,
sa: &'a Arena<Stmt<'a>>,
) -> Stmt<'a> {
let sub = |e: &'a Expr<'a>| substitute_squares_expr(e, squares, ea);
let sub_block = |block: &'a [Stmt<'a>]| -> &'a [Stmt<'a>] {
let v: Vec<Stmt<'a>> = block
.iter()
.map(|s| substitute_squares_stmt(s.clone(), squares, ea, sa))
.collect();
sa.alloc_slice(v)
};
match stmt {
Stmt::Let { var, ty, value, mutable } => {
Stmt::Let { var, ty, value: sub(value), mutable }
}
Stmt::Set { target, value } => Stmt::Set { target, value: sub(value) },
Stmt::If { cond, then_block, else_block } => Stmt::If {
cond: sub(cond),
then_block: sub_block(then_block),
else_block: else_block.map(sub_block),
},
Stmt::While { cond, body, decreasing } => Stmt::While {
cond: sub(cond),
body: sub_block(body),
decreasing: decreasing.map(sub),
},
Stmt::Return { value } => Stmt::Return { value: value.map(sub) },
Stmt::RuntimeAssert { condition, hard } => Stmt::RuntimeAssert { condition: sub(condition) , hard },
Stmt::Show { object, recipient } => {
Stmt::Show { object: sub(object), recipient: sub(recipient) }
}
other => other,
}
}
fn substitute_squares_expr<'a>(
e: &'a Expr<'a>,
squares: &[CarriedSquare],
ea: &'a Arena<Expr<'a>>,
) -> &'a Expr<'a> {
if let Expr::BinaryOp { op: BinaryOpKind::Multiply, left, right } = e {
if let (Expr::Identifier(a), Expr::Identifier(b)) = (left, right) {
if a == b {
if let Some(sq) = squares.iter().find(|sq| sq.operand == *a) {
return ea.alloc(Expr::Identifier(sq.temp));
}
}
}
}
match e {
Expr::BinaryOp { op, left, right } => ea.alloc(Expr::BinaryOp {
op: *op,
left: substitute_squares_expr(left, squares, ea),
right: substitute_squares_expr(right, squares, ea),
}),
Expr::Not { operand } => {
ea.alloc(Expr::Not { operand: substitute_squares_expr(operand, squares, ea) })
}
_ => e,
}
}
fn stmt_has_self_square(s: &Stmt, o: Symbol) -> bool {
let mut found = false;
let mut check = |e: &Expr| {
if expr_has_self_square(e, o) {
found = true;
}
};
match s {
Stmt::Let { value, .. } => check(value),
Stmt::Set { value, .. } => check(value),
Stmt::If { cond, .. } => check(cond),
Stmt::While { cond, .. } => check(cond),
Stmt::Return { value: Some(v) } => check(v),
Stmt::RuntimeAssert { condition, .. } => check(condition),
_ => {}
}
found
}
fn expr_has_self_square(e: &Expr, o: Symbol) -> bool {
if let Expr::BinaryOp { op: BinaryOpKind::Multiply, left, right } = e {
if let (Expr::Identifier(a), Expr::Identifier(b)) = (left, right) {
if *a == o && *b == o {
return true;
}
}
}
match e {
Expr::BinaryOp { left, right, .. } => {
expr_has_self_square(left, o) || expr_has_self_square(right, o)
}
Expr::Not { operand } => expr_has_self_square(operand, o),
_ => false,
}
}
fn expr_reads(e: &Expr, o: Symbol) -> bool {
match e {
Expr::Identifier(s) => *s == o,
Expr::BinaryOp { left, right, .. }
| Expr::Union { left, right }
| Expr::Intersection { left, right }
| Expr::Range { start: left, end: right } => expr_reads(left, o) || expr_reads(right, o),
Expr::Not { operand } => expr_reads(operand, o),
Expr::Index { collection, index } => expr_reads(collection, o) || expr_reads(index, o),
Expr::Slice { collection, start, end } => {
expr_reads(collection, o) || expr_reads(start, o) || expr_reads(end, o)
}
Expr::Length { collection } => expr_reads(collection, o),
Expr::Copy { expr } | Expr::Give { value: expr } | Expr::OptionSome { value: expr } => {
expr_reads(expr, o)
}
Expr::Contains { collection, value } => {
expr_reads(collection, o) || expr_reads(value, o)
}
Expr::FieldAccess { object, .. } => expr_reads(object, o),
Expr::Call { args, .. } => args.iter().any(|a| expr_reads(a, o)),
Expr::CallExpr { callee, args } => {
expr_reads(callee, o) || args.iter().any(|a| expr_reads(a, o))
}
Expr::List(items) | Expr::Tuple(items) => items.iter().any(|a| expr_reads(a, o)),
Expr::WithCapacity { value, capacity } => {
expr_reads(value, o) || expr_reads(capacity, o)
}
_ => false,
}
}
fn stmt_assigns(s: &Stmt, v: Symbol) -> bool {
matches!(s, Stmt::Let { var, .. } if *var == v)
|| matches!(s, Stmt::Set { target, .. } if *target == v)
}
fn nested_assigns_var(s: &Stmt, v: Symbol) -> bool {
fn block_assigns(block: &[Stmt], v: Symbol) -> bool {
block.iter().any(|s| stmt_assigns(s, v) || nested_assigns_var(s, v))
}
match s {
Stmt::If { then_block, else_block, .. } => {
block_assigns(then_block, v)
|| else_block.as_ref().map_or(false, |b| block_assigns(b, v))
}
Stmt::While { body, .. } | Stmt::Repeat { body, .. } => block_assigns(body, v),
Stmt::Inspect { arms, .. } => arms.iter().any(|a| block_assigns(a.body, v)),
Stmt::Zone { body, .. }
| Stmt::Concurrent { tasks: body }
| Stmt::Parallel { tasks: body } => block_assigns(body, v),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct B<'a> {
ea: &'a Arena<Expr<'a>>,
}
impl<'a> B<'a> {
fn id(&self, s: Symbol) -> &'a Expr<'a> {
self.ea.alloc(Expr::Identifier(s))
}
fn fl(&self, f: f64) -> &'a Expr<'a> {
self.ea.alloc(Expr::Literal(Literal::Float(f)))
}
fn num(&self, n: i64) -> &'a Expr<'a> {
self.ea.alloc(Expr::Literal(Literal::Number(n)))
}
fn bin(&self, op: BinaryOpKind, l: &'a Expr<'a>, r: &'a Expr<'a>) -> &'a Expr<'a> {
self.ea.alloc(Expr::BinaryOp { op, left: l, right: r })
}
fn sq(&self, s: Symbol) -> &'a Expr<'a> {
self.bin(BinaryOpKind::Multiply, self.id(s), self.id(s))
}
}
fn count_square_assigns(block: &[Stmt], temp: Symbol) -> usize {
block
.iter()
.filter(|s| matches!(s, Stmt::Set { target, value }
if *target == temp
&& matches!(value, Expr::BinaryOp { op: BinaryOpKind::Multiply, left, right }
if matches!((left, right), (Expr::Identifier(a), Expr::Identifier(b)) if a == b))))
.count()
}
fn only_square_is_temp_assign(block: &[Stmt], o: Symbol, temp: Symbol) -> bool {
block.iter().all(|s| {
if matches!(s, Stmt::Set { target, .. } if *target == temp) {
true
} else {
!stmt_has_self_square(s, o)
}
})
}
#[test]
fn mandelbrot_escape_loop_hoists_squares() {
let ea = Arena::new();
let sa = Arena::new();
let mut it = Interner::new();
let zr = it.intern("zr");
let zi = it.intern("zi");
let cr = it.intern("cr");
let ci = it.intern("ci");
let zr2 = it.intern("zr2");
let zi2 = it.intern("zi2");
let iter = it.intern("iter");
let is_inside = it.intern("isInside");
let b = B { ea: &ea };
let decl_zr = Stmt::Let { var: zr, ty: None, value: b.fl(0.0), mutable: true };
let decl_zi = Stmt::Let { var: zi, ty: None, value: b.fl(0.0), mutable: true };
let decl_iter = Stmt::Let { var: iter, ty: None, value: b.num(0), mutable: true };
let let_zr2 = Stmt::Let {
var: zr2,
ty: None,
value: b.bin(
BinaryOpKind::Add,
b.bin(BinaryOpKind::Subtract, b.sq(zr), b.sq(zi)),
b.id(cr),
),
mutable: false,
};
let let_zi2 = Stmt::Let {
var: zi2,
ty: None,
value: b.bin(
BinaryOpKind::Add,
b.bin(
BinaryOpKind::Multiply,
b.bin(BinaryOpKind::Multiply, b.fl(2.0), b.id(zr)),
b.id(zi),
),
b.id(ci),
),
mutable: false,
};
let set_zr = Stmt::Set { target: zr, value: b.id(zr2) };
let set_zi = Stmt::Set { target: zi, value: b.id(zi2) };
let guard = Stmt::If {
cond: b.bin(
BinaryOpKind::Gt,
b.bin(BinaryOpKind::Add, b.sq(zr), b.sq(zi)),
b.fl(4.0),
),
then_block: sa.alloc_slice(vec![
Stmt::Set { target: is_inside, value: b.num(0) },
Stmt::Set { target: iter, value: b.num(50) },
]),
else_block: None,
};
let step = Stmt::Set {
target: iter,
value: b.bin(BinaryOpKind::Add, b.id(iter), b.num(1)),
};
let body = sa.alloc_slice(vec![
let_zr2, let_zi2, set_zr, set_zi, guard, step,
]);
let loop_stmt = Stmt::While {
cond: b.bin(BinaryOpKind::Lt, b.id(iter), b.num(50)),
body,
decreasing: None,
};
let input = vec![decl_zr, decl_zi, decl_iter, loop_stmt];
let (out, changed) = loop_carried_cse_stmts(input, &ea, &sa, &mut it);
assert!(changed, "the transform must fire on mandelbrot's escape loop");
let zr_sq = it.intern("__lcse_zr_sq");
let zi_sq = it.intern("__lcse_zi_sq");
let seeds: Vec<&Stmt> = out
.iter()
.filter(|s| matches!(s, Stmt::Let { var, value, .. }
if (*var == zr_sq || *var == zi_sq)
&& matches!(value, Expr::Literal(Literal::Float(f)) if *f == 0.0)))
.collect();
assert_eq!(seeds.len(), 2, "both squared-temp seeds must precede the loop");
let Stmt::While { body, cond, .. } = out.last().unwrap() else {
panic!("the last statement must remain the While loop");
};
assert_eq!(count_square_assigns(body, zr_sq), 1, "zr*zr computed once per iter");
assert_eq!(count_square_assigns(body, zi_sq), 1, "zi*zi computed once per iter");
assert!(
only_square_is_temp_assign(body, zr, zr_sq),
"the only zr*zr left is the single temp assignment"
);
assert!(
only_square_is_temp_assign(body, zi, zi_sq),
"the only zi*zi left is the single temp assignment"
);
let _ = cond;
let escape = body
.iter()
.find_map(|s| match s {
Stmt::If { cond, .. } => Some(*cond),
_ => None,
})
.expect("escape guard remains");
assert!(
matches!(escape, Expr::BinaryOp { op: BinaryOpKind::Gt, left, .. }
if matches!(left, Expr::BinaryOp { op: BinaryOpKind::Add, left, right }
if matches!((left, right),
(Expr::Identifier(a), Expr::Identifier(c)) if *a == zr_sq && *c == zi_sq))),
"the escape guard must compare the hoisted temps"
);
}
#[test]
fn self_referential_update_does_not_fire() {
let ea = Arena::new();
let sa = Arena::new();
let mut it = Interner::new();
let o = it.intern("o");
let acc = it.intern("acc");
let b = B { ea: &ea };
let decl = Stmt::Let { var: o, ty: None, value: b.num(0), mutable: true };
let recompute = Stmt::Let {
var: acc,
ty: None,
value: b.sq(o),
mutable: false,
};
let step = Stmt::Set {
target: o,
value: b.bin(BinaryOpKind::Add, b.id(o), b.num(1)),
};
let guard = Stmt::If {
cond: b.bin(BinaryOpKind::Gt, b.sq(o), b.num(100)),
then_block: sa.alloc_slice(vec![Stmt::Break]),
else_block: None,
};
let body = sa.alloc_slice(vec![recompute, step, guard]);
let loop_stmt = Stmt::While {
cond: b.bin(BinaryOpKind::Lt, b.id(o), b.num(1000)),
body,
decreasing: None,
};
let (_, changed) =
loop_carried_cse_stmts(vec![decl, loop_stmt], &ea, &sa, &mut it);
assert!(!changed, "must not fire when the operand's update reads itself");
}
#[test]
fn unknown_entry_value_skips() {
let ea = Arena::new();
let sa = Arena::new();
let mut it = Interner::new();
let o = it.intern("o");
let src = it.intern("src");
let nv = it.intern("nv");
let acc = it.intern("acc");
let b = B { ea: &ea };
let decl = Stmt::Let { var: o, ty: None, value: b.id(src), mutable: true };
let recompute = Stmt::Let { var: acc, ty: None, value: b.sq(o), mutable: false };
let set = Stmt::Set { target: o, value: b.id(nv) };
let guard = Stmt::If {
cond: b.bin(BinaryOpKind::Gt, b.sq(o), b.num(4)),
then_block: sa.alloc_slice(vec![Stmt::Break]),
else_block: None,
};
let body = sa.alloc_slice(vec![recompute, set, guard]);
let loop_stmt = Stmt::While {
cond: b.bin(BinaryOpKind::Lt, b.id(o), b.num(10)),
body,
decreasing: None,
};
let (_, changed) =
loop_carried_cse_stmts(vec![decl, loop_stmt], &ea, &sa, &mut it);
assert!(!changed, "must not fire when the entry value is not a known literal");
}
}