Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

Commit

Permalink
feat: improve implementation of jump tables in bytecode
Browse files Browse the repository at this point in the history
Previously, we would issue a series of Breq instructions
each of which would decode the argument term, compare against
an immediate for a match, then conditionally branch to the target.
Now, the argument is decoded only once, and the target branch is
resolved in a single instruction. The overall code size is reduced
as well.

Additionally, I've gone through and documented all of the
bytecode ops with their semantics, and assumptions about the
runtime implementation in each.
  • Loading branch information
bitwalker committed Feb 28, 2023
1 parent 9daf23f commit 77ec7ac
Show file tree
Hide file tree
Showing 8 changed files with 1,105 additions and 518 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Firefly - A new compiler and runtime for BEAM languages

| Machine | Vendor | Operating System | Host |Subgroup | Status |
|---------|---------|------------------|-------|--------------|--------|
| wasm32 | unknown | unknown | macOS | N/A | [![wasm32-unknown-unknown (macOS)](https://github.com/GetFirefly/firefly/workflows/wasm32-unknown-unknown%20%28macOS%29/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22wasm32-unknown-unknown%22+branch%3Adevelop) |
| wasm32 | unknown | unknown | Linux | N/A | [![wasm32-unknown-unknown (Linux)](https://github.com/GetFirefly/firefly/workflows/wasm32-unknown-unknown%20(Linux)/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22wasm32-unknown-unknown+%28Linux%29%22+branch%3Adevelop) |
| x86_64 | apple | darwin | macOS | compiler | [![x86_64-apple-darwin compiler](https://github.com/GetFirefly/firefly/workflows/x86_64-apple-darwin%20compiler/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22x86_64-apple-darwin+compiler%22+branch%3Adevelop)
| x86_64 | unknown | linux-gnu | Linux | compiler | [![x86_64-unknown-linux-gnu compiler](https://github.com/GetFirefly/firefly/workflows/x86_64-unknown-linux-gnu%20compiler/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22x86_64-unknown-linux-gnu+compiler%22+branch%3Adevelop)
[![x86_64-apple-darwin](https://github.com/GetFirefly/firefly/workflows/x86_64-apple-darwin%20compiler/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22x86_64-apple-darwin+compiler%22+branch%3Adevelop)
[![x86_64-unknown-linux-gnu](https://github.com/GetFirefly/firefly/workflows/x86_64-unknown-linux-gnu%20compiler/badge.svg?branch=develop)](https://github.com/GetFirefly/firefly/actions?query=workflow%3A%22x86_64-unknown-linux-gnu+compiler%22+branch%3Adevelop)

* [Getting Started](#getting-started)
* [Installation](#install)
Expand Down
10 changes: 5 additions & 5 deletions compiler/driver/src/compiler/passes/bytecode/lower_ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ impl<'a> BytecodeBuilder<'a> {
builder.mark_as_nif();
}

// Build lookup map for syntax_ssa blocks to bytecode blocks, creating the blocks in the process
// Build lookup map for syntax_ssa blocks to bytecode blocks, creating the blocks in the
// process
self.blocks.extend(function.dfg.blocks().map(|(b, _data)| {
let arity = function.dfg.block_param_types(b).len() as u8;
let bc_block = builder.create_block(arity);
Expand Down Expand Up @@ -488,13 +489,12 @@ impl<'a> BytecodeBuilder<'a> {
let loc = self.location_id_from_span(builder, span);
let arg = self.values[&op.arg];

let mut jt = builder.build_jump_table(arg, loc);
for (value, dest) in op.arms.iter() {
let dest = self.blocks[dest];
builder.build_br_eq(arg, *value, dest, loc);
jt.entry(*value, dest);
}

let default = self.blocks[&op.default];
builder.build_br(default, &[], loc);
jt.finish(self.blocks[&op.default], &[]);

Ok(())
}
Expand Down
22 changes: 22 additions & 0 deletions library/bytecode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Firefly Bytecode

This crate defines the instruction set for our bytecode emulator. It provides the following:

* A datastructure which is used to represent individual bytecode-compiled modules, as well as the
result of linking together several bytecode-compiled modules as a whole program, called `ByteCode`
* An instruction set optimized for the way our compiler generates code, of ~120 instructions (currently).
Many of these instructions are similar to those of a real ISA, but a number are fused instructions, performing
multiple operations at once, and a few are superinstructions which have quite complex implementations.
* A bytecode "builder" which can be used to iteratively construct a bytecode module without having to manage
any of the gritty details by hand.
* A textual format to allow for easy review of the generated bytecode in a form similar to our other IR formats
* A binary format to which bytecode-compiled programs are encoded/decoded when shipped as an executable
* Support for source-level debug info down to the instruction level

## Further Reading

You can find the compiler pass which generates this bytecode [here](`../compiler/driver/src/compiler/passes/bytecode/lower_ssa.rs`).

The current implementation of each opcode in our bytecode emulator can be found [here](`../runtimes/emulator/src/emulator/scheduler.rs`).

A detailed explanation of each instruction/opcode and its semantics can be found [here](src/ops.rs).
140 changes: 95 additions & 45 deletions library/bytecode/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,47 @@ where
pub fn finish(self) {}
}

pub struct JumpTableBuilder<'a, 'b, A, T>
where
A: Atom,
T: AtomTable<Atom = A>,
{
builder: &'b mut FunctionBuilder<'a, A, T>,
arg: Register,
loc: Option<LocationId>,
entries: Vec<(u32, BlockId)>,
}
impl<'a, 'b, A, T> JumpTableBuilder<'a, 'b, A, T>
where
A: Atom,
T: AtomTable<Atom = A>,
{
pub fn entry(&mut self, imm: u32, dest: BlockId) {
self.entries.push((imm, dest));
}

pub fn finish(mut self, default: BlockId, args: &[Register]) {
self.builder.push(
Opcode::JumpTable(JumpTable {
reg: self.arg,
len: self.entries.len() as u8,
}),
self.loc,
);
for (imm, dest) in self.entries.drain(..) {
assert_ne!(dest, self.builder.current_block);
self.builder.push(
Opcode::JumpTableEntry(JumpTableEntry {
imm,
offset: dest as JumpOffset,
}),
self.loc,
)
}
self.builder.build_br(default, args, self.loc);
}
}

// Builders
impl<'a, A, T> FunctionBuilder<'a, A, T>
where
Expand Down Expand Up @@ -432,29 +473,16 @@ where
);
}

pub fn build_br_eq(&mut self, reg: Register, imm: u32, dest: BlockId, loc: Option<LocationId>) {
assert_ne!(dest, self.current_block);

self.push(
Opcode::Breq(Breq {
reg,
imm,
offset: dest as JumpOffset,
}),
loc,
);
}

pub fn build_switch(
&mut self,
reg: Register,
arms: &[(u32, BlockId)],
pub fn build_jump_table<'b, 'c: 'b>(
&'c mut self,
arg: Register,
loc: Option<LocationId>,
) {
assert_ne!(arms.len(), 0);

for (imm, dest) in arms.iter() {
self.build_br_eq(reg, *imm, *dest, loc);
) -> JumpTableBuilder<'a, 'b, A, T> {
JumpTableBuilder {
builder: self,
arg,
loc,
entries: vec![],
}
}

Expand Down Expand Up @@ -508,7 +536,8 @@ where
let dst = dsts[index];
index += 1;

// The destination register is clobbered if a subsequent move relies on it as the source register.
// The destination register is clobbered if a subsequent move relies on it as the source
// register.
let is_clobbered = rest.contains(&dst);

if is_clobbered {
Expand Down Expand Up @@ -566,23 +595,26 @@ where
args: &[Register],
loc: Option<LocationId>,
) -> Register {
let dest = self.alloc_register();
// We need to reserve a register for the return address
self.alloc_register();
// Then reserve registers for the callee arguments and move their values into place
for arg in args {
let dest = self.alloc_register();
self.push(Opcode::Mov(Mov { dest, src: *arg }), loc);
match self.builder.code.function_by_id(callee) {
Function::Native { .. } => self.build_call_nif(callee, args, loc),
Function::Bytecode { mfa, .. } | Function::Bif { mfa, .. } => {
assert_eq!(
mfa.arity as usize,
args.len(),
"incorrect number of arguments for callee"
);
let dest = self.alloc_register();
// We need to reserve a register for the return address
self.alloc_register();
// Then reserve registers for the callee arguments and move their values into place
for arg in args {
let dest = self.alloc_register();
self.push(Opcode::Mov(Mov { dest, src: *arg }), loc);
}
self.push(Opcode::CallStatic(CallStatic { dest, callee }), loc);
dest
}
}
self.push(
Opcode::CallStatic(CallStatic {
dest,
callee,
arity: args.len() as Arity,
}),
loc,
);
dest
}

pub fn build_call_apply2(
Expand Down Expand Up @@ -650,10 +682,25 @@ where
}

pub fn build_enter(&mut self, callee: FunId, args: &[Register], loc: Option<LocationId>) {
match self.builder.code.function_by_id(callee) {
Function::Native { .. } => self.build_enter_nif(callee, args, loc),
Function::Bytecode { mfa, .. } | Function::Bif { mfa, .. } => {
assert_eq!(
mfa.arity as usize,
args.len(),
"incorrect number of arguments for callee"
);
self.prepare_tail_call_args(args, loc);
self.push(Opcode::EnterStatic(EnterStatic { callee }), loc);
}
}
}

pub fn build_enter_nif(&mut self, callee: FunId, args: &[Register], loc: Option<LocationId>) {
self.prepare_tail_call_args(args, loc);
self.push(
Opcode::EnterStatic(EnterStatic {
callee,
Opcode::EnterNative(EnterNative {
callee: callee as usize as *const (),
arity: args.len() as Arity,
}),
loc,
Expand Down Expand Up @@ -730,7 +777,8 @@ where
let dst = dsts[index];
index += 1;

// The destination register is clobbered if a subsequent move relies on it as the source register.
// The destination register is clobbered if a subsequent move relies on it as the source
// register.
let is_clobbered = rest.contains(&dst);

if is_clobbered {
Expand Down Expand Up @@ -1881,7 +1929,8 @@ where
block.offset = block_offset;
// Append code
self.builder.code.code.append(&mut block.code);
// Insert corresponding debug info by calculating ranges of instructions covered by the same location
// Insert corresponding debug info by calculating ranges of instructions covered by the
// same location
let mut range_loc = None;
let mut range_start = block_offset;
for (i, loc) in block.locations.iter().copied().enumerate() {
Expand Down Expand Up @@ -1921,7 +1970,7 @@ where
Opcode::Br(Br { ref mut offset })
| Opcode::Brz(Brz { ref mut offset, .. })
| Opcode::Brnz(Brnz { ref mut offset, .. })
| Opcode::Breq(Breq { ref mut offset, .. })
| Opcode::JumpTableEntry(JumpTableEntry { ref mut offset, .. })
| Opcode::LandingPad(LandingPad { ref mut offset, .. }) => {
// Locate the offset of the block this instruction occurs in,
// and determine the relative offset to the first instruction in
Expand All @@ -1936,7 +1985,8 @@ where
.unwrap();
*offset = -relative;
} else {
// The current block occurs before the target block, so this is a forwards jump
// The current block occurs before the target block, so this is a forwards
// jump
let relative = target_block_offset as isize - ip as isize;
*offset = relative.try_into().unwrap()
}
Expand Down
Loading

0 comments on commit 77ec7ac

Please sign in to comment.