Add support for Zig/LLVM "fast calling convention"

This commit is contained in:
Brian Carroll 2021-11-15 19:20:16 +00:00
parent 8ad2f13ba9
commit e9f920827e
5 changed files with 205 additions and 122 deletions

View file

@ -444,6 +444,13 @@ impl<'a> WasmBackend<'a> {
Ok(()) 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)), x => Err(format!("statement not yet implemented: {:?}", x)),
} }
} }
@ -558,7 +565,8 @@ impl<'a> WasmBackend<'a> {
arguments: &'a [Symbol], arguments: &'a [Symbol],
return_layout: WasmLayout, return_layout: WasmLayout,
) -> Result<(), String> { ) -> Result<(), String> {
self.storage.load_symbols(&mut self.code_builder, arguments); self.storage
.load_symbols_fastcc(&mut self.code_builder, arguments, &return_layout);
let build_result = decode_low_level( let build_result = decode_low_level(
&mut self.code_builder, &mut self.code_builder,
@ -764,11 +772,27 @@ impl<'a> WasmBackend<'a> {
}, },
None => { None => {
let mut param_types = Vec::with_capacity_in(arguments.len(), self.env.arena); let mut param_types = Vec::with_capacity_in(1 + arguments.len(), self.env.arena);
param_types.extend(arguments.iter().map(|a| self.storage.get(a).value_type()));
let ret_type = if ret_layout.is_stack_memory() {
param_types.push(ValueType::I32);
None
} else {
Some(ret_layout.value_type())
};
for arg in arguments {
param_types.push(match self.storage.get(arg) {
StoredValue::StackMemory { size, .. } if *size > 4 && *size <= 8 => {
ValueType::I64
}
_ => ValueType::I32,
});
}
let signature_index = self.module.types.insert(Signature { let signature_index = self.module.types.insert(Signature {
param_types, 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; let import_index = self.module.import.entries.len() as u32;

View file

@ -7,6 +7,7 @@ pub mod wasm_module;
use bumpalo::{self, collections::Vec, Bump}; use bumpalo::{self, collections::Vec, Bump};
use roc_collections::all::{MutMap, MutSet}; use roc_collections::all::{MutMap, MutSet};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{Interns, Symbol}; use roc_module::symbol::{Interns, Symbol};
use roc_mono::ir::{Proc, ProcLayout}; use roc_mono::ir::{Proc, ProcLayout};
use roc_mono::layout::LayoutIds; use roc_mono::layout::LayoutIds;
@ -47,34 +48,50 @@ pub fn build_module_help<'a>(
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> Result<WasmModule<'a>, String> { ) -> Result<WasmModule<'a>, String> {
let mut layout_ids = LayoutIds::default(); 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 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);
// Collect the symbols & names for the procedures // Collect the symbols & names for the procedures,
for (i, (sym, layout)) in procedures.keys().enumerate() { // and filter out procs we're going to inline
proc_symbols.push(*sym); let mut fn_index = 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 let fn_name = layout_ids
.get_toplevel(*sym, layout) .get_toplevel(sym, &layout)
.to_symbol_string(*sym, &env.interns); .to_symbol_string(sym, &env.interns);
if env.exposed_to_host.contains(sym) { if env.exposed_to_host.contains(&sym) {
exports.push(Export { exports.push(Export {
name: fn_name.clone(), name: fn_name.clone(),
ty: ExportType::Func, ty: ExportType::Func,
index: i as u32, index: fn_index as u32,
}); });
} }
let linker_sym = SymInfo::for_function(i as u32, fn_name); let linker_sym = SymInfo::for_function(fn_index as u32, fn_name);
linker_symbols.push(linker_sym); 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 module, linker_symbols) = {
let mut backend = WasmBackend::new(env, layout_ids, proc_symbols, linker_symbols, exports); let mut backend = WasmBackend::new(
for ((sym, _), proc) in procedures.into_iter() { 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.build_proc(proc, sym)?;
} }
(backend.module, backend.linker_symbols) (backend.module, backend.linker_symbols)

View file

@ -5,7 +5,7 @@ use roc_collections::all::MutMap;
use roc_module::symbol::Symbol; use roc_module::symbol::Symbol;
use crate::layout::WasmLayout; use crate::layout::WasmLayout;
use crate::wasm_module::{CodeBuilder, LocalId, ValueType, VmSymbolState}; use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState};
use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_SIZE, PTR_TYPE}; use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_SIZE, PTR_TYPE};
pub enum StoredValueKind { pub enum StoredValueKind {
@ -194,6 +194,67 @@ impl<'a> Storage<'a> {
}) })
} }
/// Load a symbol using the C Calling Convention
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, .. }
| StoredValue::StackMemory {
location: StackMemoryLocation::PointerArg(local_id),
..
} => {
code_builder.get_local(local_id);
code_builder.set_top_symbol(sym);
}
StoredValue::StackMemory {
location: StackMemoryLocation::FrameOffset(offset),
..
} => {
code_builder.get_local(self.stack_frame_pointer.unwrap());
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 /// 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, /// Avoid calling this method in a loop with one symbol at a time! It will work,
/// but it generates very inefficient Wasm code. /// but it generates very inefficient Wasm code.
@ -204,61 +265,55 @@ impl<'a> Storage<'a> {
return; return;
} }
for sym in symbols.iter() { for sym in symbols.iter() {
let storage = self.get(sym).to_owned(); self.load_symbol_ccc(code_builder, *sym);
match storage { }
StoredValue::VirtualMachineStack { }
vm_state,
value_type, /// Load symbols in a way compatible with LLVM's "fast calling convention"
size, /// 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
let next_local_id = self.get_next_local_id(); /// in favour of CPU registers (or VM stack values, which eventually become CPU registers).
let maybe_next_vm_state = /// We need to convert some of our structs from our internal C-like representation to work with Zig.
code_builder.load_symbol(*sym, vm_state, next_local_id); /// Why not just always use the fastcc representation? Because of non-Zig platforms.
match maybe_next_vm_state { pub fn load_symbols_fastcc(
// The act of loading the value changed the VM state, so update it &mut self,
Some(next_vm_state) => { code_builder: &mut CodeBuilder,
self.symbol_storage_map.insert( symbols: &[Symbol],
*sym, return_layout: &WasmLayout,
StoredValue::VirtualMachineStack { ) {
vm_state: next_vm_state, if return_layout.is_stack_memory() {
value_type, // Load the address where the return value should be written
size, self.load_symbol_ccc(code_builder, symbols[0]);
}, };
);
} for sym in symbols {
None => { if let StoredValue::StackMemory {
// Loading the value required creating a new local, because location,
// it was not in a convenient position in the VM stack. size,
self.local_types.push(value_type); alignment_bytes,
self.symbol_storage_map.insert( } = self.get(sym)
*sym, {
StoredValue::Local { if *size == 0 {
local_id: next_local_id, unimplemented!("Passing zero-sized values is not implemented yet");
value_type, } else if *size > 8 {
size, return self.load_symbol_ccc(code_builder, *sym);
},
);
}
}
}
StoredValue::Local { local_id, .. }
| StoredValue::StackMemory {
location: StackMemoryLocation::PointerArg(local_id),
..
} => {
code_builder.get_local(local_id);
code_builder.set_top_symbol(*sym);
} }
StoredValue::StackMemory { let (local_id, offset) = location.local_and_offset(self.stack_frame_pointer);
location: StackMemoryLocation::FrameOffset(offset), code_builder.get_local(local_id);
.. let align = Align::from(*alignment_bytes);
} => {
code_builder.get_local(self.stack_frame_pointer.unwrap()); if *size == 1 {
code_builder.i32_const(offset as i32); code_builder.i32_load8_u(align, offset);
code_builder.i32_add(); } else if *size == 2 {
code_builder.set_top_symbol(*sym); 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);
} }
} }
} }

View file

@ -9,7 +9,7 @@ use roc_can::builtins::builtin_defs_map;
use roc_collections::all::{MutMap, MutSet}; use roc_collections::all::{MutMap, MutSet};
use roc_gen_wasm::MEMORY_NAME; use roc_gen_wasm::MEMORY_NAME;
use tempfile::tempdir; use tempfile::{TempDir, tempdir};
const TEST_WRAPPER_NAME: &str = "test_wrapper"; const TEST_WRAPPER_NAME: &str = "test_wrapper";
@ -118,39 +118,34 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
let mut module_bytes = std::vec::Vec::with_capacity(4096); let mut module_bytes = std::vec::Vec::with_capacity(4096);
wasm_module.serialize_mut(&mut module_bytes); wasm_module.serialize_mut(&mut module_bytes);
// for debugging (e.g. with wasm2wat or wasm-objdump)
if false {
use std::io::Write;
let mut hash_state = DefaultHasher::new();
src.hash(&mut hash_state);
let src_hash = hash_state.finish();
// Filename contains a hash of the Roc test source code. Helpful when comparing across commits.
let dir = "/tmp/roc/gen_wasm";
std::fs::create_dir_all(dir).unwrap();
let path = format!("{}/test-{:016x}.wasm", dir, src_hash);
// Print out filename (appears just after test name)
println!("dumping file {:?}", path);
match std::fs::File::create(path) {
Err(e) => eprintln!("Problem creating wasm debug file: {:?}", e),
Ok(mut file) => {
file.write_all(&module_bytes).unwrap();
}
}
}
// now, do wasmer stuff // now, do wasmer stuff
use wasmer::{Instance, Module, Store}; use wasmer::{Instance, Module, Store};
let store = Store::default(); let store = Store::default();
// Keep the final .wasm file for debugging with wasm-objdump or wasm2wat
const DEBUG_WASM_FILE: bool = true;
let wasmer_module = { let wasmer_module = {
let dir = tempdir().unwrap(); let tmp_dir: TempDir; // directory for normal test runs, deleted when dropped
let dirpath = dir.path(); let debug_dir: String; // persistent directory for debugging
let dirpath: &Path =
if DEBUG_WASM_FILE {
// Directory name based on a hash of the Roc source
let mut hash_state = DefaultHasher::new();
src.hash(&mut hash_state);
let src_hash = hash_state.finish();
debug_dir = format!("/tmp/roc/gen_wasm/{:016x}", src_hash);
std::fs::create_dir_all(&debug_dir).unwrap();
println!("Debug command:\n\twasm-objdump -sdx {}/final.wasm", &debug_dir);
Path::new(&debug_dir)
} else {
tmp_dir = tempdir().unwrap();
tmp_dir.path()
};
let final_wasm_file = dirpath.join("final.wasm"); let final_wasm_file = dirpath.join("final.wasm");
let app_o_file = dirpath.join("app.o"); let app_o_file = dirpath.join("app.o");

View file

@ -296,31 +296,23 @@ fn long_str_literal() {
); );
} }
// #[test] #[test]
// fn small_str_concat_empty_first_arg() { fn small_str_concat_empty_first_arg() {
// assert_llvm_evals_to!( assert_evals_to!(
// r#"Str.concat "" "JJJJJJJJJJJJJJJ""#, r#"Str.concat "" "JJJJJJJ""#,
// [ [
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0x4a,
// 0x4a, 0b1000_0111
// 0x4a, ],
// 0x4a, [u8; 8]
// 0x4a, );
// 0x4a, }
// 0x4a,
// 0x4a,
// 0x4a,
// 0b1000_1111
// ],
// [u8; 16]
// );
// }
// #[test] // #[test]
// fn small_str_concat_empty_second_arg() { // fn small_str_concat_empty_second_arg() {