diff --git a/cli/src/build.rs b/cli/src/build.rs index cad9b9bf9c..82b8242bc7 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -240,7 +240,7 @@ pub fn build_file<'a>( app_o_file.to_str().unwrap(), ]; if matches!(opt_level, OptLevel::Development) { - inputs.push(bitcode::OBJ_PATH); + inputs.push(bitcode::BUILTINS_HOST_OBJ_PATH); } let (mut child, _) = // TODO use lld diff --git a/compiler/build/src/link.rs b/compiler/build/src/link.rs index a630a74609..d5285b3602 100644 --- a/compiler/build/src/link.rs +++ b/compiler/build/src/link.rs @@ -97,7 +97,7 @@ pub fn build_zig_host_native( "build-exe", "-fPIE", shared_lib_path.to_str().unwrap(), - bitcode::OBJ_PATH, + bitcode::BUILTINS_HOST_OBJ_PATH, ]); } else { command.args(&["build-obj", "-fPIC"]); @@ -186,7 +186,7 @@ pub fn build_zig_host_native( "build-exe", "-fPIE", shared_lib_path.to_str().unwrap(), - bitcode::OBJ_PATH, + bitcode::BUILTINS_HOST_OBJ_PATH, ]); } else { command.args(&["build-obj", "-fPIC"]); @@ -282,7 +282,7 @@ pub fn build_c_host_native( if let Some(shared_lib_path) = shared_lib_path { command.args(&[ shared_lib_path.to_str().unwrap(), - bitcode::OBJ_PATH, + bitcode::BUILTINS_HOST_OBJ_PATH, "-fPIE", "-pie", "-lm", diff --git a/compiler/builtins/bitcode/build.zig b/compiler/builtins/bitcode/build.zig index 9dba3a8d4b..93b970edd7 100644 --- a/compiler/builtins/bitcode/build.zig +++ b/compiler/builtins/bitcode/build.zig @@ -59,8 +59,9 @@ fn generateLlvmIrFile( // Generate Object File // TODO: figure out how to get this to emit symbols that are only scoped to linkage (global but hidden). -// Also, zig has -ffunction-sections, but I am not sure how to add it here. -// With both of those changes, unused zig functions will be cleaned up by the linker saving around 100k. +// @bhansconnect: I believe anything with global scope will still be preserved by the linker even if it +// is never called. I think it could theoretically be called by a dynamic lib that links to the executable +// or something similar. fn generateObjectFile( b: *Builder, mode: std.builtin.Mode, @@ -75,6 +76,7 @@ fn generateObjectFile( obj.setOutputDir("."); obj.strip = true; obj.target = target; + obj.link_function_sections = true; const obj_step = b.step(step_name, "Build object file for linking"); obj_step.dependOn(&obj.step); } diff --git a/compiler/builtins/src/bitcode.rs b/compiler/builtins/src/bitcode.rs index 45b297143f..a9a72917c0 100644 --- a/compiler/builtins/src/bitcode.rs +++ b/compiler/builtins/src/bitcode.rs @@ -1,10 +1,15 @@ use std::ops::Index; -pub const OBJ_PATH: &str = env!( +pub const BUILTINS_HOST_OBJ_PATH: &str = env!( "BUILTINS_HOST_O", "Env var BUILTINS_HOST_O not found. Is there a problem with the build script?" ); +pub const BUILTINS_WASM32_OBJ_PATH: &str = env!( + "BUILTINS_WASM32_O", + "Env var BUILTINS_WASM32_O not found. Is there a problem with the build script?" +); + #[derive(Debug, Default)] pub struct IntrinsicName { pub options: [&'static str; 14], diff --git a/compiler/gen_wasm/src/backend.rs b/compiler/gen_wasm/src/backend.rs index e0cedec8d5..791dfffc04 100644 --- a/compiler/gen_wasm/src/backend.rs +++ b/compiler/gen_wasm/src/backend.rs @@ -11,7 +11,8 @@ use roc_mono::layout::{Layout, LayoutIds}; use crate::layout::WasmLayout; use crate::storage::{Storage, StoredValue, StoredValueKind}; use crate::wasm_module::linking::{ - DataSymbol, LinkingSection, RelocationSection, WasmObjectSymbol, WASM_SYM_UNDEFINED, + DataSymbol, LinkingSection, RelocationSection, WasmObjectSymbol, WASM_SYM_BINDING_WEAK, + WASM_SYM_UNDEFINED, }; use crate::wasm_module::sections::{ CodeSection, DataMode, DataSection, DataSegment, ExportSection, FunctionSection, GlobalSection, @@ -21,7 +22,10 @@ use crate::wasm_module::{ code_builder, BlockType, CodeBuilder, ConstExpr, Export, ExportType, Global, GlobalType, LocalId, Signature, SymInfo, ValueType, }; -use crate::{copy_memory, CopyMemoryConfig, Env, PTR_TYPE}; +use crate::{ + copy_memory, CopyMemoryConfig, Env, BUILTINS_IMPORT_MODULE_NAME, MEMORY_NAME, PTR_TYPE, + STACK_POINTER_NAME, +}; /// The memory address where the constants data will be loaded during module instantiation. /// We avoid address zero and anywhere near it. They're valid addresses but maybe bug-prone. @@ -31,8 +35,6 @@ const CONST_SEGMENT_BASE_ADDR: u32 = 1024; /// Index of the data segment where we store constants const CONST_SEGMENT_INDEX: usize = 0; -const IMPORT_MODULE_BUILTINS: &str = "builtins"; - pub struct WasmBackend<'a> { env: &'a Env<'a>, @@ -66,7 +68,7 @@ impl<'a> WasmBackend<'a> { let num_procs = proc_symbols.len(); exports.push(Export { - name: "__linear_memory".to_string(), + name: MEMORY_NAME.to_string(), ty: ExportType::Mem, index: 0, }); @@ -78,10 +80,17 @@ impl<'a> WasmBackend<'a> { }, init: ConstExpr::I32(MEMORY_INIT_SIZE as i32), }; - linker_symbols.push(SymInfo::Global(WasmObjectSymbol::Defined { - flags: 0, + + exports.push(Export { + name: STACK_POINTER_NAME.to_string(), + ty: ExportType::Global, index: 0, - name: "__stack_pointer".to_string(), + }); + + linker_symbols.push(SymInfo::Global(WasmObjectSymbol::Defined { + flags: WASM_SYM_BINDING_WEAK, + index: 0, + name: STACK_POINTER_NAME.to_string(), })); let const_segment = DataSegment { @@ -759,7 +768,7 @@ impl<'a> WasmBackend<'a> { let import_index = self.module.import.entries.len() as u32; let import = Import { - module: IMPORT_MODULE_BUILTINS, + module: BUILTINS_IMPORT_MODULE_NAME, name: name.to_string(), description: ImportDesc::Func { signature_index }, }; diff --git a/compiler/gen_wasm/src/lib.rs b/compiler/gen_wasm/src/lib.rs index 26e3e63bdb..6fcad7c739 100644 --- a/compiler/gen_wasm/src/lib.rs +++ b/compiler/gen_wasm/src/lib.rs @@ -21,6 +21,9 @@ const PTR_TYPE: ValueType = ValueType::I32; pub const STACK_POINTER_GLOBAL_ID: u32 = 0; pub const FRAME_ALIGNMENT_BYTES: i32 = 16; +pub const MEMORY_NAME: &str = "memory"; +pub const BUILTINS_IMPORT_MODULE_NAME: &str = "builtins"; +pub const STACK_POINTER_NAME: &str = "__stack_pointer"; pub struct Env<'a> { pub arena: &'a Bump, diff --git a/compiler/test_gen/src/gen_num.rs b/compiler/test_gen/src/gen_num.rs index d88148090f..f1f0bdd27b 100644 --- a/compiler/test_gen/src/gen_num.rs +++ b/compiler/test_gen/src/gen_num.rs @@ -1330,7 +1330,7 @@ fn pow_int() { } #[test] -#[cfg(any(feature = "gen-llvm", feature = "gen-dev"))] +#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))] fn atan() { assert_evals_to!("Num.atan 10", 1.4711276743037347, f64); } diff --git a/compiler/test_gen/src/helpers/dev.rs b/compiler/test_gen/src/helpers/dev.rs index d49d602931..4dcab7ccd9 100644 --- a/compiler/test_gen/src/helpers/dev.rs +++ b/compiler/test_gen/src/helpers/dev.rs @@ -193,7 +193,10 @@ pub fn helper( app_o_file.clone(), // Long term we probably want a smarter way to link in zig builtins. // With the current method all methods are kept and it adds about 100k to all outputs. - &[app_o_file.to_str().unwrap(), bitcode::OBJ_PATH], + &[ + app_o_file.to_str().unwrap(), + bitcode::BUILTINS_HOST_OBJ_PATH, + ], LinkType::Dylib, ) .expect("failed to link dynamic library"); diff --git a/compiler/test_gen/src/helpers/wasm.rs b/compiler/test_gen/src/helpers/wasm.rs index d0a8ebfa26..b9ee9459aa 100644 --- a/compiler/test_gen/src/helpers/wasm.rs +++ b/compiler/test_gen/src/helpers/wasm.rs @@ -4,8 +4,12 @@ use std::hash::{Hash, Hasher}; use crate::helpers::from_wasm32_memory::FromWasm32Memory; use crate::helpers::wasm32_test_result::Wasm32TestResult; +use roc_builtins::bitcode; use roc_can::builtins::builtin_defs_map; use roc_collections::all::{MutMap, MutSet}; +use roc_gen_wasm::MEMORY_NAME; + +use tempfile::tempdir; const TEST_WRAPPER_NAME: &str = "test_wrapper"; @@ -143,8 +147,43 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>( use wasmer::{Instance, Module, Store}; let store = Store::default(); - // let module = Module::from_file(&store, &test_wasm_path).unwrap(); - let wasmer_module = Module::from_binary(&store, &module_bytes).unwrap(); + + let wasmer_module = { + let dir = tempdir().unwrap(); + let dirpath = dir.path(); + let final_wasm_file = dirpath.join("final.wasm"); + let app_o_file = dirpath.join("app.o"); + + // write the module to a file so the linker can access it + std::fs::write(&app_o_file, &module_bytes).unwrap(); + + std::process::Command::new("zig") + .args(&[ + "wasm-ld", + // input files + app_o_file.to_str().unwrap(), + bitcode::BUILTINS_WASM32_OBJ_PATH, + // output + "-o", + final_wasm_file.to_str().unwrap(), + // we don't define `_start` + "--no-entry", + // If you only specify test_wrapper, it will stop at the call to UserApp_main_1 + // But if you specify both exports, you get all the dependencies. + // + // It seems that it will not write out an export you didn't explicitly specify, + // even if it's a dependency of another export! + // In our case we always export main and test_wrapper so that's OK. + "--export", + "test_wrapper", + "--export", + "#UserApp_main_1", + ]) + .output() + .unwrap(); + + Module::from_file(&store, &final_wasm_file).unwrap() + }; // First, we create the `WasiEnv` use wasmer_wasi::WasiState; @@ -171,7 +210,7 @@ where let instance = crate::helpers::wasm::helper_wasm(&arena, src, stdlib, &expected); - let memory = instance.exports.get_memory("__linear_memory").unwrap(); + let memory = instance.exports.get_memory(MEMORY_NAME).unwrap(); let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap();