Skip to content

Commit

Permalink
Inline function return variables.
Browse files Browse the repository at this point in the history
This change replaces the default return variable `$0` with the variable
in the outer context where the return value will end up after leaving
the function. This saves us an instruction when we compile the trace.
More importantly however, this guards us against a future optimisation
in rustc that allows SIR to assign to $0 multiple times and at the
beginning of a block, which could lead to another function overwriting
its value (see rust-lang/rust#72205).
  • Loading branch information
ptersilie committed Jun 1, 2020
1 parent 9c91b43 commit 4a022d9
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 29 deletions.
38 changes: 19 additions & 19 deletions ykcompile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ pub struct TraceCompiler {
available_regs: Vec<u8>,
/// Maps locals to their assigned registers.
assigned_regs: HashMap<Local, u8>,
/// Stores the destination locals to which we copy RAX to after leaving an inlined call.
leaves: Vec<Option<Place>>,
/// Stores the destination local of the outermost function and moves its content into RAX at
/// the end of the trace.
rtn_var: Option<Place>,
}

impl TraceCompiler {
Expand Down Expand Up @@ -127,6 +128,15 @@ impl TraceCompiler {

fn free_register(&mut self, local: &Local) {
if let Some(reg) = self.assigned_regs.remove(local) {
if local == &self.rtn_var.as_ref().unwrap().local {
// We currently assume that we only trace a single function which leaves its return
// value in RAX. Since we now inline a function's return variable this won't happen
// automatically anymore. To keep things working, we thus copy the return value of
// the most outer function into RAX at the end of the trace.
dynasm!(self.asm
; mov rax, Rq(reg)
);
}
self.available_regs.push(reg);
}
}
Expand Down Expand Up @@ -217,19 +227,9 @@ impl TraceCompiler {
},
}
}
// Remember the return destination.
self.leaves.push(dest.as_ref().cloned());
Ok(())
}

fn c_leave(&mut self) -> Result<(), CompileError> {
let dest = self.leaves.pop();
if let Some(d) = dest {
if let Some(d) = d {
// When we see a leave statement move whatever's left in RAX into the destination
// local.
self.mov_local_local(d.local, Local(0))?;
}
if self.rtn_var.is_none() {
// Remember the return variable of the most outer function.
self.rtn_var = dest.as_ref().cloned();
}
Ok(())
}
Expand Down Expand Up @@ -313,7 +313,7 @@ impl TraceCompiler {
};
}
Statement::Enter(op, args, dest, off) => self.c_enter(op, args, dest, *off)?,
Statement::Leave => self.c_leave()?,
Statement::Leave => {}
Statement::StorageLive(_) => {}
Statement::StorageDead(l) => self.free_register(l),
Statement::Call(target, args, dest) => self.c_call(target, args, dest)?,
Expand Down Expand Up @@ -388,7 +388,7 @@ impl TraceCompiler {
// Use all the 64-bit registers we can (R15-R8, RDX, RCX).
available_regs: vec![15, 14, 13, 12, 11, 10, 9, 8, 2, 1],
assigned_regs: HashMap::new(),
leaves: Vec::new(),
rtn_var: None,
};

for i in 0..tt.len() {
Expand Down Expand Up @@ -452,7 +452,7 @@ mod tests {
asm: dynasmrt::x64::Assembler::new().unwrap(),
available_regs: vec![15, 14, 13, 12, 11, 10, 9, 8, 2, 1],
assigned_regs: HashMap::new(),
leaves: Vec::new(),
rtn_var: None,
};

for _ in 0..32 {
Expand All @@ -470,7 +470,7 @@ mod tests {
asm: dynasmrt::x64::Assembler::new().unwrap(),
available_regs: vec![15, 14, 13, 12, 11, 10, 9, 8, 2, 1],
assigned_regs: HashMap::new(),
leaves: Vec::new(),
rtn_var: None,
};

let mut seen = HashSet::new();
Expand Down
30 changes: 20 additions & 10 deletions yktrace/src/tir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,16 @@ impl TirTrace {
let newdest = dest
.as_ref()
.map(|(ret_val, _ret_bb)| rnm.rename_place(&ret_val));
// Rename all `Local`s within the arguments.
let newargs = rnm.rename_args(&args);
// Inform VarRenamer about this function's offset, which is equal to the
// number of variables assigned in the outer body.
rnm.enter(body.num_locals);
// FIXME It seems that calls always have a destination despite it being
// an `Option`. If this is not always the case, we may want add the
// `Local` offset (`var_len`) to this statement so we can assign the
// arguments to the correct `Local`s during trace compilation.
assert!(newdest.is_some());
// Rename all `Local`s within the arguments.
let newargs = rnm.rename_args(&args);
// Inform VarRenamer about this function's offset, which is equal to the
// number of variables assigned in the outer body.
rnm.enter(body.num_locals, newdest.as_ref().unwrap().clone());
TirOp::Statement(Statement::Enter(
op.clone(),
newargs,
Expand Down Expand Up @@ -313,33 +313,37 @@ struct VarRenamer {
/// restored again after leaving that call.
stack: Vec<u32>,
/// Current offset used to rename variables.
offset: u32
offset: u32,
returns: Vec<Place>
}

impl VarRenamer {
fn new() -> Self {
VarRenamer {
stack: vec![0],
offset: 0
offset: 0,
returns: Vec::new()
}
}

fn offset(&self) -> u32 {
self.offset
}

fn enter(&mut self, num_locals: usize) {
fn enter(&mut self, num_locals: usize, dest: Place) {
// When entering an inlined function call update the current offset by adding the number of
// assigned variables in the outer context. Also add this offset to the stack, so we can
// restore it once we leave the inlined function call again.
self.offset += num_locals as u32;
self.stack.push(self.offset);
self.returns.push(dest);
}

fn leave(&mut self) {
// When we leave an inlined function call, we pop the previous offset from the stack,
// reverting the offset to what it was before the function was entered.
self.stack.pop();
self.returns.pop();
if let Some(v) = self.stack.last() {
self.offset = *v;
} else {
Expand Down Expand Up @@ -380,8 +384,14 @@ impl VarRenamer {

fn rename_place(&self, place: &Place) -> Place {
if &place.local == &Local(0) {
// Local(0) is used for returning values from calls, so it doesn't need to be renamed.
place.clone()
// Replace the default return variable $0 with the variable in the outer context where
// the return value will end up after leaving the function. This saves us an
// instruction when we compile the trace.
if let Some(v) = self.returns.last() {
v.clone()
} else {
panic!("Expected return value!")
}
} else {
let mut p = place.clone();
p.local = self.rename_local(&p.local);
Expand Down

0 comments on commit 4a022d9

Please sign in to comment.