Skip to content

Commit

Permalink
Add function calls to the back end. (FuelLabs#2843)
Browse files Browse the repository at this point in the history
Introduce function calls to the ASMgen.

- Still need to add support for ref-type return values and passing excess args on the stack.
  • Loading branch information
otrho authored Oct 5, 2022
1 parent 9673b41 commit 3a063c6
Show file tree
Hide file tree
Showing 99 changed files with 5,446 additions and 4,320 deletions.
324 changes: 143 additions & 181 deletions sway-core/src/asm_generation/abstract_instruction_set.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::{
asm_generation::{register_allocator, DataSection, InstructionSet, RegisterSequencer},
asm_generation::{register_allocator, AllocatedAbstractInstructionSet, RegisterSequencer},
asm_lang::{
allocated_ops::AllocatedOp, Label, Op, OrganizationalOp, RealizedOp, VirtualImmediate12,
VirtualImmediate18, VirtualImmediate24, VirtualOp,
allocated_ops::AllocatedOp, AllocatedAbstractOp, Op, OrganizationalOp, RealizedOp,
VirtualOp, VirtualRegister,
},
};
use std::{collections::HashMap, fmt};

use std::{collections::BTreeSet, fmt};

use either::Either;

Expand All @@ -17,184 +18,102 @@ pub struct AbstractInstructionSet {
}

impl AbstractInstructionSet {
pub(crate) fn optimize(self) -> AbstractInstructionSet {
self.remove_sequential_jumps()
.remove_redundant_moves()
.remove_unused_ops()
}

/// Removes any jumps that jump to the subsequent line
pub(crate) fn remove_sequential_jumps(&self) -> AbstractInstructionSet {
let mut buf = vec![];
for i in 0..self.ops.len() - 1 {
if let Op {
opcode: Either::Right(OrganizationalOp::Jump(ref label)),
..
} = self.ops[i]
{
if let Op {
opcode: Either::Right(OrganizationalOp::Label(ref label2)),
..
} = self.ops[i + 1]
{
if label == label2 {
// this is a jump to the next line
// omit these by doing nothing
continue;
}
}
}
buf.push(self.ops[i].clone());
fn remove_sequential_jumps(mut self) -> AbstractInstructionSet {
let dead_jumps: Vec<_> = self
.ops
.windows(2)
.enumerate()
.filter_map(|(idx, ops)| match (&ops[0].opcode, &ops[1].opcode) {
(
Either::Right(OrganizationalOp::Jump(dst_label)),
Either::Right(OrganizationalOp::Label(label)),
) if dst_label == label => Some(idx),
_otherwise => None,
})
.collect();

// Replace the dead jumps with NOPs, as it's cheaper.
for idx in dead_jumps {
self.ops[idx] = Op {
opcode: Either::Left(VirtualOp::NOOP),
comment: "removed redundant JUMP".into(),
owning_span: None,
};
}
// the last item cannot sequentially jump by definition so we add it in here
if let Some(x) = self.ops.last() {
buf.push(x.clone())
};

// scan through the jumps and remove any labels that are unused
// this could of course be N instead of 2N if i did this in the above for loop.
// However, the sweep for unused labels is inevitable regardless of the above phase
// so might as well do it here.
let mut buf2 = vec![];
for op in &buf {
match op.opcode {
Either::Right(OrganizationalOp::Label(ref label)) => {
if label_is_used(&buf, label) {
buf2.push(op.clone());

self
}

fn remove_redundant_moves(mut self) -> AbstractInstructionSet {
// This has a lot of room for improvement.
//
// For now it is just removing MOVEs to registers which are _never_ used. It doesn't
// analyse control flow or other redundancies. Some obvious improvements are:
//
// - Perform a control flow analysis to remove MOVEs to registers which are not used
// _after_ the MOVE.
//
// - Remove the redundant use of temporaries. E.g.:
// MOVE t, a MOVE b, a
// MOVE b, t => USE b
// USE b

loop {
// Gather all the uses for each register.
let uses: BTreeSet<&VirtualRegister> =
self.ops.iter().fold(BTreeSet::new(), |mut acc, op| {
acc.append(&mut op.use_registers());
acc
});

// Loop again and find MOVEs which have a non-constant destination which is never used.
let mut dead_moves = Vec::new();
for (idx, op) in self.ops.iter().enumerate() {
if let Either::Left(VirtualOp::MOVE(
dst_reg @ VirtualRegister::Virtual(_),
_src_reg,
)) = &op.opcode
{
if !uses.contains(dst_reg) {
dead_moves.push(idx);
}
}
_ => buf2.push(op.clone()),
}
}

AbstractInstructionSet { ops: buf2 }
}
if dead_moves.is_empty() {
break;
}

/// Runs two passes -- one to get the instruction offsets of the labels
/// and one to replace the labels in the organizational ops
pub(crate) fn realize_labels(
self,
data_section: &DataSection,
) -> RealizedAbstractInstructionSet {
let mut label_namespace: HashMap<&Label, u64> = Default::default();
let mut offset_map = vec![];
let mut counter = 0;
for op in &self.ops {
offset_map.push(counter);
match op.opcode {
Either::Right(OrganizationalOp::Label(ref lab)) => {
label_namespace.insert(lab, counter);
}
// A special case for LWDataId which may be 1 or 2 ops, depending on the source size.
Either::Left(VirtualOp::LWDataId(_, ref data_id)) => {
let type_of_data = data_section.type_of_data(data_id).expect(
"Internal miscalculation in data section -- data id did not match up to any actual data",
);
counter += if type_of_data.is_copy_type() { 1 } else { 2 };
}
// these ops will end up being exactly one op, so the counter goes up one
Either::Right(OrganizationalOp::Jump(..))
| Either::Right(OrganizationalOp::JumpIfNotEq(..))
| Either::Right(OrganizationalOp::JumpIfNotZero(..))
| Either::Left(_) => {
counter += 1;
}
Either::Right(OrganizationalOp::Comment) => (),
Either::Right(OrganizationalOp::DataSectionOffsetPlaceholder) => {
// If the placeholder is 32 bits, this is 1. if 64, this should be 2. We use LW
// to load the data, which loads a whole word, so for now this is 2.
counter += 2
}
// Replace the dead moves with NOPs, as it's cheaper.
for idx in dead_moves {
self.ops[idx] = Op {
opcode: Either::Left(VirtualOp::NOOP),
comment: "removed redundant MOVE".into(),
owning_span: None,
};
}
}

let mut realized_ops = vec![];
for (
ix,
Op {
opcode,
owning_span,
comment,
},
) in self.ops.clone().into_iter().enumerate()
{
let offset = offset_map[ix];
match opcode {
Either::Left(op) => realized_ops.push(RealizedOp {
opcode: op,
owning_span,
comment,
offset,
}),
Either::Right(org_op) => match org_op {
OrganizationalOp::Jump(ref lab) => {
let imm = VirtualImmediate24::new_unchecked(
*label_namespace.get(lab).unwrap(),
"Programs with more than 2^24 labels are unsupported right now",
);
realized_ops.push(RealizedOp {
opcode: VirtualOp::JI(imm),
owning_span,
comment,
offset,
});
}
OrganizationalOp::JumpIfNotEq(r1, r2, ref lab) => {
let imm = VirtualImmediate12::new_unchecked(
*label_namespace.get(lab).unwrap(),
"Programs with more than 2^12 labels are unsupported right now",
);
realized_ops.push(RealizedOp {
opcode: VirtualOp::JNEI(r1, r2, imm),
owning_span,
comment,
offset,
});
}
OrganizationalOp::JumpIfNotZero(r1, ref lab) => {
let imm = VirtualImmediate18::new_unchecked(
*label_namespace.get(lab).unwrap(),
"Programs with more than 2^18 labels are unsupported right now",
);
realized_ops.push(RealizedOp {
opcode: VirtualOp::JNZI(r1, imm),
owning_span,
comment,
offset,
});
}
OrganizationalOp::DataSectionOffsetPlaceholder => {
realized_ops.push(RealizedOp {
opcode: VirtualOp::DataSectionOffsetPlaceholder,
owning_span: None,
comment: String::new(),
offset,
});
}
OrganizationalOp::Comment => continue,
OrganizationalOp::Label(..) => continue,
},
};
}
RealizedAbstractInstructionSet { ops: realized_ops }
self
}
}

impl fmt::Display for AbstractInstructionSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
".program:\n{}",
self.ops
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<_>>()
.join("\n")
)
}
}
fn remove_unused_ops(mut self) -> AbstractInstructionSet {
// Just remove NOPs for now.
self.ops.retain(|op| match &op.opcode {
Either::Left(VirtualOp::NOOP) => false,
_otherwise => true,
});

/// "Realized" here refers to labels -- there are no more organizational
/// ops or labels. In this struct, they are all "realized" to offsets.
pub struct RealizedAbstractInstructionSet {
ops: Vec<RealizedOp>,
}
self
}

impl RealizedAbstractInstructionSet {
/// Assigns an allocatable register to each virtual register used by some instruction in the
/// list `self.ops`. The algorithm used is Chaitin's graph-coloring register allocation
/// algorithm (https://en.wikipedia.org/wiki/Chaitin%27s_algorithm). The individual steps of
Expand All @@ -203,7 +122,7 @@ impl RealizedAbstractInstructionSet {
pub(crate) fn allocate_registers(
self,
register_sequencer: &mut RegisterSequencer,
) -> InstructionSet {
) -> AllocatedAbstractInstructionSet {
// Step 1: Liveness Analysis.
let live_out = register_allocator::liveness_analysis(&self.ops);

Expand All @@ -226,26 +145,69 @@ impl RealizedAbstractInstructionSet {
// Step 5: Use the stack to assign a register for each virtual register.
let pool = register_allocator::assign_registers(&mut stack);

// Steph 6: Update all instructions to use the resulting register pool.
// Step 6: Update all instructions to use the resulting register pool.
let mut buf = vec![];
for op in &reduced_ops {
buf.push(AllocatedOp {
opcode: op.opcode.allocate_registers(&pool),
buf.push(AllocatedAbstractOp {
opcode: op.allocate_registers(&pool),
comment: op.comment.clone(),
owning_span: op.owning_span.clone(),
})
}

InstructionSet { ops: buf }
AllocatedAbstractInstructionSet { ops: buf }
}
}

/// helper function to check if a label is used in a given buffer of ops
fn label_is_used(buf: &[Op], label: &Label) -> bool {
buf.iter().any(|Op { ref opcode, .. }| match opcode {
Either::Right(OrganizationalOp::Jump(ref l)) if label == l => true,
Either::Right(OrganizationalOp::JumpIfNotEq(_, _, ref l)) if label == l => true,
Either::Right(OrganizationalOp::JumpIfNotZero(_, ref l)) if label == l => true,
_ => false,
})
impl fmt::Display for AbstractInstructionSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
".program:\n{}",
self.ops
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<_>>()
.join("\n")
)
}
}

/// "Realized" here refers to labels -- there are no more organizational
/// ops or labels. In this struct, they are all "realized" to offsets.
pub struct RealizedAbstractInstructionSet {
pub(super) ops: Vec<RealizedOp>,
}

impl RealizedAbstractInstructionSet {
pub(crate) fn pad_to_even(self) -> Vec<AllocatedOp> {
let mut ops = self
.ops
.into_iter()
.map(
|RealizedOp {
opcode,
comment,
owning_span,
offset: _,
}| {
AllocatedOp {
opcode,
comment,
owning_span,
}
},
)
.collect::<Vec<_>>();

if ops.len() & 1 != 0 {
ops.push(AllocatedOp {
opcode: crate::asm_lang::allocated_ops::AllocatedOpcode::NOOP,
comment: "word-alignment of data section".into(),
owning_span: None,
});
}

ops
}
}
Loading

0 comments on commit 3a063c6

Please sign in to comment.