mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-28 14:24:45 +00:00
Add support for Zig/LLVM "fast calling convention"
This commit is contained in:
parent
8ad2f13ba9
commit
e9f920827e
5 changed files with 205 additions and 122 deletions
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue