use std::collections::{HashMap, HashSet};
use rucc_base::Interner;
use rucc_ir::{Abi, CallInfo, Extra, Func, FuncId, Inst, Module, Opcode, Signature, Value};
use crate::ipa::Sites;
use crate::purity::Facts;
use crate::stats::Kind;
use crate::{CallGraph, Fuel, Stats, dce, ipa, purity};
const GONE: &str = "parameter nothing reads removed, and the argument at every call with it";
const VOIDED: &str = "return value no call reads removed, and the result at every call with it";
const NO_FUEL_RETURN: &str = "return value left alone, the pass ran out of fuel";
const NO_FUEL: &str = "parameter left alone, the pass ran out of fuel";
pub const NAME: &str = "ipa-sra";
const ROUNDS: usize = 3;
pub fn remove(
module: &mut Module,
graph: &CallGraph,
names: &Interner,
fuel: &mut Fuel,
) -> Vec<(FuncId, Stats)> {
let closed = ipa::closed(module, graph);
if closed.is_empty() {
return Vec::new();
}
let mut facts = Facts::of_module(module, names);
purity::infer(module, graph, &mut facts);
let mut stats: HashMap<FuncId, Stats> = HashMap::new();
for _ in 0..ROUNDS {
let changed = round(module, &closed, fuel, &mut stats);
if changed.is_empty() {
break;
}
for id in changed {
dce::dce_in(&mut module[id], &facts, &mut Fuel::unlimited());
stats.entry(id).or_default();
}
}
module.funcs().filter_map(|id| Some((id, stats.remove(&id)?))).collect()
}
fn round(
module: &mut Module,
closed: &[FuncId],
fuel: &mut Fuel,
stats: &mut HashMap<FuncId, Stats>,
) -> Vec<FuncId> {
let sites = ipa::sites(module, closed);
let mut changed: Vec<FuncId> = Vec::new();
for &id in closed {
let Some(calls) = sites.get(&id) else { continue };
if calls.ragged || calls.calls.is_empty() {
continue;
}
let mut keep = read(&module[id]);
if keep.iter().all(|&it| it) {
continue;
}
let (mut gone, mut denied) = (0_u32, 0_u32);
for keeping in &mut keep {
if *keeping {
continue;
}
if fuel.take() {
gone += 1;
} else {
*keeping = true;
denied += 1;
}
}
let counted = stats.entry(id).or_default();
counted.record(Kind::Optimized, GONE, gone);
counted.record(Kind::Missed, NO_FUEL, denied);
if gone == 0 {
continue;
}
let signature = trim(module[id].signature(), &keep);
narrow(&mut module[id], signature.clone(), &keep);
for &(caller, at) in &calls.calls {
shorten(&mut module[caller], at, signature.clone(), &keep);
note(&mut changed, caller);
}
}
void_returns(module, closed, &sites, fuel, stats, &mut changed);
changed
}
fn void_returns(
module: &mut Module,
closed: &[FuncId],
sites: &HashMap<FuncId, Sites>,
fuel: &mut Fuel,
stats: &mut HashMap<FuncId, Stats>,
changed: &mut Vec<FuncId>,
) {
let mut reads: HashMap<FuncId, HashSet<Value>> = HashMap::new();
for &id in closed {
let Some(calls) = sites.get(&id) else { continue };
if calls.ragged || calls.calls.is_empty() || !returning(&module[id]) {
continue;
}
if !calls.calls.iter().all(|&(caller, at)| plain_call(module, caller, at)) {
continue;
}
let mut unread = true;
for &(caller, at) in &calls.calls {
let named = reads.entry(caller).or_insert_with(|| ipa::operands(&module[caller]));
let handed = module[caller][at].first_result.expect("a call that returns a value");
if named.contains(&handed) {
unread = false;
break;
}
}
if !unread {
continue;
}
let counted = stats.entry(id).or_default();
if !fuel.take() {
counted.record(Kind::Missed, NO_FUEL_RETURN, 1);
continue;
}
counted.record(Kind::Optimized, VOIDED, 1);
let mut signature = module[id].signature().clone();
signature.returns.clear();
void(&mut module[id], signature.clone());
note(changed, id);
for &(caller, at) in &calls.calls {
discard(&mut module[caller], at, signature.clone());
note(changed, caller);
}
}
}
fn returning(func: &Func) -> bool {
let returns = &func.signature().returns;
let [only] = returns.as_slice() else { return false };
if !matches!(only.abi, Abi::Plain | Abi::Sext | Abi::Zext) {
return false;
}
func.blocks().all(|block| func.insts(block).all(|inst| func[inst].opcode != Opcode::TailCall))
}
fn plain_call(module: &Module, caller: FuncId, at: Inst) -> bool {
module[caller][at].opcode == Opcode::Call && module[caller][at].first_result.is_some()
}
fn note(changed: &mut Vec<FuncId>, id: FuncId) {
if !changed.contains(&id) {
changed.push(id);
}
}
fn void(func: &mut Func, signature: Signature) {
func.set_signature(signature);
let returns: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block))
.filter(|&inst| func[inst].opcode == Opcode::Return)
.collect();
for inst in returns {
let kept: Vec<Value> = func.mem_in(inst).into_iter().collect();
let args = func.push_values(&kept);
func[inst].args = args;
}
}
fn discard(func: &mut Func, inst: Inst, signature: Signature) {
let Extra::Call(at) = func[inst].extra else { return };
let signature = func.add_signature(signature);
let info = CallInfo { signature, ..func[at] };
let call = func.add_call(info);
func[inst].extra = Extra::Call(call);
func.drop_results(inst, 1);
}
fn read(func: &Func) -> Vec<bool> {
let Some(entry) = func.entry() else { return vec![true; func.signature().params.len()] };
let named = ipa::operands(func);
func[entry].params.iter().map(|value| named.contains(value)).collect()
}
fn trim(signature: &Signature, keep: &[bool]) -> Signature {
let mut trimmed = signature.clone();
trimmed.params =
signature.params.iter().zip(keep).filter(|&(_, &keep)| keep).map(|(&it, _)| it).collect();
trimmed
}
fn narrow(func: &mut Func, signature: Signature, keep: &[bool]) {
let entry = func.entry().expect("a closed function has an entry block");
func.set_signature(signature);
let mut index = 0;
func.retain_params(entry, |_| {
let keeping = keep[index];
index += 1;
keeping
});
}
fn shorten(func: &mut Func, inst: Inst, signature: Signature, keep: &[bool]) {
let Extra::Call(at) = func[inst].extra else { return };
let args: Vec<Value> = func[func[inst].args]
.iter()
.zip(keep)
.filter(|&(_, &keep)| keep)
.map(|(&it, _)| it)
.collect();
let signature = func.add_signature(signature);
let info = CallInfo { signature, ..func[at] };
let call = func.add_call(info);
let args = func.push_values(&args);
func[inst].args = args;
func[inst].extra = Extra::Call(call);
}
#[cfg(test)]
mod tests {
use rucc_base::Symbol;
use rucc_ir::{Builder, Def, Flags, InstData, Linkage, Opcode, Pic, Type};
use rucc_target::{TargetInfo, Triple};
use super::*;
const INT: Type = Type::int(32);
const BIT: Type = Type::int(1);
struct Unit {
names: Interner,
module: Module,
side: Symbol,
}
impl Unit {
fn new() -> Self {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let module = Module::new(names.intern("t.c"), &target);
let side = names.intern("side");
Self { names, module, side }
}
fn name(&mut self, name: &str) -> Symbol {
self.names.intern(name)
}
fn side(&self) -> Symbol {
self.side
}
fn add(
&mut self,
name: Symbol,
linkage: Linkage,
signature: Signature,
body: impl FnOnce(&mut Builder<'_>, &[Value]),
) {
let params = signature.params.iter().map(|it| it.ty).collect::<Vec<Type>>();
let mut func = Func::new(name, signature);
func.linkage = linkage;
let block = func.create_block();
let values: Vec<Value> =
params.into_iter().map(|ty| func.append_param(block, ty)).collect();
let mut build = Builder::new(&mut func, block);
body(&mut build, &values);
build.ret(&[]);
self.module.add_func(func);
}
fn private(
&mut self,
name: &str,
params: &[Type],
body: impl FnOnce(&mut Builder<'_>, &[Value]),
) -> Symbol {
let name = self.name(name);
self.add(name, Linkage::Internal, Signature::new().with_params(params), body);
name
}
fn outside(&mut self, body: impl FnOnce(&mut Builder<'_>, &[Value])) -> Symbol {
let name = self.name("f");
self.add(name, Linkage::External, Signature::new(), body);
name
}
fn remove(&mut self) -> Vec<(FuncId, Stats)> {
self.run(&mut Fuel::unlimited())
}
fn run(&mut self, fuel: &mut Fuel) -> Vec<(FuncId, Stats)> {
let graph = CallGraph::of(&self.module, Pic::Executable);
remove(&mut self.module, &graph, &self.names, fuel)
}
fn func(&self, at: Symbol) -> &Func {
let id = self.module.funcs().find(|&id| self.module[id].name == at);
&self.module[id.expect("the module has a function of that name")]
}
fn shape(&self, at: Symbol) -> (Vec<Type>, Vec<Type>) {
let func = self.func(at);
let entry = func.entry().expect("a function with a body");
let block = func[entry].params.iter().map(|&it| func[it].ty).collect();
(func.signature().param_types().collect(), block)
}
fn passes(&self, at: Symbol, to: Symbol) -> Vec<i128> {
let func = self.func(at);
for block in func.blocks() {
for inst in func.insts(block) {
let Extra::Call(call) = func[inst].extra else { continue };
if func[call].callee != Some(to) {
continue;
}
return func[func[inst].args].iter().map(|&it| number(func, it)).collect();
}
}
panic!("the caller still has a call to that function")
}
fn counts(&self, at: Symbol, opcode: Opcode) -> usize {
let func = self.func(at);
func.blocks()
.flat_map(|block| func.insts(block))
.filter(|&inst| func[inst].opcode == opcode)
.count()
}
fn verified(&self) {
for id in self.module.funcs() {
let done = rucc_ir::verify_func(&self.module, &self.module[id], &self.names);
assert!(done.is_ok(), "{done:?}");
}
}
fn handing_back(
&mut self,
name: &str,
linkage: Linkage,
params: &[Type],
body: impl FnOnce(&mut Builder<'_>, &[Value]) -> Value,
) -> Symbol {
let name = self.name(name);
let signature = Signature::new().with_params(params).with_returns(&[INT]);
let mut func = Func::new(name, signature);
func.linkage = linkage;
let block = func.create_block();
let values: Vec<Value> =
params.iter().map(|&ty| func.append_param(block, ty)).collect();
let mut build = Builder::new(&mut func, block);
let answer = body(&mut build, &values);
build.ret(&[answer]);
self.module.add_func(func);
name
}
fn gives(&self, at: Symbol) -> Vec<Type> {
self.func(at).signature().return_types().collect()
}
fn produces(&self, at: Symbol, to: Symbol) -> usize {
let func = self.func(at);
for block in func.blocks() {
for inst in func.insts(block) {
let Extra::Call(call) = func[inst].extra else { continue };
if func[call].callee != Some(to) {
continue;
}
return func[inst].results().count();
}
}
panic!("the caller still has a call to that function")
}
fn hands_over(&self, at: Symbol) -> Vec<usize> {
let func = self.func(at);
func.blocks()
.flat_map(|block| func.insts(block))
.filter(|&inst| func[inst].opcode == Opcode::Return)
.map(|inst| func[func[inst].args].len())
.collect()
}
fn said(&self, done: &[(FuncId, Stats)], at: Symbol) -> Stats {
done.iter()
.find(|(id, _)| self.module[*id].name == at)
.map_or_else(Stats::new, |(_, stats)| stats.clone())
}
}
fn number(func: &Func, value: Value) -> i128 {
let Def::Result { inst, .. } = func[value].def else {
panic!("an argument that is a number")
};
let Extra::Imm(at) = func[inst].extra else { panic!("an argument that is a number") };
func[at].signed(func[value].ty)
}
fn opaque(build: &mut Builder<'_>, side: Symbol) {
let signature = build.func().add_signature(Signature::new());
build.call(side, signature, &[]);
}
fn call_reading(build: &mut Builder<'_>, at: Symbol, params: &[Type], args: &[Value]) -> Value {
let signature = Signature::new().with_params(params).with_returns(&[Type::int(32)]);
let signature = build.func().add_signature(signature);
let inst = build.call(at, signature, args);
build.func()[inst].results().next().expect("a call that hands a value back")
}
fn call(build: &mut Builder<'_>, at: Symbol, params: &[Type], args: &[Value]) {
let signature = build.func().add_signature(Signature::new().with_params(params));
build.call(at, signature, args);
}
#[test]
fn a_parameter_nothing_in_the_body_reads_goes_and_so_does_the_argument() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, _| opaque(build, side));
let f = unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
let done = unit.remove();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, GONE), 1);
assert_eq!(unit.shape(g), (Vec::new(), Vec::new()));
assert_eq!(unit.passes(f, g), Vec::<i128>::new());
assert_eq!(unit.counts(f, Opcode::IConst), 0, "the seven was only there for the call");
unit.verified();
}
#[test]
fn a_parameter_the_body_reads_stays() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, params| {
build.binary(Opcode::SDiv, params[0], params[0], Flags::NONE);
opaque(build, side);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn the_parameters_that_stay_keep_their_order_and_the_arguments_they_were_passed() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT, INT, INT], |build, params| {
build.binary(Opcode::SDiv, params[1], params[1], Flags::NONE);
opaque(build, side);
});
let f = unit.outside(|build, _| {
let one = build.iconst(INT, 1);
let two = build.iconst(INT, 2);
let three = build.iconst(INT, 3);
call(build, g, &[INT, INT, INT], &[one, two, three]);
});
let done = unit.remove();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, GONE), 2);
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
assert_eq!(unit.passes(f, g), vec![2]);
unit.verified();
}
#[test]
fn a_value_the_caller_worked_out_only_for_the_argument_goes_with_it() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, _| opaque(build, side));
let f = unit.outside(|build, _| {
let three = build.iconst(INT, 3);
let five = build.iconst(INT, 5);
let product = build.binary(Opcode::Mul, three, five, Flags::NONE);
call(build, g, &[INT], &[product]);
});
unit.remove();
assert_eq!(unit.counts(f, Opcode::Mul), 0);
assert_eq!(unit.counts(f, Opcode::IConst), 0);
}
#[test]
fn the_sum_two_parameters_were_only_used_in_takes_all_three_away() {
let mut unit = Unit::new();
let side = unit.side();
let deep = unit.private("deep", &[INT], |build, _| opaque(build, side));
let mid = unit.private("mid", &[INT, INT], |build, params| {
let sum = build.binary(Opcode::Add, params[0], params[1], Flags::NONE);
call(build, deep, &[INT], &[sum]);
opaque(build, side);
});
let f = unit.outside(|build, _| {
let one = build.iconst(INT, 1);
let two = build.iconst(INT, 2);
call(build, mid, &[INT, INT], &[one, two]);
});
let done = unit.remove();
assert_eq!(unit.said(&done, deep).count(Kind::Optimized, GONE), 1);
assert_eq!(unit.said(&done, mid).count(Kind::Optimized, GONE), 2);
assert_eq!(unit.shape(deep), (Vec::new(), Vec::new()));
assert_eq!(unit.shape(mid), (Vec::new(), Vec::new()));
assert_eq!(unit.counts(mid, Opcode::Add), 0);
assert_eq!(unit.passes(f, mid), Vec::<i128>::new());
unit.verified();
}
#[test]
fn a_function_another_object_can_call_is_left_alone() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.name("g");
unit.add(g, Linkage::External, Signature::new().with_params(&[INT]), |build, _| {
opaque(build, side);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn a_function_whose_address_this_unit_hands_out_is_left_alone() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, _| opaque(build, side));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
let extra = Extra::Symbol(g);
build.value(InstData { extra, ..InstData::new(Opcode::GlobalAddr) }, Type::PTR);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn a_function_nothing_in_the_unit_calls_is_left_alone() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, _| opaque(build, side));
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn a_variadic_function_is_left_alone() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.name("g");
let mut signature = Signature::new().with_params(&[INT]);
signature.variadic = true;
unit.add(g, Linkage::Internal, signature, |build, _| opaque(build, side));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn a_call_passing_the_wrong_number_of_arguments_stops_the_removal() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT], |build, _| opaque(build, side));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
call(build, g, &[INT, INT], &[seven, seven]);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn a_parameter_only_the_recursive_call_hands_on_is_left_alone() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.name("g");
unit.add(g, Linkage::Internal, Signature::new().with_params(&[INT]), |build, params| {
call(build, g, &[INT], &[params[0]]);
opaque(build, side);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.remove().is_empty());
assert_eq!(unit.shape(g), (vec![INT], vec![INT]));
}
#[test]
fn without_fuel_the_parameters_stay_and_the_chances_are_still_counted() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.private("g", &[INT, INT], |build, _| opaque(build, side));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT, INT], &[seven, seven]);
});
let done = unit.run(&mut Fuel::of(0));
assert_eq!(unit.said(&done, g).count(Kind::Missed, NO_FUEL), 2);
assert_eq!(unit.said(&done, g).count(Kind::Optimized, GONE), 0);
assert_eq!(unit.shape(g), (vec![INT, INT], vec![INT, INT]));
}
#[test]
fn a_return_value_no_call_reads_goes_and_so_does_the_result() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::Internal, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
let f = unit.outside(|build, _| {
call_reading(build, g, &[], &[]);
});
let done = unit.remove();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, VOIDED), 1);
assert_eq!(unit.gives(g), Vec::new());
assert_eq!(unit.produces(f, g), 0);
assert_eq!(unit.hands_over(g), vec![0]);
unit.verified();
}
#[test]
fn the_value_the_body_was_computing_for_the_return_goes_with_it() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::Internal, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
unit.outside(|build, _| {
call_reading(build, g, &[], &[]);
});
unit.remove();
assert_eq!(unit.counts(g, Opcode::IConst), 0, "the three was only there for the return");
unit.verified();
}
#[test]
fn a_return_value_a_call_reads_stays() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::Internal, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
let h = unit.private("h", &[INT], |build, params| {
build.binary(Opcode::SDiv, params[0], params[0], Flags::NONE);
opaque(build, side);
});
unit.outside(|build, _| {
let answer = call_reading(build, g, &[], &[]);
call(build, h, &[INT], &[answer]);
});
unit.remove();
assert_eq!(unit.gives(g), vec![INT]);
assert_eq!(unit.hands_over(g), vec![1]);
unit.verified();
}
#[test]
fn every_return_in_the_body_stops_handing_a_value_over() {
let mut unit = Unit::new();
let side = unit.side();
let name = unit.name("g");
let signature = Signature::new().with_params(&[BIT]).with_returns(&[INT]);
let mut func = Func::new(name, signature);
func.linkage = Linkage::Internal;
let entry = func.create_block();
let yes = func.create_block();
let no = func.create_block();
let cond = func.append_param(entry, BIT);
let mut build = Builder::new(&mut func, entry);
opaque(&mut build, side);
build.br_if(cond, yes, &[], no, &[]);
let mut build = Builder::new(&mut func, yes);
let three = build.iconst(INT, 3);
build.ret(&[three]);
let mut build = Builder::new(&mut func, no);
let four = build.iconst(INT, 4);
build.ret(&[four]);
unit.module.add_func(func);
let g = name;
unit.outside(|build, _| {
let one = build.iconst(BIT, 1);
call_reading(build, g, &[BIT], &[one]);
});
unit.remove();
assert_eq!(unit.gives(g), Vec::new());
assert_eq!(unit.hands_over(g), vec![0, 0]);
unit.verified();
}
#[test]
fn a_function_another_object_can_call_keeps_its_return_value() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::External, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
unit.outside(|build, _| {
call_reading(build, g, &[], &[]);
});
unit.remove();
assert_eq!(unit.gives(g), vec![INT]);
}
#[test]
fn a_function_whose_address_this_unit_hands_out_keeps_its_return_value() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::Internal, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
unit.outside(|build, _| {
call_reading(build, g, &[], &[]);
let extra = Extra::Symbol(g);
build.value(InstData { extra, ..InstData::new(Opcode::GlobalAddr) }, Type::PTR);
});
unit.remove();
assert_eq!(unit.gives(g), vec![INT]);
}
#[test]
fn without_fuel_the_return_value_stays_and_the_chance_is_still_counted() {
let mut unit = Unit::new();
let side = unit.side();
let g = unit.handing_back("g", Linkage::Internal, &[], |build, _| {
opaque(build, side);
build.iconst(INT, 3)
});
unit.outside(|build, _| {
call_reading(build, g, &[], &[]);
});
let done = unit.run(&mut Fuel::of(0));
assert_eq!(unit.said(&done, g).count(Kind::Optimized, VOIDED), 0);
assert_eq!(unit.said(&done, g).count(Kind::Missed, NO_FUEL_RETURN), 1);
assert_eq!(unit.gives(g), vec![INT]);
unit.verified();
}
}