use std::collections::{HashMap, HashSet};
use rucc_base::Interner;
use rucc_mir::{Func, Inst, Opcode, Reg};
use rucc_regalloc::Allocation;
use rucc_regalloc::assign::Place;
use rucc_regalloc::live::{Area, Live, Range};
use rucc_regalloc::order::Order;
use rucc_regalloc::rewrite::At;
use rucc_target::FrameInsts;
use crate::frame::Local;
pub const CROWDED: usize = 2048;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub size: u32,
pub align: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Slots {
cells: Vec<Cell>,
locals: Vec<usize>,
slots: Vec<usize>,
}
impl Slots {
#[must_use]
pub fn apart(locals: &[Local], widths: &[u32]) -> Self {
let mut cells = Vec::with_capacity(locals.len() + widths.len());
for &Local { size, align } in locals {
cells.push(Cell { size, align });
}
for &width in widths {
cells.push(Cell { size: width, align: width });
}
Self {
locals: (0..locals.len()).collect(),
slots: (locals.len()..cells.len()).collect(),
cells,
}
}
#[must_use]
pub fn share(
func: &Func,
reach: &Reach,
allocation: &Allocation,
locals: &[Local],
widths: &[u32],
) -> Self {
if locals.len() + widths.len() > CROWDED {
return Self::apart(locals, widths);
}
let mut wants = Vec::with_capacity(locals.len() + widths.len());
let mut reached = areas(func, reach, &allocation.live, &allocation.order);
for (local, &Local { size, align }) in locals.iter().enumerate() {
let area = reached.get_mut(local).and_then(Option::take);
wants.push(Want { what: What::Local(local), size, align, area });
}
let held = spilled(allocation, widths.len());
let moved = moved(allocation, widths.len());
for (slot, &width) in widths.iter().enumerate() {
let area = held[slot]
.and_then(|reg| allocation.live.area(reg))
.map(|live| merged(live.pieces().chain(moved[slot].iter().copied())));
wants.push(Want { what: What::Slot(slot), size: width, align: width, area });
}
fit(wants, locals.len(), widths.len())
}
#[must_use]
pub fn cells(&self) -> &[Cell] {
&self.cells
}
#[must_use]
pub fn local(&self, local: usize) -> Option<usize> {
self.locals.get(local).copied()
}
#[must_use]
pub fn slot(&self, slot: u32) -> Option<usize> {
self.slots.get(usize::try_from(slot).ok()?).copied()
}
#[must_use]
pub fn saved(&self) -> usize {
self.locals.len() + self.slots.len() - self.cells.len()
}
}
#[derive(Debug)]
struct Want {
what: What,
size: u32,
align: u32,
area: Option<Vec<Range>>,
}
#[derive(Debug, Clone, Copy)]
enum What {
Local(usize),
Slot(usize),
}
fn fit(mut wants: Vec<Want>, locals: usize, slots: usize) -> Slots {
let mut order: Vec<usize> = (0..wants.len()).collect();
order.sort_by_key(|&want| {
let Want { size, align, .. } = wants[want];
(std::cmp::Reverse(align), std::cmp::Reverse(size), want)
});
let mut cells: Vec<Cell> = Vec::new();
let mut busy: Vec<Option<Vec<Range>>> = Vec::new();
let mut of_local = vec![0; locals];
let mut of_slot = vec![0; slots];
for want in order {
let Want { what, size, align, area } = std::mem::replace(
&mut wants[want],
Want { what: What::Local(0), size: 0, align: 0, area: None },
);
let into = area.as_ref().and_then(|area| {
(0..cells.len())
.find(|&cell| busy[cell].as_ref().is_some_and(|busy| !clashes(busy, area)))
});
let cell = match into {
Some(cell) => {
cells[cell].size = cells[cell].size.max(size);
cells[cell].align = cells[cell].align.max(align);
let held = busy[cell].take().unwrap_or_default();
busy[cell] = Some(merged(held.into_iter().chain(area.into_iter().flatten())));
cell
}
None => {
cells.push(Cell { size, align });
busy.push(area);
cells.len() - 1
}
};
match what {
What::Local(local) => of_local[local] = cell,
What::Slot(slot) => of_slot[slot] = cell,
}
}
Slots { cells, locals: of_local, slots: of_slot }
}
fn spilled(allocation: &Allocation, slots: usize) -> Vec<Option<Reg>> {
let mut held = vec![None; slots];
for (reg, place) in allocation.assignment.placed() {
if let Place::Slot(slot) = place {
if let Some(at) = usize::try_from(slot).ok().and_then(|slot| held.get_mut(slot)) {
*at = Some(reg);
}
}
}
held
}
#[derive(Debug, Clone, Default)]
pub struct Reach {
through: Vec<Option<Carried>>,
}
impl Reach {
fn touches(&self, local: usize, live: &Live, order: &Order) -> Option<Vec<Range>> {
let held = self.through.get(local)?.as_ref()?;
let mut spots: Vec<Range> = Vec::new();
for ® in &held.regs {
spots.extend(live.area(reg).into_iter().flat_map(Area::pieces));
}
for &inst in &held.at {
spots.push(Range { start: order.early(inst), end: order.late(inst) });
}
Some(spots)
}
#[must_use]
pub fn shares(&self, local: usize) -> bool {
self.through.get(local).is_some_and(Option::is_some)
}
}
fn areas(func: &Func, reach: &Reach, live: &Live, order: &Order) -> Vec<Option<Vec<Range>>> {
let blocks = order.blocks();
let count = reach.through.len();
let words = count.div_ceil(64);
let starts: Vec<u32> = blocks.iter().map(|&block| order.start(block)).collect();
let holding = |point: u32| starts.partition_point(|&start| start <= point).saturating_sub(1);
let mut touched = vec![vec![0u64; words]; blocks.len()];
let mut inside: Vec<Vec<(usize, Range)>> = vec![Vec::new(); blocks.len()];
for local in 0..count {
let Some(spots) = reach.touches(local, live, order) else { continue };
for spot in spots {
for at in holding(spot.start)..=holding(spot.end) {
let block = blocks[at];
let start = spot.start.max(order.start(block));
let end = spot.end.min(order.end(block));
touched[at][local / 64] |= 1 << (local % 64);
inside[at].push((local, Range { start, end }));
}
}
}
for spots in inside.iter_mut() {
spots.sort_unstable_by_key(|&(local, Range { start, .. })| (local, start));
let mut kept = 0;
for at in 1..spots.len() {
if spots[at].0 == spots[kept].0 {
spots[kept].1.end = spots[kept].1.end.max(spots[at].1.end);
} else {
kept += 1;
spots[kept] = spots[at];
}
}
spots.truncate(spots.len().min(kept + 1));
}
let mut place = vec![0usize; func.block_count()];
for (at, &block) in blocks.iter().enumerate() {
place[block.index()] = at;
}
let mut ahead: Vec<Vec<usize>> = vec![Vec::new(); blocks.len()];
let mut behind: Vec<Vec<usize>> = vec![Vec::new(); blocks.len()];
for (at, &block) in blocks.iter().enumerate() {
for call in &func[block].succs {
let to = place[call.block.index()];
ahead[at].push(to);
behind[to].push(at);
}
}
let written = spread(&behind, &touched, words, true);
let read = spread(&ahead, &touched, words, false);
let mut out = vec![None; count];
for (local, pieces) in out.iter_mut().enumerate() {
if reach.shares(local) {
*pieces = Some(Vec::new());
}
}
for (at, &block) in blocks.iter().enumerate() {
let whole = Range { start: order.start(block), end: order.end(block) };
for word in 0..words {
let mut bits = written[at][word] & read[at][word];
while bits != 0 {
let local = word * 64 + bits.trailing_zeros() as usize;
bits &= bits - 1;
if let Some(pieces) = out[local].as_mut() {
pieces.push(whole);
}
}
}
for &(local, spot) in &inside[at] {
let held = |bits: &[Vec<u64>]| bits[at][local / 64] & (1 << (local % 64)) != 0;
let start = if held(&written) { whole.start } else { spot.start };
let end = if held(&read) { whole.end } else { spot.end };
if let Some(pieces) = out[local].as_mut() {
pieces.push(Range { start, end });
}
}
}
for pieces in out.iter_mut().flatten() {
*pieces = merged(std::mem::take(pieces));
}
out
}
fn spread(
edges: &[Vec<usize>],
touched: &[Vec<u64>],
words: usize,
forward: bool,
) -> Vec<Vec<u64>> {
let mut out = vec![vec![0u64; words]; edges.len()];
let mut going = true;
while going {
going = false;
for at in 0..edges.len() {
let at = if forward { at } else { edges.len() - 1 - at };
let mut row = out[at].clone();
for &from in &edges[at] {
for word in 0..words {
let had = row[word];
row[word] |= out[from][word] | touched[from][word];
going |= row[word] != had;
}
}
out[at] = row;
}
}
out
}
#[derive(Debug, Clone, Default)]
struct Carried {
regs: Vec<Reg>,
at: Vec<Inst>,
}
#[must_use]
pub fn reach(
func: &Func,
addresses: &[(Inst, usize)],
count: usize,
insts: &FrameInsts,
names: &mut Interner,
) -> Reach {
let lea = Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.lea)));
let mut through: Vec<Option<Carried>> = vec![None; count];
for &(inst, local) in addresses {
let Some(held) = through.get_mut(local) else { continue };
let held = held.get_or_insert_with(Carried::default);
if func[inst].opcode == lea {
match def(func, inst) {
Some(reg) => held.regs.push(reg),
None => {
through[local] = None;
continue;
}
}
}
held.at.push(inst);
}
let readers = readers(func);
let crossing = crossing(func);
for held in &mut through {
if let Some(carried) = held.take() {
*held = follow(func, lea, &readers, &crossing, carried);
}
}
Reach { through }
}
fn follow(
func: &Func,
lea: Opcode,
readers: &HashMap<Reg, Vec<Inst>>,
crossing: &HashSet<Reg>,
mut held: Carried,
) -> Option<Carried> {
let mut seen: HashSet<Reg> = held.regs.iter().copied().collect();
let mut queue = held.regs.clone();
while let Some(reg) = queue.pop() {
if crossing.contains(®) {
return None;
}
for &inst in readers.get(®).map(Vec::as_slice).unwrap_or_default() {
if !addressed(func, inst, reg) {
return None;
}
if func[inst].opcode == lea {
let next = def(func, inst)?;
if seen.insert(next) {
held.regs.push(next);
queue.push(next);
}
}
}
}
Some(held)
}
fn addressed(func: &Func, inst: Inst, reg: Reg) -> bool {
let data = &func[inst];
let Some(mem) = data.mem else { return false };
let amode = func[mem];
func[data.operands].iter().enumerate().all(|(at, operand)| {
if operand.reg != reg || operand.role.is_def() {
return true;
}
let at = u8::try_from(at).ok();
at.is_some() && (amode.base == at || amode.index == at)
})
}
fn def(func: &Func, inst: Inst) -> Option<Reg> {
let mut found = None;
for operand in &func[func[inst].operands] {
if !operand.role.is_def() {
continue;
}
if operand.reg.number().is_none() || found.is_some() {
return None;
}
found = Some(operand.reg);
}
found
}
fn readers(func: &Func) -> HashMap<Reg, Vec<Inst>> {
let mut readers: HashMap<Reg, Vec<Inst>> = HashMap::new();
for block in func.blocks() {
for inst in func.insts(block) {
for operand in &func[func[inst].operands] {
if operand.role.is_def() || operand.reg.number().is_none() {
continue;
}
let at = readers.entry(operand.reg).or_default();
if at.last() != Some(&inst) {
at.push(inst);
}
}
}
}
readers
}
fn crossing(func: &Func) -> HashSet<Reg> {
let mut crossing = HashSet::new();
for block in func.blocks() {
crossing.extend(func[block].params.iter().map(|param| param.reg));
for call in &func[block].succs {
crossing.extend(call.args.iter().copied());
}
}
crossing
}
fn moved(allocation: &Allocation, slots: usize) -> Vec<Vec<Range>> {
let order = &allocation.order;
let mut moved = vec![Vec::new(); slots];
for edit in &allocation.edits {
let at = match edit.at {
At::Before(inst) => order.early(inst),
At::After(inst) => order.late(inst),
At::StartOf(block) => order.start(block),
At::EndOf(block) => order.end(block),
};
let around =
Range { start: at.saturating_sub(1), end: at.saturating_add(1).min(order.points()) };
for place in [edit.mov.to, edit.mov.from] {
if let Place::Slot(slot) = place {
if let Some(at) = usize::try_from(slot).ok().and_then(|slot| moved.get_mut(slot)) {
at.push(around);
}
}
}
}
moved
}
fn merged(pieces: impl IntoIterator<Item = Range>) -> Vec<Range> {
let mut pieces: Vec<Range> = pieces.into_iter().collect();
pieces.sort_by_key(|piece| (piece.start, piece.end));
let mut merged: Vec<Range> = Vec::with_capacity(pieces.len());
for piece in pieces {
match merged.last_mut() {
Some(last) if piece.start <= last.end => last.end = last.end.max(piece.end),
_ => merged.push(piece),
}
}
merged
}
fn clashes(one: &[Range], two: &[Range]) -> bool {
let (mut mine, mut theirs) = (0, 0);
while mine < one.len() && theirs < two.len() {
if one[mine].overlaps(two[theirs]) {
return true;
}
if one[mine].end < two[theirs].end {
mine += 1;
} else {
theirs += 1;
}
}
false
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_mir::{Block, BlockCall, Mem, Operand};
use rucc_regalloc::assign::Env;
use rucc_target::x86_64::{FRAME, GPR, REGS, SYSV};
use super::*;
use crate::frame::{Frame, Layout};
struct Building {
names: Interner,
func: Func,
lea: Opcode,
nop: Opcode,
addresses: Vec<(Inst, usize)>,
}
impl Building {
fn new() -> (Self, Block) {
let mut names = Interner::new();
let func = Func::new(names.intern("f"));
let lea = Opcode::new(names.intern(&format!("{}{}", FRAME.prefix, FRAME.lea)));
let nop = Opcode::new(names.intern("x64.nop"));
let mut building = Self { names, func, lea, nop, addresses: Vec::new() };
let block = building.func.create_block();
(building, block)
}
fn local(&mut self, block: Block, which: usize) -> Reg {
let sp = Operand::read(Reg::physical(SYSV.stack_pointer), GPR);
let reg = self.func.new_vreg(GPR);
let inst = self.func.build(block, self.lea).def(reg, GPR).mem(Mem::at(sp)).finish();
self.addresses.push((inst, which));
reg
}
fn through(&mut self, block: Block, addr: Reg) {
let at = Operand::read(addr, GPR);
self.func.build(block, self.nop).mem(Mem::at(at)).finish();
}
fn held(&mut self, block: Block, reg: Reg) {
self.func.build(block, self.nop).uses(reg, GPR).finish();
}
fn value(&mut self, block: Block) -> Reg {
let reg = self.func.new_vreg(GPR);
self.func.build(block, self.nop).def(reg, GPR).finish();
reg
}
fn allocate(&mut self, locals: usize, registers: usize) -> (Reach, Allocation) {
let reach = reach(&self.func, &self.addresses, locals, &FRAME, &mut self.names);
let env =
Env::new().with(GPR, &SYSV.int_order[..registers], &SYSV.int_order[registers..]);
let allocation = rucc_regalloc::run(&mut self.func, &env, "test");
(reach, allocation)
}
}
const WORD: Local = Local { size: 8, align: 8 };
#[test]
fn two_locals_that_are_never_both_wanted_are_the_same_bytes() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
building.through(block, first);
let second = building.local(block, 1);
building.through(block, second);
let (reach, allocation) = building.allocate(2, 4);
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_eq!(plan.cells().len(), 1, "one run of bytes for the two of them");
assert_eq!(plan.local(0), plan.local(1));
assert_eq!(plan.saved(), 1);
}
#[test]
fn two_locals_that_are_both_wanted_at_once_are_not() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
let second = building.local(block, 1);
building.through(block, first);
building.through(block, second);
let (reach, allocation) = building.allocate(2, 4);
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_eq!(plan.cells().len(), 2);
assert_ne!(plan.local(0), plan.local(1));
assert_eq!(plan.saved(), 0);
}
#[test]
fn a_local_and_a_spilled_value_that_do_not_meet_share_one_run_of_bytes() {
let (mut building, block) = Building::new();
let addr = building.local(block, 0);
building.through(block, addr);
let values: Vec<Reg> = (0..3).map(|_| building.value(block)).collect();
for ® in &values {
building.held(block, reg);
}
let (reach, allocation) = building.allocate(1, 2);
assert_eq!(allocation.assignment.spilled(), 1, "one value went to the stack");
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD], &[8]);
assert_eq!(plan.cells().len(), 1);
assert_eq!(plan.local(0), plan.slot(0));
}
#[test]
fn a_local_whose_address_is_handed_to_something_shares_with_nothing() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
building.held(block, first);
let second = building.local(block, 1);
building.through(block, second);
let (reach, allocation) = building.allocate(2, 4);
assert!(!reach.shares(0), "an address that got away");
assert!(reach.shares(1));
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_eq!(plan.cells().len(), 2);
assert_ne!(plan.local(0), plan.local(1));
}
#[test]
fn a_local_whose_address_is_carried_into_a_block_shares_with_nothing() {
let (mut building, block) = Building::new();
let addr = building.local(block, 0);
let next = building.func.create_block();
let param = building.func.append_param(next, GPR);
building.func.build(block, building.nop).finish();
building.func.succs_mut(block).push(BlockCall::with(next, vec![addr]));
building.through(next, param);
let (reach, _) = building.allocate(1, 4);
assert!(!reach.shares(0), "an address that goes between blocks");
}
#[test]
fn a_local_touched_again_later_keeps_its_bytes_over_everything_in_between() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
building.through(block, first);
let second = building.local(block, 1);
building.through(block, second);
let again = building.local(block, 0);
building.through(block, again);
let (reach, allocation) = building.allocate(2, 4);
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_ne!(plan.local(0), plan.local(1));
assert_eq!(plan.saved(), 0);
}
#[test]
fn a_local_touched_in_a_loop_keeps_its_bytes_over_the_rest_of_the_loop() {
let (mut building, block) = Building::new();
let header = building.func.create_block();
let body = building.func.create_block();
building.func.build(block, building.nop).finish();
building.func.succs_mut(block).push(BlockCall::to(header));
let held = building.local(header, 1);
building.through(header, held);
building.func.build(header, building.nop).finish();
building.func.succs_mut(header).push(BlockCall::to(body));
let addr = building.local(body, 0);
building.through(body, addr);
building.func.build(body, building.nop).finish();
building.func.succs_mut(body).push(BlockCall::to(header));
let (reach, allocation) = building.allocate(2, 4);
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_ne!(plan.local(0), plan.local(1));
}
#[test]
fn a_block_that_is_its_own_neighbour_is_a_loop_like_any_other() {
let (mut building, block) = Building::new();
let loops = building.func.create_block();
building.func.build(block, building.nop).finish();
building.func.succs_mut(block).push(BlockCall::to(loops));
let held = building.local(loops, 1);
building.through(loops, held);
let addr = building.local(loops, 0);
building.through(loops, addr);
building.func.build(loops, building.nop).finish();
building.func.succs_mut(loops).push(BlockCall::to(loops));
let (reach, allocation) = building.allocate(2, 4);
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_ne!(plan.local(0), plan.local(1));
}
#[test]
fn an_address_a_second_address_computation_reads_is_the_same_local_followed_on() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
let derived = building.func.new_vreg(GPR);
let at = Operand::read(first, GPR);
building.func.build(block, building.lea).def(derived, GPR).mem(Mem::at(at)).finish();
let second = building.local(block, 1);
building.through(block, second);
building.through(block, derived);
let (reach, allocation) = building.allocate(2, 4);
assert!(reach.shares(0), "a derived address is still an address into this frame");
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_eq!(plan.cells().len(), 2, "the two locals are wanted at once after all");
}
#[test]
fn a_cell_two_things_share_is_as_wide_and_as_strict_as_both_of_them() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
building.through(block, first);
let second = building.local(block, 1);
building.through(block, second);
let (reach, allocation) = building.allocate(2, 4);
let narrow = Local { size: 4, align: 4 };
let wide = Local { size: 16, align: 16 };
let plan = Slots::share(&building.func, &reach, &allocation, &[narrow, wide], &[]);
assert_eq!(plan.cells(), [Cell { size: 16, align: 16 }]);
assert_eq!(plan.local(0), plan.local(1));
}
#[test]
fn a_local_nothing_on_the_address_list_names_shares_with_nothing() {
let (mut building, block) = Building::new();
let addr = building.local(block, 0);
building.through(block, addr);
let (reach, allocation) = building.allocate(2, 4);
assert!(!reach.shares(1));
let plan = Slots::share(&building.func, &reach, &allocation, &[WORD, WORD], &[]);
assert_eq!(plan.cells().len(), 2);
}
#[test]
fn the_frame_with_nothing_sharing_gives_every_local_and_every_slot_a_run_of_its_own() {
let plan = Slots::apart(&[WORD, Local { size: 4, align: 4 }], &[8, 16]);
assert_eq!(plan.cells().len(), 4);
assert_eq!(plan.saved(), 0);
assert_eq!((plan.local(0), plan.local(1)), (Some(0), Some(1)));
assert_eq!((plan.slot(0), plan.slot(1)), (Some(2), Some(3)));
assert_eq!(plan.cells()[3], Cell { size: 16, align: 16 });
}
#[test]
fn a_frame_whose_locals_share_is_smaller_and_puts_them_at_the_same_offset() {
let (mut building, block) = Building::new();
let first = building.local(block, 0);
building.through(block, first);
let second = building.local(block, 1);
building.through(block, second);
let (reach, allocation) = building.allocate(2, 4);
let locals = [Local { size: 64, align: 8 }; 2];
let base = Layout { leaf: false, locals: &locals, ..Layout::new(&SYSV, REGS) };
let apart = Frame::of(&building.func, &allocation, &base);
let plan = Slots::share(&building.func, &reach, &allocation, &locals, &[]);
let layout = Layout { share: Some(&plan), ..base };
let together = Frame::of(&building.func, &allocation, &layout);
assert_ne!(apart.local(0), apart.local(1));
assert_eq!(together.local(0), together.local(1));
assert_eq!((apart.size(), together.size()), (136, 72));
}
#[test]
fn a_run_of_bytes_that_ends_part_way_through_its_alignment_costs_the_frame_nothing() {
let (mut building, block) = Building::new();
let addr = building.local(block, 0);
building.through(block, addr);
let (_, allocation) = building.allocate(1, 4);
let ragged = Local { size: 24, align: 16 };
let whole = Local { size: 32, align: 16 };
let size = |locals: &[Local]| {
let layout = Layout { leaf: false, locals, ..Layout::new(&SYSV, REGS) };
Frame::of(&building.func, &allocation, &layout).size()
};
assert_eq!(size(&[ragged, whole]), size(&[whole, ragged]));
assert_eq!(size(&[ragged, whole]), 56);
}
#[test]
fn a_function_with_more_slots_than_anything_real_is_laid_out_the_old_way() {
let (mut building, block) = Building::new();
let addr = building.local(block, 0);
building.through(block, addr);
let (reach, allocation) = building.allocate(1, 4);
let locals = vec![WORD; CROWDED + 1];
let plan = Slots::share(&building.func, &reach, &allocation, &locals, &[]);
assert_eq!(plan.cells().len(), locals.len());
assert_eq!(plan.saved(), 0);
}
}