Open Robbepop opened 4 years ago
The Runwell IR builder initial implementation seems to be working as of recently. There are several test cases for which it already produces correct SSA IR code. The SSA conversion procedure implements the algorithm introduced in this paper.
The next step is to use the new Runwell IR builder API to construct SSA IR from given Wasm bytecode input.
Note that current tests do not automatically test the constructed functions for expected output since we are currently missing an API for that which is planned.
Some examples include the following:
Given the following test code:
#[test]
fn simple_if() -> Result<(), IrError> {
let mut b = Function::build()
.with_inputs(&[IntType::I32.into()])?
.with_outputs(&[])?
.declare_variables(1, IntType::I32.into())?
.body();
let then_block = b.create_block();
let else_block = b.create_block();
let exit_block = b.create_block();
let input = Variable::from_raw(RawIdx::from_u32(0));
let var = Variable::from_raw(RawIdx::from_u32(1));
let v0 = b.read_var(input)?;
let v1 = b.ins()?.constant(IntConst::I32(0))?;
let v2 = b.ins()?.icmp(IntType::I32, CompareIntOp::Eq, v0, v1)?;
b.ins()?.if_then_else(v2, then_block, else_block)?;
b.switch_to_block(then_block)?;
let v3 = b.ins()?.constant(IntConst::I32(10))?;
b.write_var(var, v3)?;
b.ins()?.br(exit_block)?;
b.seal_block()?;
b.switch_to_block(else_block)?;
let v4 = b.ins()?.constant(IntConst::I32(20))?;
b.write_var(var, v4)?;
b.ins()?.br(exit_block)?;
b.seal_block()?;
b.switch_to_block(exit_block)?;
let v5 = b.read_var(var)?;
b.ins()?.return_value(v5)?;
b.seal_block()?;
let fun = b.finalize()?;
println!("{}", fun);
Ok(())
}
The test prints out the following constructed IR function:
fn (v0: i32)
bb0:
v1: i32 = const 0
v2: i32 = icmp i32 eq v0 v1
if v2 then bb1 else bb2
bb1:
v3: i32 = const 10
br bb3
bb2:
v4: i32 = const 20
br bb3
bb3:
v5: i32 = ϕ [ bb1 -> v3, bb2 -> v4 ]
ret v5
Given the following test code:
#[test]
fn simple_loop() -> Result<(), IrError> {
let mut b = Function::build()
.with_inputs(&[IntType::I32.into()])?
.with_outputs(&[])?
.declare_variables(1, IntType::I32.into())?
.body();
let loop_head = b.create_block();
let loop_body = b.create_block();
let loop_exit = b.create_block();
let input = Variable::from_raw(RawIdx::from_u32(0));
let counter = Variable::from_raw(RawIdx::from_u32(1));
let v0 = b.ins()?.constant(IntConst::I32(0))?;
b.write_var(counter, v0)?;
b.ins()?.br(loop_head)?;
b.switch_to_block(loop_head)?;
let v1 = b.read_var(counter)?;
let v2 = b.read_var(input)?;
let v3 = b.ins()?.icmp(IntType::I32, CompareIntOp::Slt, v1, v2)?;
b.ins()?.if_then_else(v3, loop_body, loop_exit)?;
b.switch_to_block(loop_body)?;
let v4 = b.read_var(counter)?;
let v5 = b.ins()?.constant(IntConst::I32(1))?;
let v6 = b.ins()?.iadd(IntType::I32, v4, v5)?;
b.write_var(counter, v6)?;
b.ins()?.br(loop_head)?;
b.seal_block()?;
b.switch_to_block(loop_head)?;
b.seal_block()?;
b.switch_to_block(loop_exit)?;
let v7 = b.read_var(counter)?;
b.ins()?.return_value(v7)?;
b.seal_block()?;
let fun = b.finalize()?;
println!("{}", fun);
Ok(())
}
The test prints out the following constructed IR function:
fn (v0: i32)
bb0:
v1: i32 = const 0
br bb1
bb1:
v2: i32 = ϕ [ bb0 -> v1, bb2 -> v7 ]
v4: i32 = scmp i32 slt v2 v0
if v4 then bb2 else bb3
bb2:
v6: i32 = const 1
v7: i32 = iadd i32 v2 v6
br bb1
bb3:
ret v2
The
runwell
IR is the intermediate representation of therunwell
JIT compiler. It is an SSA based bytecode that is inspired by both Wasm and LLVM IR.Example Transformation
Below we show an example transformation of the Wasm input function and the resulting
runwell
IR of the same function.The Wasm function identified by
89
is the function we want to translate. It takes a single parameter of typei32
, returns nothing and has 2 local variables of typei32
. Internally it calls function120
and function78
.With internally called functions
120
and76
: (Note that we are only interested in the conversion of function89
.)The final translated
runwell
IR function: It consists of 3 basic blocks. Note that this SSA representation is not minimal not pruned and not optimized.Example 2
Another simpler example that doesn't include branches shows that our IR is very similar to Wasm.
Before:
After:
Note that the function returned by
call.indirect table: 0, offset: 4
has signature[i32, i32] => [i32]
.Example 3
Given Wasm:
After
runwell
IR translation:Example 4 (with Stack Table)
Input Wasm function:
Output
runwell
IR:Stack during computing of the
runwell
IR:Design
The
runwell
IR is designed to represent Wasm bytecode. For local variables we use thelocal
keyword where LLVM would have usedalloca
instead. All operations operate on the direct value, e.g.Instead of
While this creates a bigger IR it also heavily simplifies the representation and procedures on it. Note that it generally shouldn't impose a problem for the codegen backend to procude the same optimized code for both versions.
There are a few operations that operate on immediates, namely
T.const
,local.get
,local.set
,global.get
,global.set
,load
,store
andcall
. ForT.const
the immediate value is the constant itself while forlocal.get
,local.set
,global.get
global.set
,load
,store
andcall
the immediate value is the respective identifier of the operated on object, e.g. in the case oflocal.get
the immediate value is the ID of the local variable.Syntax
Syntactically the
runwell
IR shall be as simple as possible for readers while adhering to the Wasm and LLVM origins.