Merge remote-tracking branch 'origin/trunk' into decimal-literals

This commit is contained in:
Folkert 2021-11-21 19:31:38 +01:00
commit 5529841d68
404 changed files with 9919 additions and 6764 deletions

View file

@ -2,12 +2,13 @@ use bumpalo::{self, collections::Vec};
use code_builder::Align;
use roc_collections::all::MutMap;
use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use roc_mono::ir::{CallType, Expr, JoinPointId, Literal, Proc, Stmt};
use roc_mono::layout::{Layout, LayoutIds};
use roc_mono::layout::{Builtin, Layout, LayoutIds};
use crate::layout::WasmLayout;
use crate::low_level::{build_call_low_level, LowlevelBuildResult};
use crate::low_level::{decode_low_level, LowlevelBuildResult};
use crate::storage::{Storage, StoredValue, StoredValueKind};
use crate::wasm_module::linking::{
DataSymbol, LinkingSection, RelocationSection, WasmObjectSymbol, WASM_SYM_BINDING_WEAK,
@ -22,8 +23,8 @@ use crate::wasm_module::{
LocalId, Signature, SymInfo, ValueType,
};
use crate::{
copy_memory, CopyMemoryConfig, Env, BUILTINS_IMPORT_MODULE_NAME, MEMORY_NAME, PTR_TYPE,
STACK_POINTER_GLOBAL_ID, STACK_POINTER_NAME,
copy_memory, CopyMemoryConfig, Env, BUILTINS_IMPORT_MODULE_NAME, MEMORY_NAME, PTR_SIZE,
PTR_TYPE, STACK_POINTER_GLOBAL_ID, STACK_POINTER_NAME,
};
/// The memory address where the constants data will be loaded during module instantiation.
@ -294,16 +295,17 @@ impl<'a> WasmBackend<'a> {
_ => {
self.storage.load_symbols(&mut self.code_builder, &[*sym]);
self.code_builder.br(self.block_depth); // jump to end of function (for stack frame pop)
}
}
// jump to the "stack frame pop" code at the end of the function
self.code_builder.br(self.block_depth - 1);
Ok(())
}
Stmt::Switch {
cond_symbol,
cond_layout: _,
cond_layout,
branches,
default_branch,
ret_layout: _,
@ -321,21 +323,45 @@ impl<'a> WasmBackend<'a> {
cond_storage,
);
// create (number_of_branches - 1) new blocks.
// create a block for each branch except the default
for _ in 0..branches.len() {
self.start_block(BlockType::NoResult)
}
let is_bool = matches!(cond_layout, Layout::Builtin(Builtin::Int1));
let cond_type = WasmLayout::new(cond_layout).value_type();
// then, we jump whenever the value under scrutiny is equal to the value of a branch
for (i, (value, _, _)) in branches.iter().enumerate() {
// put the cond_symbol on the top of the stack
self.storage
.load_symbols(&mut self.code_builder, &[*cond_symbol]);
self.code_builder.i32_const(*value as i32);
// compare the 2 topmost values
self.code_builder.i32_eq();
if is_bool {
// We already have a bool, don't need to compare against a const to get one
if *value == 0 {
self.code_builder.i32_eqz();
}
} else {
match cond_type {
ValueType::I32 => {
self.code_builder.i32_const(*value as i32);
self.code_builder.i32_eq();
}
ValueType::I64 => {
self.code_builder.i64_const(*value as i64);
self.code_builder.i64_eq();
}
ValueType::F32 => {
self.code_builder.f32_const(f32::from_bits(*value as u32));
self.code_builder.f32_eq();
}
ValueType::F64 => {
self.code_builder.f64_const(f64::from_bits(*value as u64));
self.code_builder.f64_eq();
}
}
}
// "break" out of `i` surrounding blocks
self.code_builder.br_if(i as u32);
@ -418,6 +444,13 @@ impl<'a> WasmBackend<'a> {
Ok(())
}
Stmt::Refcounting(_modify, following) => {
// TODO: actually deal with refcounting. For hello world, we just skipped it.
self.build_stmt(following, ret_layout)?;
Ok(())
}
x => Err(format!("statement not yet implemented: {:?}", x)),
}
}
@ -444,6 +477,11 @@ impl<'a> WasmBackend<'a> {
arguments,
}) => match call_type {
CallType::ByName { name: func_sym, .. } => {
// If this function is just a lowlevel wrapper, then inline it
if let Some(lowlevel) = LowLevel::from_inlined_wrapper(*func_sym) {
return self.build_low_level(lowlevel, arguments, *sym, wasm_layout);
}
let mut wasm_args_tmp: Vec<Symbol>;
let (wasm_args, has_return_val) = match wasm_layout {
WasmLayout::StackMemory { .. } => {
@ -486,39 +524,80 @@ impl<'a> WasmBackend<'a> {
}
CallType::LowLevel { op: lowlevel, .. } => {
let return_layout = WasmLayout::new(layout);
self.storage.load_symbols(&mut self.code_builder, arguments);
let build_result = build_call_low_level(
&mut self.code_builder,
&mut self.storage,
lowlevel,
arguments,
&return_layout,
);
use LowlevelBuildResult::*;
match build_result {
Done => Ok(()),
BuiltinCall(name) => {
self.call_imported_builtin(name, arguments, &return_layout);
Ok(())
}
NotImplemented => Err(format!(
"Low level operation {:?} is not yet implemented",
lowlevel
)),
}
self.build_low_level(*lowlevel, arguments, *sym, wasm_layout)
}
x => Err(format!("the call type, {:?}, is not yet implemented", x)),
},
Expr::Struct(fields) => self.create_struct(sym, layout, fields),
Expr::StructAtIndex {
index,
field_layouts,
structure,
} => {
if let StoredValue::StackMemory { location, .. } = self.storage.get(structure) {
let (local_id, mut offset) =
location.local_and_offset(self.storage.stack_frame_pointer);
for field in field_layouts.iter().take(*index as usize) {
offset += field.stack_size(PTR_SIZE);
}
self.storage.copy_value_from_memory(
&mut self.code_builder,
*sym,
local_id,
offset,
);
} else {
unreachable!("Unexpected storage for {:?}", structure)
}
Ok(())
}
x => Err(format!("Expression is not yet implemented {:?}", x)),
}
}
fn build_low_level(
&mut self,
lowlevel: LowLevel,
arguments: &'a [Symbol],
return_sym: Symbol,
return_layout: WasmLayout,
) -> Result<(), String> {
// Load symbols using the "fast calling convention" that Zig uses instead of the C ABI we normally use.
// It's only different from the C ABI for small structs, and we are using Zig for all of those cases.
// This is a workaround for a bug in Zig. If later versions fix it, we can change to the C ABI.
self.storage.load_symbols_fastcc(
&mut self.code_builder,
arguments,
return_sym,
&return_layout,
);
let build_result = decode_low_level(
&mut self.code_builder,
&mut self.storage,
lowlevel,
arguments,
&return_layout,
);
use LowlevelBuildResult::*;
match build_result {
Done => Ok(()),
BuiltinCall(name) => {
self.call_zig_builtin(name, arguments, &return_layout);
Ok(())
}
NotImplemented => Err(format!(
"Low level operation {:?} is not yet implemented",
lowlevel
)),
}
}
fn load_literal(
&mut self,
lit: &Literal<'a>,
@ -578,8 +657,8 @@ impl<'a> WasmBackend<'a> {
self.lookup_string_constant(string, sym, layout);
self.code_builder.get_local(local_id);
self.code_builder.insert_memory_relocation(linker_sym_index);
self.code_builder.i32_const(elements_addr as i32);
self.code_builder
.i32_const_mem_addr(elements_addr, linker_sym_index);
self.code_builder.i32_store(Align::Bytes4, offset);
self.code_builder.get_local(local_id);
@ -701,12 +780,10 @@ impl<'a> WasmBackend<'a> {
Ok(())
}
fn call_imported_builtin(
&mut self,
name: &'a str,
arguments: &[Symbol],
ret_layout: &WasmLayout,
) {
/// Generate a call instruction to a Zig builtin function.
/// And if we haven't seen it before, add an Import and linker data for it.
/// Zig calls use LLVM's "fast" calling convention rather than our usual C ABI.
fn call_zig_builtin(&mut self, name: &'a str, arguments: &[Symbol], ret_layout: &WasmLayout) {
let (fn_index, linker_symbol_index) = match self.builtin_sym_index_map.get(name) {
Some(sym_idx) => match &self.linker_symbols[*sym_idx] {
SymInfo::Function(WasmObjectSymbol::Imported { index, .. }) => {
@ -716,11 +793,29 @@ impl<'a> WasmBackend<'a> {
},
None => {
let mut param_types = Vec::with_capacity_in(arguments.len(), self.env.arena);
param_types.extend(arguments.iter().map(|a| self.storage.get(a).value_type()));
let mut param_types = Vec::with_capacity_in(1 + arguments.len(), self.env.arena);
let ret_type = if ret_layout.is_stack_memory() {
param_types.push(ValueType::I32);
None
} else {
Some(ret_layout.value_type())
};
// Zig's "fast calling convention" packs structs into CPU registers (stack machine slots) if possible.
// If they're small enough they can go into an I32 or I64. If they're big, they're pointers (I32).
for arg in arguments {
param_types.push(match self.storage.get(arg) {
StoredValue::StackMemory { size, .. } if *size > 4 && *size <= 8 => {
ValueType::I64
}
stored => stored.value_type(),
});
}
let signature_index = self.module.types.insert(Signature {
param_types,
ret_type: Some(ret_layout.value_type()), // TODO: handle builtins with no return value
ret_type,
});
let import_index = self.module.import.entries.len() as u32;
@ -731,14 +826,15 @@ impl<'a> WasmBackend<'a> {
};
self.module.import.entries.push(import);
let sym_idx = self.linker_symbols.len() as u32;
let sym_idx = self.linker_symbols.len();
let sym_info = SymInfo::Function(WasmObjectSymbol::Imported {
flags: WASM_SYM_UNDEFINED,
index: import_index,
});
self.linker_symbols.push(sym_info);
self.builtin_sym_index_map.insert(name, sym_idx);
(import_index, sym_idx)
(import_index, sym_idx as u32)
}
};
self.code_builder.call(

View file

@ -7,6 +7,7 @@ pub mod wasm_module;
use bumpalo::{self, collections::Vec, Bump};
use roc_collections::all::{MutMap, MutSet};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{Interns, Symbol};
use roc_mono::ir::{Proc, ProcLayout};
use roc_mono::layout::LayoutIds;
@ -36,7 +37,7 @@ pub fn build_module<'a>(
env: &'a Env,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> Result<std::vec::Vec<u8>, String> {
let mut wasm_module = build_module_help(env, procedures)?;
let (mut wasm_module, _) = build_module_help(env, procedures)?;
let mut buffer = std::vec::Vec::with_capacity(4096);
wasm_module.serialize_mut(&mut buffer);
Ok(buffer)
@ -45,36 +46,54 @@ pub fn build_module<'a>(
pub fn build_module_help<'a>(
env: &'a Env,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> Result<WasmModule<'a>, String> {
) -> Result<(WasmModule<'a>, u32), String> {
let mut layout_ids = LayoutIds::default();
let mut proc_symbols = Vec::with_capacity_in(procedures.len(), env.arena);
let mut generated_procs = Vec::with_capacity_in(procedures.len(), env.arena);
let mut generated_symbols = Vec::with_capacity_in(procedures.len(), env.arena);
let mut linker_symbols = Vec::with_capacity_in(procedures.len() * 2, env.arena);
let mut exports = Vec::with_capacity_in(procedures.len(), env.arena);
let mut exports = Vec::with_capacity_in(4, env.arena);
let mut main_fn_index = None;
// Collect the symbols & names for the procedures
for (i, (sym, layout)) in procedures.keys().enumerate() {
proc_symbols.push(*sym);
// Collect the symbols & names for the procedures,
// and filter out procs we're going to inline
let mut fn_index: u32 = 0;
for ((sym, layout), proc) in procedures.into_iter() {
if LowLevel::from_inlined_wrapper(sym).is_some() {
continue;
}
generated_procs.push(proc);
generated_symbols.push(sym);
let fn_name = layout_ids
.get_toplevel(*sym, layout)
.to_symbol_string(*sym, &env.interns);
.get_toplevel(sym, &layout)
.to_symbol_string(sym, &env.interns);
if env.exposed_to_host.contains(sym) {
if env.exposed_to_host.contains(&sym) {
main_fn_index = Some(fn_index);
exports.push(Export {
name: fn_name.clone(),
ty: ExportType::Func,
index: i as u32,
index: fn_index,
});
}
let linker_sym = SymInfo::for_function(i as u32, fn_name);
let linker_sym = SymInfo::for_function(fn_index, fn_name);
linker_symbols.push(linker_sym);
fn_index += 1;
}
// Main loop: Build the Wasm module
// Build the Wasm module
let (mut module, linker_symbols) = {
let mut backend = WasmBackend::new(env, layout_ids, proc_symbols, linker_symbols, exports);
for ((sym, _), proc) in procedures.into_iter() {
let mut backend = WasmBackend::new(
env,
layout_ids,
generated_symbols.clone(),
linker_symbols,
exports,
);
for (proc, sym) in generated_procs.into_iter().zip(generated_symbols) {
backend.build_proc(proc, sym)?;
}
(backend.module, backend.linker_symbols)
@ -83,7 +102,7 @@ pub fn build_module_help<'a>(
let symbol_table = LinkingSubSection::SymbolTable(linker_symbols);
module.linking.subsections.push(symbol_table);
Ok(module)
Ok((module, main_fn_index.unwrap()))
}
pub struct CopyMemoryConfig {

View file

@ -15,10 +15,10 @@ pub enum LowlevelBuildResult {
NotImplemented,
}
pub fn build_call_low_level<'a>(
pub fn decode_low_level<'a>(
code_builder: &mut CodeBuilder<'a>,
storage: &mut Storage<'a>,
lowlevel: &LowLevel,
lowlevel: LowLevel,
args: &'a [Symbol],
ret_layout: &WasmLayout,
) -> LowlevelBuildResult {
@ -27,16 +27,39 @@ pub fn build_call_low_level<'a>(
let panic_ret_type = || panic!("Invalid return layout for {:?}: {:?}", lowlevel, ret_layout);
match lowlevel {
StrConcat | StrJoinWith | StrIsEmpty | StrStartsWith | StrStartsWithCodePt
| StrEndsWith | StrSplit | StrCountGraphemes | StrFromInt | StrFromUtf8 | StrTrimLeft
| StrFromUtf8Range | StrToUtf8 | StrRepeat | StrFromFloat | StrTrim | ListLen
| ListGetUnsafe | ListSet | ListSingle | ListRepeat | ListReverse | ListConcat
StrConcat => return BuiltinCall(bitcode::STR_CONCAT),
StrJoinWith => return NotImplemented, // needs Array
StrIsEmpty => {
code_builder.i64_const(i64::MIN);
code_builder.i64_eq();
}
StrStartsWith => return BuiltinCall(bitcode::STR_STARTS_WITH),
StrStartsWithCodePt => return BuiltinCall(bitcode::STR_STARTS_WITH_CODE_PT),
StrEndsWith => return BuiltinCall(bitcode::STR_ENDS_WITH),
StrSplit => return NotImplemented, // needs Array
StrCountGraphemes => return NotImplemented, // test needs Array
StrFromInt => return NotImplemented, // choose builtin based on storage size
StrFromUtf8 => return NotImplemented, // needs Array
StrTrimLeft => return BuiltinCall(bitcode::STR_TRIM_LEFT),
StrTrimRight => return BuiltinCall(bitcode::STR_TRIM_RIGHT),
StrFromUtf8Range => return NotImplemented, // needs Array
StrToUtf8 => return NotImplemented, // needs Array
StrRepeat => return BuiltinCall(bitcode::STR_REPEAT),
StrFromFloat => {
// linker errors for __ashlti3, __fixunsdfti, __multi3, __udivti3, __umodti3
// https://gcc.gnu.org/onlinedocs/gccint/Integer-library-routines.html
// https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html
return NotImplemented;
}
StrTrim => return BuiltinCall(bitcode::STR_TRIM),
ListLen | ListGetUnsafe | ListSet | ListSingle | ListRepeat | ListReverse | ListConcat
| ListContains | ListAppend | ListPrepend | ListJoin | ListRange | ListMap | ListMap2
| ListMap3 | ListMap4 | ListMapWithIndex | ListKeepIf | ListWalk | ListWalkUntil
| ListWalkBackwards | ListKeepOks | ListKeepErrs | ListSortWith | ListTakeFirst
| ListTakeLast | ListDrop | ListDropAt | ListSwap | ListAny | ListFindUnsafe | DictSize
| DictEmpty | DictInsert | DictRemove | DictContains | DictGetUnsafe | DictKeys
| DictValues | DictUnion | DictIntersection | DictDifference | DictWalk | SetFromList => {
| ListWalkBackwards | ListKeepOks | ListKeepErrs | ListSortWith | ListSublist
| ListDropAt | ListSwap | ListAny | ListAll | ListFindUnsafe | DictSize | DictEmpty
| DictInsert | DictRemove | DictContains | DictGetUnsafe | DictKeys | DictValues
| DictUnion | DictIntersection | DictDifference | DictWalk | SetFromList => {
return NotImplemented;
}
@ -129,6 +152,9 @@ pub fn build_call_low_level<'a>(
NumIsMultipleOf => return NotImplemented,
NumAbs => match ret_layout.value_type() {
I32 => {
let arg_storage = storage.get(&args[0]).to_owned();
storage.ensure_value_has_local(code_builder, args[0], arg_storage);
storage.load_symbols(code_builder, args);
code_builder.i32_const(0);
storage.load_symbols(code_builder, args);
code_builder.i32_sub();
@ -138,6 +164,9 @@ pub fn build_call_low_level<'a>(
code_builder.select();
}
I64 => {
let arg_storage = storage.get(&args[0]).to_owned();
storage.ensure_value_has_local(code_builder, args[0], arg_storage);
storage.load_symbols(code_builder, args);
code_builder.i64_const(0);
storage.load_symbols(code_builder, args);
code_builder.i64_sub();

View file

@ -5,7 +5,7 @@ use roc_collections::all::MutMap;
use roc_module::symbol::Symbol;
use crate::layout::WasmLayout;
use crate::wasm_module::{CodeBuilder, LocalId, ValueType, VirtualMachineSymbolState};
use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState};
use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_SIZE, PTR_TYPE};
pub enum StoredValueKind {
@ -33,7 +33,7 @@ impl StackMemoryLocation {
pub enum StoredValue {
/// A value stored implicitly in the VM stack (primitives only)
VirtualMachineStack {
vm_state: VirtualMachineSymbolState,
vm_state: VmSymbolState,
value_type: ValueType,
size: u32,
},
@ -126,7 +126,7 @@ impl<'a> Storage<'a> {
}
}
_ => StoredValue::VirtualMachineStack {
vm_state: VirtualMachineSymbolState::NotYetPushed,
vm_state: VmSymbolState::NotYetPushed,
value_type: *value_type,
size: *size,
},
@ -194,6 +194,63 @@ impl<'a> Storage<'a> {
})
}
/// Load a single symbol using the C Calling Convention
/// *Private* because external code should always load symbols in bulk (see load_symbols)
fn load_symbol_ccc(&mut self, code_builder: &mut CodeBuilder, sym: Symbol) {
let storage = self.get(&sym).to_owned();
match storage {
StoredValue::VirtualMachineStack {
vm_state,
value_type,
size,
} => {
let next_local_id = self.get_next_local_id();
let maybe_next_vm_state = code_builder.load_symbol(sym, vm_state, next_local_id);
match maybe_next_vm_state {
// The act of loading the value changed the VM state, so update it
Some(next_vm_state) => {
self.symbol_storage_map.insert(
sym,
StoredValue::VirtualMachineStack {
vm_state: next_vm_state,
value_type,
size,
},
);
}
None => {
// Loading the value required creating a new local, because
// it was not in a convenient position in the VM stack.
self.local_types.push(value_type);
self.symbol_storage_map.insert(
sym,
StoredValue::Local {
local_id: next_local_id,
value_type,
size,
},
);
}
}
}
StoredValue::Local { local_id, .. } => {
code_builder.get_local(local_id);
code_builder.set_top_symbol(sym);
}
StoredValue::StackMemory { location, .. } => {
let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer);
code_builder.get_local(local_id);
if offset != 0 {
code_builder.i32_const(offset as i32);
code_builder.i32_add();
}
code_builder.set_top_symbol(sym);
}
}
}
/// Load symbols to the top of the VM stack
/// Avoid calling this method in a loop with one symbol at a time! It will work,
/// but it generates very inefficient Wasm code.
@ -204,61 +261,60 @@ impl<'a> Storage<'a> {
return;
}
for sym in symbols.iter() {
let storage = self.get(sym).to_owned();
match storage {
StoredValue::VirtualMachineStack {
vm_state,
value_type,
size,
} => {
let next_local_id = self.get_next_local_id();
let maybe_next_vm_state =
code_builder.load_symbol(*sym, vm_state, next_local_id);
match maybe_next_vm_state {
// The act of loading the value changed the VM state, so update it
Some(next_vm_state) => {
self.symbol_storage_map.insert(
*sym,
StoredValue::VirtualMachineStack {
vm_state: next_vm_state,
value_type,
size,
},
);
}
None => {
// Loading the value required creating a new local, because
// it was not in a convenient position in the VM stack.
self.local_types.push(value_type);
self.symbol_storage_map.insert(
*sym,
StoredValue::Local {
local_id: next_local_id,
value_type,
size,
},
);
}
}
}
StoredValue::Local { local_id, .. }
| StoredValue::StackMemory {
location: StackMemoryLocation::PointerArg(local_id),
..
} => {
code_builder.get_local(local_id);
code_builder.set_top_symbol(*sym);
self.load_symbol_ccc(code_builder, *sym);
}
}
/// Load symbols in a way compatible with LLVM's "fast calling convention"
/// A bug in Zig means it always uses this for Wasm even when we specify C calling convention.
/// It squashes small structs into primitive values where possible, avoiding stack memory
/// in favour of CPU registers (or VM stack values, which eventually become CPU registers).
/// We need to convert some of our structs from our internal C-like representation to work with Zig.
/// We are sticking to C ABI for better compatibility on the platform side.
pub fn load_symbols_fastcc(
&mut self,
code_builder: &mut CodeBuilder,
symbols: &[Symbol],
return_symbol: Symbol,
return_layout: &WasmLayout,
) {
// Note: we are not doing verify_stack_match in this case so we may generate more code.
// We would need more bookkeeping in CodeBuilder to track which representation is on the stack!
if return_layout.is_stack_memory() {
// Load the address where the return value should be written
// Apparently for return values we still use a pointer to stack memory
self.load_symbol_ccc(code_builder, return_symbol);
};
for sym in symbols {
if let StoredValue::StackMemory {
location,
size,
alignment_bytes,
} = self.get(sym)
{
if *size == 0 {
unimplemented!("Passing zero-sized values is not implemented yet");
} else if *size > 8 {
return self.load_symbol_ccc(code_builder, *sym);
}
StoredValue::StackMemory {
location: StackMemoryLocation::FrameOffset(offset),
..
} => {
code_builder.get_local(self.stack_frame_pointer.unwrap());
code_builder.i32_const(offset as i32);
code_builder.i32_add();
code_builder.set_top_symbol(*sym);
let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer);
code_builder.get_local(local_id);
let align = Align::from(*alignment_bytes);
if *size == 1 {
code_builder.i32_load8_u(align, offset);
} else if *size == 2 {
code_builder.i32_load16_u(align, offset);
} else if *size <= 4 {
code_builder.i32_load(align, offset);
} else {
code_builder.i64_load(align, offset);
}
} else {
self.load_symbol_ccc(code_builder, *sym);
}
}
}
@ -319,6 +375,67 @@ impl<'a> Storage<'a> {
}
}
/// Generate code to copy a StoredValue from an arbitrary memory location
/// (defined by a pointer and offset).
pub fn copy_value_from_memory(
&mut self,
code_builder: &mut CodeBuilder,
to_symbol: Symbol,
from_ptr: LocalId,
from_offset: u32,
) -> u32 {
let to_storage = self.get(&to_symbol).to_owned();
match to_storage {
StoredValue::StackMemory {
location,
size,
alignment_bytes,
} => {
let (to_ptr, to_offset) = location.local_and_offset(self.stack_frame_pointer);
copy_memory(
code_builder,
CopyMemoryConfig {
from_ptr,
from_offset,
to_ptr,
to_offset,
size,
alignment_bytes,
},
);
size
}
StoredValue::VirtualMachineStack {
value_type, size, ..
}
| StoredValue::Local {
value_type, size, ..
} => {
use crate::wasm_module::Align::*;
code_builder.get_local(from_ptr);
match (value_type, size) {
(ValueType::I64, 8) => code_builder.i64_load(Bytes8, from_offset),
(ValueType::I32, 4) => code_builder.i32_load(Bytes4, from_offset),
(ValueType::I32, 2) => code_builder.i32_load16_s(Bytes2, from_offset),
(ValueType::I32, 1) => code_builder.i32_load8_s(Bytes1, from_offset),
(ValueType::F32, 4) => code_builder.f32_load(Bytes4, from_offset),
(ValueType::F64, 8) => code_builder.f64_load(Bytes8, from_offset),
_ => {
panic!("Cannot store {:?} with alignment of {:?}", value_type, size);
}
};
if let StoredValue::Local { local_id, .. } = to_storage {
code_builder.set_local(local_id);
}
size
}
}
}
/// Generate code to copy from one StoredValue to another
/// Copies the _entire_ value. For struct fields etc., see `copy_value_to_memory`
pub fn clone_value(
@ -422,7 +539,7 @@ impl<'a> Storage<'a> {
} = storage
{
let local_id = self.get_next_local_id();
if vm_state != VirtualMachineSymbolState::NotYetPushed {
if vm_state != VmSymbolState::NotYetPushed {
code_builder.load_symbol(symbol, vm_state, local_id);
code_builder.set_local(local_id);
}

View file

@ -1,7 +1,6 @@
use bumpalo::collections::vec::Vec;
use bumpalo::Bump;
use core::panic;
use std::fmt::Debug;
use roc_module::symbol::Symbol;
@ -10,6 +9,13 @@ use super::opcodes::{OpCode, OpCode::*};
use super::serialize::{SerialBuffer, Serialize};
use crate::{round_up_to_alignment, FRAME_ALIGNMENT_BYTES, STACK_POINTER_GLOBAL_ID};
const ENABLE_DEBUG_LOG: bool = false;
macro_rules! log_instruction {
($($x: expr),+) => {
if ENABLE_DEBUG_LOG { println!($($x,)*); }
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LocalId(pub u32);
@ -29,6 +35,7 @@ impl Serialize for ValueType {
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum BlockType {
NoResult,
Value(ValueType),
@ -43,6 +50,31 @@ impl BlockType {
}
}
/// A control block in our model of the VM
/// Child blocks cannot "see" values from their parent block
struct VmBlock<'a> {
/// opcode indicating what kind of block this is
opcode: OpCode,
/// the stack of values for this block
value_stack: Vec<'a, Symbol>,
/// whether this block pushes a result value to its parent
has_result: bool,
}
impl std::fmt::Debug for VmBlock<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{:?} {}",
self.opcode,
if self.has_result {
"Result"
} else {
"NoResult"
}
))
}
}
/// Wasm memory alignment. (Rust representation matches Wasm encoding)
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
@ -73,7 +105,7 @@ impl From<u32> for Align {
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum VirtualMachineSymbolState {
pub enum VmSymbolState {
/// Value doesn't exist yet
NotYetPushed,
@ -113,6 +145,8 @@ macro_rules! instruction_memargs {
#[derive(Debug)]
pub struct CodeBuilder<'a> {
arena: &'a Bump,
/// The main container for the instructions
code: Vec<'a, u8>,
@ -135,8 +169,8 @@ pub struct CodeBuilder<'a> {
inner_length: Vec<'a, u8>,
/// Our simulation model of the Wasm stack machine
/// Keeps track of where Symbol values are in the VM stack
vm_stack: Vec<'a, Symbol>,
/// Nested blocks of instructions. A child block can't "see" the stack of its parent block
vm_block_stack: Vec<'a, VmBlock<'a>>,
/// Linker info to help combine the Roc module with builtin & platform modules,
/// e.g. to modify call instructions when function indices change
@ -146,13 +180,22 @@ pub struct CodeBuilder<'a> {
#[allow(clippy::new_without_default)]
impl<'a> CodeBuilder<'a> {
pub fn new(arena: &'a Bump) -> Self {
let mut vm_block_stack = Vec::with_capacity_in(8, arena);
let function_block = VmBlock {
opcode: BLOCK,
has_result: true,
value_stack: Vec::with_capacity_in(8, arena),
};
vm_block_stack.push(function_block);
CodeBuilder {
arena,
code: Vec::with_capacity_in(1024, arena),
insertions: Vec::with_capacity_in(32, arena),
insert_bytes: Vec::with_capacity_in(64, arena),
preamble: Vec::with_capacity_in(32, arena),
inner_length: Vec::with_capacity_in(5, arena),
vm_stack: Vec::with_capacity_in(32, arena),
vm_block_stack,
relocations: Vec::with_capacity_in(32, arena),
}
}
@ -167,45 +210,49 @@ impl<'a> CodeBuilder<'a> {
***********************************************************/
fn current_stack(&self) -> &Vec<'a, Symbol> {
let block = self.vm_block_stack.last().unwrap();
&block.value_stack
}
fn current_stack_mut(&mut self) -> &mut Vec<'a, Symbol> {
let block = self.vm_block_stack.last_mut().unwrap();
&mut block.value_stack
}
/// Set the Symbol that is at the top of the VM stack right now
/// We will use this later when we need to load the Symbol
pub fn set_top_symbol(&mut self, sym: Symbol) -> VirtualMachineSymbolState {
let len = self.vm_stack.len();
pub fn set_top_symbol(&mut self, sym: Symbol) -> VmSymbolState {
let current_stack = &mut self.vm_block_stack.last_mut().unwrap().value_stack;
let pushed_at = self.code.len();
let top_symbol: &mut Symbol = current_stack.last_mut().unwrap();
*top_symbol = sym;
if len == 0 {
panic!(
"trying to set symbol with nothing on stack, code = {:?}",
self.code
);
}
self.vm_stack[len - 1] = sym;
VirtualMachineSymbolState::Pushed { pushed_at }
VmSymbolState::Pushed { pushed_at }
}
/// Verify if a sequence of symbols is at the top of the stack
pub fn verify_stack_match(&self, symbols: &[Symbol]) -> bool {
let current_stack = self.current_stack();
let n_symbols = symbols.len();
let stack_depth = self.vm_stack.len();
let stack_depth = current_stack.len();
if n_symbols > stack_depth {
return false;
}
let offset = stack_depth - n_symbols;
for (i, sym) in symbols.iter().enumerate() {
if self.vm_stack[offset + i] != *sym {
if current_stack[offset + i] != *sym {
return false;
}
}
true
}
fn add_insertion(&mut self, insert_at: usize, opcode: u8, immediate: u32) {
fn add_insertion(&mut self, insert_at: usize, opcode: OpCode, immediate: u32) {
let start = self.insert_bytes.len();
self.insert_bytes.push(opcode);
self.insert_bytes.push(opcode as u8);
self.insert_bytes.encode_u32(immediate);
self.insertions.push(Insertion {
@ -213,6 +260,13 @@ impl<'a> CodeBuilder<'a> {
start,
end: self.insert_bytes.len(),
});
log_instruction!(
"**insert {:?} {} at byte offset {}**",
opcode,
immediate,
insert_at
);
}
/// Load a Symbol that is stored in the VM stack
@ -225,41 +279,56 @@ impl<'a> CodeBuilder<'a> {
pub fn load_symbol(
&mut self,
symbol: Symbol,
vm_state: VirtualMachineSymbolState,
vm_state: VmSymbolState,
next_local_id: LocalId,
) -> Option<VirtualMachineSymbolState> {
use VirtualMachineSymbolState::*;
) -> Option<VmSymbolState> {
use VmSymbolState::*;
match vm_state {
NotYetPushed => panic!("Symbol {:?} has no value yet. Nothing to load.", symbol),
NotYetPushed => unreachable!("Symbol {:?} has no value yet. Nothing to load.", symbol),
Pushed { pushed_at } => {
let &top = self.vm_stack.last().unwrap();
if top == symbol {
// We're lucky, the symbol is already on top of the VM stack
// No code to generate! (This reduces code size by up to 25% in tests.)
// Just let the caller know what happened
Some(Popped { pushed_at })
} else {
// Symbol is not on top of the stack. Find it.
if let Some(found_index) = self.vm_stack.iter().rposition(|&s| s == symbol) {
// Insert a local.set where the value was created
self.add_insertion(pushed_at, SETLOCAL as u8, next_local_id.0);
match self.current_stack().last() {
Some(top_symbol) if *top_symbol == symbol => {
// We're lucky, the symbol is already on top of the current block's stack.
// No code to generate! (This reduces code size by up to 25% in tests.)
// Just let the caller know what happened
Some(Popped { pushed_at })
}
_ => {
// Symbol is not on top of the stack.
// We should have saved it to a local, so go back and do that now.
// Take the value out of the stack where local.set was inserted
self.vm_stack.remove(found_index);
// It should still be on the stack in the block where it was assigned. Remove it.
let mut found = false;
for block in self.vm_block_stack.iter_mut() {
if let Some(found_index) =
block.value_stack.iter().position(|&s| s == symbol)
{
block.value_stack.remove(found_index);
found = true;
}
}
// Insert a local.get at the current position
// Go back to the code position where it was pushed, and save it to a local
if found {
self.add_insertion(pushed_at, SETLOCAL, next_local_id.0);
} else {
if ENABLE_DEBUG_LOG {
println!(
"{:?} has been popped implicitly. Leaving it on the stack.",
symbol
);
}
self.add_insertion(pushed_at, TEELOCAL, next_local_id.0);
}
// Recover the value again at the current position
self.get_local(next_local_id);
self.vm_stack.push(symbol);
self.set_top_symbol(symbol);
// This Symbol is no longer stored in the VM stack, but in a local
None
} else {
panic!(
"{:?} has state {:?} but not found in VM stack",
symbol, vm_state
);
}
}
}
@ -267,11 +336,11 @@ impl<'a> CodeBuilder<'a> {
Popped { pushed_at } => {
// This Symbol is being used for a second time
// Insert a local.tee where it was pushed, so we don't interfere with the first usage
self.add_insertion(pushed_at, TEELOCAL as u8, next_local_id.0);
self.add_insertion(pushed_at, TEELOCAL, next_local_id.0);
// Insert a local.get at the current position
self.get_local(next_local_id);
self.vm_stack.push(symbol);
self.set_top_symbol(symbol);
// This symbol has been promoted to a Local
// Tell the caller it no longer has a VirtualMachineSymbolState
@ -282,7 +351,7 @@ impl<'a> CodeBuilder<'a> {
/**********************************************************
FINALIZE AND SERIALIZE
FUNCTION HEADER
***********************************************************/
@ -375,6 +444,12 @@ impl<'a> CodeBuilder<'a> {
self.insertions.sort_by_key(|ins| ins.at);
}
/**********************************************************
SERIALIZE
***********************************************************/
/// Serialize all byte vectors in the right order
/// Also update relocation offsets relative to the base offset (code section body start)
pub fn serialize_with_relocs<T: SerialBuffer>(
@ -433,38 +508,78 @@ impl<'a> CodeBuilder<'a> {
/// Base method for generating instructions
/// Emits the opcode and simulates VM stack push/pop
fn inst(&mut self, opcode: OpCode, pops: usize, push: bool) {
let new_len = self.vm_stack.len() - pops as usize;
self.vm_stack.truncate(new_len);
fn inst_base(&mut self, opcode: OpCode, pops: usize, push: bool) {
let current_stack = self.current_stack_mut();
let new_len = current_stack.len() - pops as usize;
current_stack.truncate(new_len);
if push {
self.vm_stack.push(Symbol::WASM_ANONYMOUS_STACK_VALUE);
current_stack.push(Symbol::WASM_TMP);
}
self.code.push(opcode as u8);
}
fn inst_imm8(&mut self, opcode: OpCode, pops: usize, push: bool, immediate: u8) {
self.inst(opcode, pops, push);
self.code.push(immediate);
/// Plain instruction without any immediates
fn inst(&mut self, opcode: OpCode, pops: usize, push: bool) {
self.inst_base(opcode, pops, push);
log_instruction!(
"{:10}\t\t{:?}",
format!("{:?}", opcode),
self.current_stack()
);
}
// public for use in test code
pub fn inst_imm32(&mut self, opcode: OpCode, pops: usize, push: bool, immediate: u32) {
self.inst(opcode, pops, push);
/// Block instruction
fn inst_block(&mut self, opcode: OpCode, pops: usize, block_type: BlockType) {
self.inst_base(opcode, pops, false);
self.code.push(block_type.as_byte());
// Start a new block with a fresh value stack
self.vm_block_stack.push(VmBlock {
opcode,
value_stack: Vec::with_capacity_in(8, self.arena),
has_result: block_type != BlockType::NoResult,
});
log_instruction!(
"{:10} {:?}\t{:?}",
format!("{:?}", opcode),
block_type,
&self.vm_block_stack
);
}
fn inst_imm32(&mut self, opcode: OpCode, pops: usize, push: bool, immediate: u32) {
self.inst_base(opcode, pops, push);
self.code.encode_u32(immediate);
log_instruction!(
"{:10}\t{}\t{:?}",
format!("{:?}", opcode),
immediate,
self.current_stack()
);
}
fn inst_mem(&mut self, opcode: OpCode, pops: usize, push: bool, align: Align, offset: u32) {
self.inst(opcode, pops, push);
self.inst_base(opcode, pops, push);
self.code.push(align as u8);
self.code.encode_u32(offset);
log_instruction!(
"{:10} {:?} {}\t{:?}",
format!("{:?}", opcode),
align,
offset,
self.current_stack()
);
}
/// Insert a linker relocation for a memory address
pub fn insert_memory_relocation(&mut self, symbol_index: u32) {
/// Insert a const reference to a memory address
pub fn i32_const_mem_addr(&mut self, addr: u32, symbol_index: u32) {
self.inst_base(I32CONST, 0, true);
let offset = self.code.len() as u32;
self.code.encode_padded_u32(addr);
self.relocations.push(RelocationEntry::Offset {
type_id: OffsetRelocType::MemoryAddrLeb,
offset: self.code.len() as u32,
offset,
symbol_index,
addend: 0,
});
@ -484,22 +599,38 @@ impl<'a> CodeBuilder<'a> {
instruction_no_args!(nop, NOP, 0, false);
pub fn block(&mut self, ty: BlockType) {
self.inst_imm8(BLOCK, 0, false, ty.as_byte());
self.inst_block(BLOCK, 0, ty);
}
pub fn loop_(&mut self, ty: BlockType) {
self.inst_imm8(LOOP, 0, false, ty.as_byte());
self.inst_block(LOOP, 0, ty);
}
pub fn if_(&mut self, ty: BlockType) {
self.inst_imm8(IF, 1, false, ty.as_byte());
self.inst_block(IF, 1, ty);
}
pub fn else_(&mut self) {
// Reuse the 'then' block but clear its value stack
self.current_stack_mut().clear();
self.inst(ELSE, 0, false);
}
instruction_no_args!(else_, ELSE, 0, false);
instruction_no_args!(end, END, 0, false);
pub fn end(&mut self) {
self.inst_base(END, 0, false);
let ended_block = self.vm_block_stack.pop().unwrap();
if ended_block.has_result {
let result = ended_block.value_stack.last().unwrap();
self.current_stack_mut().push(*result)
}
log_instruction!("END \t\t{:?}", &self.vm_block_stack);
}
pub fn br(&mut self, levels: u32) {
self.inst_imm32(BR, 0, false, levels);
}
pub fn br_if(&mut self, levels: u32) {
// In dynamic execution, br_if can pop 2 values if condition is true and the target block has a result.
// But our stack model is for *static* analysis and we need it to be correct at the next instruction,
// where the branch was not taken. So we only pop 1 value, the condition.
self.inst_imm32(BRIF, 1, false, levels);
}
#[allow(dead_code)]
@ -516,30 +647,26 @@ impl<'a> CodeBuilder<'a> {
n_args: usize,
has_return_val: bool,
) {
let stack_depth = self.vm_stack.len();
if n_args > stack_depth {
panic!(
"Trying to call to call function {:?} with {:?} values but only {:?} on the VM stack\n{:?}",
function_index, n_args, stack_depth, self
);
}
self.vm_stack.truncate(stack_depth - n_args);
if has_return_val {
self.vm_stack.push(Symbol::WASM_ANONYMOUS_STACK_VALUE);
}
self.code.push(CALL as u8);
self.inst_base(CALL, n_args, has_return_val);
// Write the index of the function to be called.
// Also make a RelocationEntry so the linker can see that this byte offset relates to a function by name.
// Here we initialise the offset to an index of self.code. After completing the function, we'll add
// other factors to make it relative to the code section. (All insertions will be known then.)
let offset = self.code.len() as u32;
self.code.encode_padded_u32(function_index);
// Make a RelocationEntry so the linker can see that this byte offset relates to a function by name.
// Here we initialise the offset to an index of self.code. After completing the function, we'll add
// other factors to make it relative to the code section. (All insertions will be known then.)
self.relocations.push(RelocationEntry::Index {
type_id: IndexRelocType::FunctionIndexLeb,
offset,
symbol_index,
});
log_instruction!(
"{:10}\t{}\t{:?}",
format!("{:?}", CALL),
function_index,
self.current_stack()
);
}
#[allow(dead_code)]
@ -591,26 +718,44 @@ impl<'a> CodeBuilder<'a> {
instruction_memargs!(i64_store32, I64STORE32, 2, false);
pub fn memory_size(&mut self) {
self.inst_imm8(CURRENTMEMORY, 0, true, 0);
self.inst(CURRENTMEMORY, 0, true);
self.code.push(0);
}
pub fn memory_grow(&mut self) {
self.inst_imm8(GROWMEMORY, 1, true, 0);
self.inst(GROWMEMORY, 1, true);
self.code.push(0);
}
fn log_const<T>(&self, opcode: OpCode, x: T)
where
T: std::fmt::Debug + std::fmt::Display,
{
log_instruction!(
"{:10}\t{}\t{:?}",
format!("{:?}", opcode),
x,
self.current_stack()
);
}
pub fn i32_const(&mut self, x: i32) {
self.inst(I32CONST, 0, true);
self.inst_base(I32CONST, 0, true);
self.code.encode_i32(x);
self.log_const(I32CONST, x);
}
pub fn i64_const(&mut self, x: i64) {
self.inst(I64CONST, 0, true);
self.inst_base(I64CONST, 0, true);
self.code.encode_i64(x);
self.log_const(I64CONST, x);
}
pub fn f32_const(&mut self, x: f32) {
self.inst(F32CONST, 0, true);
self.inst_base(F32CONST, 0, true);
self.code.encode_f32(x);
self.log_const(F32CONST, x);
}
pub fn f64_const(&mut self, x: f64) {
self.inst(F64CONST, 0, true);
self.inst_base(F64CONST, 0, true);
self.code.encode_f64(x);
self.log_const(F64CONST, x);
}
// TODO: Consider creating unified methods for numerical ops like 'eq' and 'add',

View file

@ -4,8 +4,6 @@ pub mod opcodes;
pub mod sections;
pub mod serialize;
pub use code_builder::{
Align, BlockType, CodeBuilder, LocalId, ValueType, VirtualMachineSymbolState,
};
pub use code_builder::{Align, BlockType, CodeBuilder, LocalId, ValueType, VmSymbolState};
pub use linking::{LinkingSubSection, SymInfo};
pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature, WasmModule};

View file

@ -1,5 +1,5 @@
#[repr(u8)]
#[derive(Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OpCode {
UNREACHABLE = 0x00,
NOP = 0x01,

View file

@ -29,6 +29,8 @@ pub enum SectionId {
Element = 9,
Code = 10,
Data = 11,
/// DataCount section is unused. Only needed for single-pass validation of
/// memory.init and data.drop, which we don't use
DataCount = 12,
}
@ -239,8 +241,7 @@ impl<'a> Serialize for ImportSection<'a> {
#[derive(Debug)]
pub struct FunctionSection<'a> {
/// Private. See WasmModule::add_function_signature
signature_indices: Vec<'a, u32>,
pub signature_indices: Vec<'a, u32>,
}
impl<'a> FunctionSection<'a> {
@ -525,42 +526,6 @@ impl Serialize for DataSection<'_> {
}
}
/*******************************************************************
*
* Data Count section
*
* Pre-declares the number of segments in the Data section.
* This helps the runtime to validate the module in a single pass.
* The order of sections is DataCount -> Code -> Data
*
*******************************************************************/
#[derive(Debug)]
struct DataCountSection {
count: u32,
}
impl DataCountSection {
fn new(data_section: &DataSection<'_>) -> Self {
let count = data_section
.segments
.iter()
.filter(|seg| !seg.init.is_empty())
.count() as u32;
DataCountSection { count }
}
}
impl Serialize for DataCountSection {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
if self.count > 0 {
let header_indices = write_section_header(buffer, SectionId::DataCount);
buffer.encode_u32(self.count);
update_section_size(buffer, header_indices);
}
}
}
/*******************************************************************
*
* Module
@ -658,11 +623,6 @@ impl<'a> WasmModule<'a> {
counter.serialize_and_count(buffer, &self.start);
counter.serialize_and_count(buffer, &self.element);
// Data Count section forward-declares the size of the Data section
// so that Code section can be validated in one pass
let data_count_section = DataCountSection::new(&self.data);
counter.serialize_and_count(buffer, &data_count_section);
// Code section is the only one with relocations so we can stop counting
let code_section_index = counter.section_index;
let code_section_body_index = self