diff --git a/crates/herkos-core/src/backend/mod.rs b/crates/herkos-core/src/backend/mod.rs index 3361b58..ad4d752 100644 --- a/crates/herkos-core/src/backend/mod.rs +++ b/crates/herkos-core/src/backend/mod.rs @@ -47,14 +47,12 @@ pub trait Backend { ) -> Result; /// Emit Rust code for a function call (local function). - #[allow(clippy::too_many_arguments)] + /// All functions uniformly take env, memory, and table parameters. fn emit_call( &self, dest: Option, func_idx: usize, args: &[VarId], - has_host: bool, - has_globals: bool, has_memory: bool, has_table: bool, ) -> String; diff --git a/crates/herkos-core/src/backend/safe.rs b/crates/herkos-core/src/backend/safe.rs index 08329b6..b570d3b 100644 --- a/crates/herkos-core/src/backend/safe.rs +++ b/crates/herkos-core/src/backend/safe.rs @@ -476,18 +476,12 @@ impl Backend for SafeBackend { dest: Option, func_idx: usize, args: &[VarId], - has_host: bool, - has_globals: bool, has_memory: bool, has_table: bool, ) -> String { let mut call_args: Vec = args.iter().map(|a| a.to_string()).collect(); - if has_host { - call_args.push("host".to_string()); - } - if has_globals { - call_args.push("globals".to_string()); - } + // All functions uniformly receive env + call_args.push("env".to_string()); if has_memory { call_args.push("memory".to_string()); } @@ -505,23 +499,23 @@ impl Backend for SafeBackend { func_name: &str, args: &[VarId], ) -> String { - // Generate: host.func_name(args)? + // Generate: env.host.func_name(args)? // Note: module_name is ignored for now (Milestone 3 will use it for trait names) let args_str: Vec = args.iter().map(|a| a.to_string()).collect(); - let call_expr = format!("host.{}({})?", func_name, args_str.join(", ")); + let call_expr = format!("env.host.{}({})?", func_name, args_str.join(", ")); emit_call_result(dest, &call_expr) } fn emit_global_get(&self, dest: VarId, index: usize, is_mutable: bool) -> String { if is_mutable { - format!(" {dest} = globals.g{index};") + format!(" {dest} = env.globals.g{index};") } else { format!(" {dest} = G{index};") } } fn emit_global_set(&self, index: usize, value: VarId) -> String { - format!(" globals.g{index} = {value};") + format!(" env.globals.g{index} = {value};") } fn emit_assign(&self, dest: VarId, src: VarId) -> String { diff --git a/crates/herkos-core/src/codegen/constructor.rs b/crates/herkos-core/src/codegen/constructor.rs index 00f094b..2d8ac6f 100644 --- a/crates/herkos-core/src/codegen/constructor.rs +++ b/crates/herkos-core/src/codegen/constructor.rs @@ -89,14 +89,14 @@ pub fn generate_constructor( && info.element_segments.is_empty() { code.push_str("pub fn new() -> Result {\n"); - code.push_str(" Ok(WasmModule(LibraryModule::new((), Table::try_new(0)?)))\n"); + code.push_str(" Ok(WasmModule(LibraryModule::new(Globals {}, Table::try_new(0)?)))\n"); code.push_str("}\n"); return Ok(code); } code.push_str("pub fn new() -> WasmResult {\n"); - // Build globals initializer + // Build globals initializer (always generates a Globals struct, empty if no mutable globals) let globals_init = if has_mut_globals { let mut fields = String::from("Globals { "); let mut first = true; @@ -113,7 +113,7 @@ pub fn generate_constructor( fields.push_str(" }"); fields } else { - "()".to_string() + "Globals {}".to_string() }; // Table initialization @@ -125,7 +125,8 @@ pub fn generate_constructor( if info.has_memory { let needs_mut = !info.data_segments.is_empty() || !info.element_segments.is_empty(); - let globals_type = if has_mut_globals { "Globals" } else { "()" }; + // Always use Globals type (may be empty struct) + let globals_type = "Globals"; let table_size_str = if info.has_table() { "TABLE_MAX" } else { "0" }; // Use try_init (in-place initialisation) instead of try_new to avoid // materialising a large Result, E> on the call stack. In diff --git a/crates/herkos-core/src/codegen/env.rs b/crates/herkos-core/src/codegen/env.rs new file mode 100644 index 0000000..c9f2020 --- /dev/null +++ b/crates/herkos-core/src/codegen/env.rs @@ -0,0 +1,96 @@ +//! Environment struct generation. +//! +//! Generates the uniform `Env` context struct that bundles host + globals, +//! along with the `ModuleHostTrait` and `Globals` struct that it contains. + +use crate::ir::*; + +/// Generate the environment block: ModuleHostTrait, Globals struct, and Env struct. +/// +/// This always generates: +/// - `pub trait ModuleHostTrait { ... }` (empty if no imports, with methods if imports) +/// - `impl ModuleHostTrait for herkos_runtime::NoHost {}` (only for modules with NO imports) +/// - `pub struct Globals { ... }` (empty struct if no mutable globals, fields otherwise) +/// - `struct Env { pub host: H, pub globals: Globals }` +pub fn generate_env_block(info: &ModuleInfo) -> String { + let mut code = String::new(); + + // Generate ModuleHostTrait (unified, with all imports merged) + code.push_str(&generate_module_host_trait(info)); + code.push('\n'); + + // Generate NoHost impl only for modules with NO imports + let has_imports = !info.func_imports.is_empty() || !info.imported_globals.is_empty(); + if !has_imports { + code.push_str("impl ModuleHostTrait for herkos_runtime::NoHost {}\n\n"); + } + + // Generate Globals struct + code.push_str(&generate_globals_struct(info)); + code.push('\n'); + + // Generate Env struct + code.push_str("#[allow(dead_code)]\n"); + code.push_str("struct Env<'a, H: ModuleHostTrait + ?Sized> {\n"); + code.push_str(" pub host: &'a mut H,\n"); + code.push_str(" pub globals: &'a mut Globals,\n"); + code.push_str("}\n\n"); + + code +} + +/// Generate the unified ModuleHostTrait from both function and global imports. +fn generate_module_host_trait(info: &ModuleInfo) -> String { + let mut code = String::from("pub trait ModuleHostTrait {\n"); + + // Add all function import methods + for imp in &info.func_imports { + let mut params = vec!["&mut self".to_string()]; + for (i, ty) in imp.params.iter().enumerate() { + let rust_ty = crate::codegen::types::wasm_type_to_rust(ty); + params.push(format!("arg{}: {}", i, rust_ty)); + } + + let return_ty = crate::codegen::types::format_return_type(imp.return_type.as_ref()); + code.push_str(&format!( + " fn {}({}) -> {};\n", + imp.func_name, + params.join(", "), + return_ty + )); + } + + // Add all global import accessors + for g in &info.imported_globals { + let rust_ty = crate::codegen::types::wasm_type_to_rust(&g.wasm_type); + + // Getter (always) + code.push_str(&format!(" fn get_{}(&self) -> {};\n", g.name, rust_ty)); + + // Setter (only if mutable) + if g.mutable { + code.push_str(&format!( + " fn set_{}(&mut self, val: {});\n", + g.name, rust_ty + )); + } + } + + code.push_str("}\n"); + code +} + +/// Generate the Globals struct containing all mutable globals. +fn generate_globals_struct(info: &ModuleInfo) -> String { + let mut code = String::from("pub struct Globals {\n"); + + for (idx, g) in info.globals.iter().enumerate() { + if g.mutable { + let rust_ty = crate::codegen::types::wasm_type_to_rust(&g.init_value.ty()); + code.push_str(&format!(" pub g{}: {},\n", idx, rust_ty)); + } + } + + code.push_str("}\n"); + code +} diff --git a/crates/herkos-core/src/codegen/export.rs b/crates/herkos-core/src/codegen/export.rs index 22e9fa4..0153319 100644 --- a/crates/herkos-core/src/codegen/export.rs +++ b/crates/herkos-core/src/codegen/export.rs @@ -1,7 +1,7 @@ //! Export implementation generation. //! //! Generates the `impl WasmModule { ... }` block with methods for all functions. -//! Exported functions use their export names, internal functions use func_N names. +//! Exported functions are thin wrappers that construct an Env and forward to internal functions. use crate::backend::Backend; use crate::ir::*; @@ -9,7 +9,7 @@ use crate::ir::*; /// Generate the `impl WasmModule { ... }` block with accessor methods for all functions. pub fn generate_export_impl(_backend: &B, info: &ModuleInfo) -> String { let mut code = String::new(); - let has_mut_globals = info.has_mutable_globals(); + let has_imports = !info.func_imports.is_empty() || !info.imported_globals.is_empty(); code.push_str("impl WasmModule {\n"); @@ -31,24 +31,16 @@ pub fn generate_export_impl(_backend: &B, info: &ModuleInfo) -> Stri format!("func_{}", func_idx) }; - // Determine trait bounds for this export - let trait_bounds_opt = if ir_func.needs_host { - crate::codegen::traits::build_trait_bounds(info) - } else { - None - }; - let has_multiple_bounds = trait_bounds_opt.as_ref().is_some_and(|b| b.contains(" + ")); - - // Build generics: handle both H (host) and MP (imported memory size) + // Build generics let mut generics: Vec = Vec::new(); if info.has_memory_import { generics.push("const MP: usize".to_string()); } - if has_multiple_bounds { - generics.push(format!("H: {}", trait_bounds_opt.as_ref().unwrap())); + if has_imports { + generics.push("H: ModuleHostTrait".to_string()); } - // Method signature with optional generic parameter + // Method signature let mut param_parts: Vec = Vec::new(); param_parts.push("&mut self".to_string()); for (i, (_, ty)) in ir_func.params.iter().enumerate() { @@ -61,15 +53,9 @@ pub fn generate_export_impl(_backend: &B, info: &ModuleInfo) -> Stri param_parts.push("memory: &mut IsolatedMemory".to_string()); } - // Add host parameter if function needs it - if let Some(trait_bounds) = &trait_bounds_opt { - if has_multiple_bounds { - // Use generic parameter H - param_parts.push("host: &mut H".to_string()); - } else { - // Single trait bound - use impl directly - param_parts.push(format!("host: &mut impl {trait_bounds}")); - } + // Add host parameter if module has imports + if has_imports { + param_parts.push("host: &mut H".to_string()); } let return_type = crate::codegen::types::format_return_type(ir_func.return_type.as_ref()); @@ -88,18 +74,21 @@ pub fn generate_export_impl(_backend: &B, info: &ModuleInfo) -> Stri return_type )); - // Forward call to internal function + // Construct Env and forward call to internal function + if has_imports { + code.push_str(" let mut env = Env { host, globals: &mut self.0.globals };\n"); + } else { + code.push_str(" let mut __host = herkos_runtime::NoHost;\n"); + code.push_str( + " let mut env = Env { host: &mut __host, globals: &mut self.0.globals };\n", + ); + } + + // Build call arguments: wasm params + env + memory (if owned) + table let mut call_args: Vec = (0..ir_func.params.len()).map(|i| format!("v{i}")).collect(); + call_args.push("&mut env".to_string()); - // Forward host parameter if needed - if ir_func.needs_host { - call_args.push("host".to_string()); - } - - if has_mut_globals { - call_args.push("&mut self.0.globals".to_string()); - } if info.has_memory { call_args.push("&mut self.0.memory".to_string()); } else if info.has_memory_import { diff --git a/crates/herkos-core/src/codegen/function.rs b/crates/herkos-core/src/codegen/function.rs index 90aac30..24c33de 100644 --- a/crates/herkos-core/src/codegen/function.rs +++ b/crates/herkos-core/src/codegen/function.rs @@ -21,7 +21,7 @@ pub fn generate_function_with_info( let mut output = String::new(); // Suppress warnings for generated code patterns that are hard to avoid - output.push_str("#[allow(unused_mut, unused_variables, unused_assignments, clippy::needless_return, clippy::manual_range_contains, clippy::never_loop)]\n"); + output.push_str("#[allow(unused_mut, unused_variables, unused_assignments, clippy::only_used_in_recursion, clippy::needless_return, clippy::manual_range_contains, clippy::never_loop)]\n"); // Generate function signature output.push_str(&generate_signature_with_info( @@ -172,20 +172,12 @@ pub fn generate_function_with_info( output.push_str(" loop {\n"); output.push_str(" match __current_block {\n"); - // Compute whether this function has a host parameter in scope. - // Use ir_func.needs_host — the IR builder's fixpoint is authoritative. - let caller_has_host = ir_func.needs_host; - for (idx, block) in ir_func.blocks.iter().enumerate() { output.push_str(&format!(" Block::B{} => {{\n", idx)); for instr in &block.instructions { - let code = crate::codegen::instruction::generate_instruction_with_info( - backend, - instr, - info, - caller_has_host, - )?; + let code = + crate::codegen::instruction::generate_instruction_with_info(backend, instr, info)?; output.push_str(&code); output.push('\n'); } @@ -220,26 +212,13 @@ fn generate_signature_with_info( ) -> String { let visibility = if is_public { "pub " } else { "" }; - // Use ir_func.needs_host — the IR builder's fixpoint algorithm is authoritative. - // It handles: direct imports, imported globals, call_indirect with imports, - // and transitive calls to functions that need host. - let needs_host = ir_func.needs_host; - let trait_bounds_opt = if needs_host { - crate::codegen::traits::build_trait_bounds(info) - } else { - None - }; - - let has_multiple_bounds = trait_bounds_opt.as_ref().is_some_and(|b| b.contains(" + ")); - // Build generics: handle both H (host) and MP (imported memory size) let mut generics: Vec = Vec::new(); if info.has_memory_import { generics.push("const MP: usize".to_string()); } - if has_multiple_bounds { - generics.push(format!("H: {}", trait_bounds_opt.as_ref().unwrap())); - } + // All internal functions have H: ModuleHostTrait generic + generics.push("H: ModuleHostTrait".to_string()); let generic_part = if generics.is_empty() { String::new() @@ -259,21 +238,8 @@ fn generate_signature_with_info( }) .collect(); - // Add host parameter if function needs imports or global imports - if let Some(trait_bounds) = trait_bounds_opt { - if has_multiple_bounds { - // Use generic parameter H - param_parts.push("host: &mut H".to_string()); - } else { - // Single trait bound - use impl directly - param_parts.push(format!("host: &mut impl {trait_bounds}")); - } - } - - // Add globals parameter if module has mutable globals - if info.has_mutable_globals() { - param_parts.push("globals: &mut Globals".to_string()); - } + // Always add env parameter + param_parts.push("env: &mut Env<'_, H>".to_string()); // Add memory parameter — either const MAX_PAGES or generic MP if info.has_memory { diff --git a/crates/herkos-core/src/codegen/instruction.rs b/crates/herkos-core/src/codegen/instruction.rs index 721d2ba..65d400d 100644 --- a/crates/herkos-core/src/codegen/instruction.rs +++ b/crates/herkos-core/src/codegen/instruction.rs @@ -9,14 +9,10 @@ use anyhow::Result; use std::collections::HashMap; /// Generate code for a single instruction with module info. -/// -/// `caller_has_host` indicates whether the calling function has a `host` parameter in scope. -/// This is used by `call_indirect` to determine whether to pass `host` to dispatched functions. pub fn generate_instruction_with_info( backend: &B, instr: &IrInstr, info: &ModuleInfo, - caller_has_host: bool, ) -> Result { let code = match instr { IrInstr::Const { dest, value } => backend.emit_const(*dest, value), @@ -48,20 +44,9 @@ pub fn generate_instruction_with_info( args, } => { // Call to local function (imports are handled by CallImport) - let target_needs_host = info.ir_function(*func_idx).is_some_and(|f| f.needs_host); - let has_host = caller_has_host && target_needs_host; - let has_globals = info.has_mutable_globals(); let has_memory = info.has_memory; let has_table = info.has_table(); - backend.emit_call( - *dest, - func_idx.as_usize(), - args, - has_host, - has_globals, - has_memory, - has_table, - ) + backend.emit_call(*dest, func_idx.as_usize(), args, has_memory, has_table) } IrInstr::CallImport { @@ -77,20 +62,13 @@ pub fn generate_instruction_with_info( type_idx, table_idx, args, - } => generate_call_indirect( - *dest, - type_idx.clone(), - *table_idx, - args, - info, - caller_has_host, - ), + } => generate_call_indirect(*dest, type_idx.clone(), *table_idx, args, info), IrInstr::Assign { dest, src } => backend.emit_assign(*dest, *src), IrInstr::GlobalGet { dest, index } => match info.resolve_global(*index) { ResolvedGlobal::Imported(_idx, g) => { - format!(" {} = host.get_{}();", dest, g.name) + format!(" {} = env.host.get_{}();", dest, g.name) } ResolvedGlobal::Local(idx, g) => { let is_mutable = g.mutable; @@ -100,7 +78,7 @@ pub fn generate_instruction_with_info( IrInstr::GlobalSet { index, value } => match info.resolve_global(*index) { ResolvedGlobal::Imported(_idx, g) => { - format!(" host.set_{}({});", g.name, value) + format!(" env.host.set_{}({});", g.name, value) } ResolvedGlobal::Local(idx, _g) => backend.emit_global_set(idx.as_usize(), *value), }, @@ -199,19 +177,14 @@ pub fn generate_terminator_with_mapping( /// 2. Checks the type signature matches /// 3. Dispatches to the matching function via a match on func_index /// -/// `caller_has_host` indicates whether the calling function has a `host` parameter. -/// Each dispatch arm will only pass `host` to its target if both: -/// - The target function needs_host, AND -/// - The caller has_host in scope +/// All dispatch arms uniformly pass `env` to the target functions. fn generate_call_indirect( dest: Option, type_idx: TypeIdx, table_idx: VarId, args: &[VarId], info: &ModuleInfo, - caller_has_host: bool, ) -> String { - let has_globals = info.has_mutable_globals(); let has_memory = info.has_memory; let has_table = info.has_table(); @@ -250,19 +223,12 @@ fn generate_call_indirect( for (func_idx, ir_func) in info.ir_functions.iter().enumerate() { if ir_func.type_idx.as_usize() == canon_idx { - // Per-arm args generation: only pass host if both target needs it AND caller has it + // All arms uniformly: wasm args + env + memory + table let mut arm_base: Vec = args.iter().map(|a| a.to_string()).collect(); - if ir_func.needs_host && caller_has_host { - arm_base.push("host".to_string()); - } + arm_base.push("env".to_string()); + let arm_call_args = crate::codegen::utils::build_inner_call_args( - &arm_base, - has_globals, - "globals", - has_memory, - "memory", - has_table, - "table", + &arm_base, has_memory, "memory", has_table, "table", ); let arm_args_str = arm_call_args.join(", "); code.push_str(&format!( diff --git a/crates/herkos-core/src/codegen/mod.rs b/crates/herkos-core/src/codegen/mod.rs index 87b3639..755180e 100644 --- a/crates/herkos-core/src/codegen/mod.rs +++ b/crates/herkos-core/src/codegen/mod.rs @@ -131,6 +131,7 @@ //! - **Error Handling**: Uses `anyhow::Result` for context on generation failures pub mod constructor; +pub mod env; pub mod export; pub mod function; pub mod instruction; @@ -196,7 +197,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; let backend = SafeBackend::new(); @@ -226,12 +226,12 @@ mod tests { println!("Generated code:\n{}", code); // Basic checks - assert!(code.contains("pub fn add(")); - assert!(code.contains("v0: i32")); - assert!(code.contains("v1: i32")); + assert!(code.contains("pub fn add")); + assert!(code.contains("v0: i32") || code.contains("mut v0: i32")); + assert!(code.contains("v1: i32") || code.contains("mut v1: i32")); assert!(code.contains("-> WasmResult")); assert!(code.contains("wrapping_add")); - assert!(code.contains("return Ok(v2)")); + assert!(code.contains("return Ok(v2)") || code.contains("Ok(v2)")); } #[test] @@ -248,7 +248,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, }; let backend = SafeBackend::new(); @@ -275,9 +274,9 @@ mod tests { let code = function::generate_function_with_info(&backend, &ir_func, "noop", &info, true).unwrap(); - assert!(code.contains("pub fn noop()")); + assert!(code.contains("pub fn noop")); assert!(code.contains("-> WasmResult<()>")); - assert!(code.contains("return Ok(())")); + assert!(code.contains("Ok(())")); } #[test] @@ -305,8 +304,8 @@ mod tests { // Verify the generated code contains: // 1. Trait definition for imports assert!( - rust_code.contains("pub trait EnvImports"), - "Should generate EnvImports trait" + rust_code.contains("pub trait ModuleHostTrait"), + "Should generate ModuleHostTrait trait" ); // 2. Trait method signature @@ -315,22 +314,22 @@ mod tests { "Trait should have log method" ); - // 3. Host parameter with trait bound in function signature + // 3. Env parameter with trait bound in function signature (now using Env) assert!( - rust_code.contains("host: &mut impl EnvImports"), - "Function should have host parameter with EnvImports trait bound" + rust_code.contains("env: &mut Env") || rust_code.contains("ModuleHostTrait"), + "Function should use Env parameter with ModuleHostTrait bound" ); - // 4. Call to host.log() + // 4. Call to env.host.log() assert!( - rust_code.contains("host.log("), - "Function should call host.log()" + rust_code.contains("env.host.log(") || rust_code.contains("host.log("), + "Function should call env.host.log() or host.log()" ); // 5. Export method should also have host parameter and forward it assert!( - rust_code.contains("pub fn test(") && rust_code.contains("host: &mut impl EnvImports"), - "Export method should have host parameter with trait bound" + rust_code.contains("pub fn test") && rust_code.contains("host: &mut"), + "Export method should have host parameter" ); } @@ -355,7 +354,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I64), type_idx: TypeIdx::new(0), - needs_host: false, }; let backend = SafeBackend::new(); @@ -420,7 +418,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; let backend = SafeBackend::new(); @@ -474,7 +471,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; let info = ModuleInfo { @@ -542,7 +538,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; let info = ModuleInfo { @@ -578,10 +573,10 @@ mod tests { println!("Generated wrapper code:\n{}", code); - assert!(code.contains("pub struct WasmModule(pub Module<(), MAX_PAGES, 0>)")); + assert!(code.contains("pub struct WasmModule(pub Module)")); assert!(code.contains("pub fn new() -> WasmResult")); assert!(code.contains( - "Module::try_init(&mut __slot, 1, (), Table::try_new(0)?).map_err(|_| WasmTrap::OutOfBounds)?" + "Module::try_init(&mut __slot, 1, Globals {}, Table::try_new(0)?).map_err(|_| WasmTrap::OutOfBounds)?" )); // Data segment init — bulk call assert!(code.contains("module.memory.init_data(0,")); @@ -611,7 +606,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; let info = ModuleInfo { diff --git a/crates/herkos-core/src/codegen/module.rs b/crates/herkos-core/src/codegen/module.rs index bcf30dc..6b4a72c 100644 --- a/crates/herkos-core/src/codegen/module.rs +++ b/crates/herkos-core/src/codegen/module.rs @@ -5,10 +5,9 @@ use crate::backend::Backend; use crate::codegen::constructor::{emit_const_globals, generate_constructor, rust_code_preamble}; +use crate::codegen::env::generate_env_block; use crate::codegen::export::generate_export_impl; use crate::codegen::function::generate_function_with_info; -use crate::codegen::traits::generate_host_traits; -use crate::codegen::types::wasm_type_to_rust; use crate::ir::*; use anyhow::{Context, Result}; @@ -49,26 +48,15 @@ fn generate_wrapper_module(backend: &B, info: &ModuleInfo) -> Result rust_code.push('\n'); } - // Host trait definitions - rust_code.push_str(&generate_host_traits(backend, info)); - - // Globals struct (mutable globals only) - if has_mut_globals { - rust_code.push_str("pub struct Globals {\n"); - for (idx, g) in info.globals.iter().enumerate() { - if g.mutable { - let rust_ty = wasm_type_to_rust(&g.init_value.ty()); - rust_code.push_str(&format!(" pub g{idx}: {rust_ty},\n")); - } - } - rust_code.push_str("}\n\n"); - } + // Environment block: ModuleHostTrait, NoHost impl, Globals struct, Env struct + rust_code.push_str(&generate_env_block(info)); // Const items for immutable globals rust_code.push_str(&emit_const_globals(backend, info)); // Newtype wrapper struct (required to allow `impl WasmModule` on a foreign type) - let globals_type = if has_mut_globals { "Globals" } else { "()" }; + // Always use Globals for the type (it may be empty but is always generated) + let globals_type = "Globals"; let table_size_str = if info.has_table() { "TABLE_MAX" } else { "0" }; if info.has_memory { rust_code.push_str(&format!( diff --git a/crates/herkos-core/src/codegen/utils.rs b/crates/herkos-core/src/codegen/utils.rs index a5a8a47..e1cc76b 100644 --- a/crates/herkos-core/src/codegen/utils.rs +++ b/crates/herkos-core/src/codegen/utils.rs @@ -1,19 +1,16 @@ //! General-purpose utility functions for code generation. -/// Build a call args vector by conditionally adding globals/memory/table/host. +/// Build a call args vector by conditionally adding memory and table. +/// +/// Note: Globals are now part of the env parameter (always first after wasm args). pub fn build_inner_call_args( base_args: &[String], - has_globals: bool, - globals_expr: &str, has_memory: bool, memory_expr: &str, has_table: bool, table_expr: &str, ) -> Vec { let mut call_args = base_args.to_vec(); - if has_globals { - call_args.push(globals_expr.to_string()); - } if has_memory { call_args.push(memory_expr.to_string()); } diff --git a/crates/herkos-core/src/ir/builder/assembly.rs b/crates/herkos-core/src/ir/builder/assembly.rs index fa7849a..52bd859 100644 --- a/crates/herkos-core/src/ir/builder/assembly.rs +++ b/crates/herkos-core/src/ir/builder/assembly.rs @@ -28,50 +28,10 @@ pub(super) fn assemble_module_metadata( let type_signatures = build_call_indirect_signatures(parsed); let func_imports = build_function_imports(parsed); - // Enrich IR functions with signature metadata (type_idx and needs_host) - enrich_ir_functions( - parsed, - &canonical_type, - &mut ir_functions, - &imported_globals, - )?; - - // Propagate needs_host transitively through direct function calls. - // If function A calls function B and B.needs_host is true, then A.needs_host must also be true - // (because A's signature must have `host` to pass it to B). - // This is a fixpoint algorithm that converges when no more functions are marked. - let mut changed = true; - while changed { - changed = false; - // Snapshot current needs_host state to avoid borrow checker issues - let needs_host_snapshot: Vec = ir_functions.iter().map(|f| f.needs_host).collect(); - let mut to_mark: Vec = Vec::new(); - - for (func_idx, ir_func) in ir_functions.iter().enumerate() { - if ir_func.needs_host { - continue; // Already marked - } - - // Check if this function directly calls any function that needs host - let calls_host_needing = ir_func.blocks.iter().any(|block| { - block.instructions.iter().any(|instr| { - matches!(instr, IrInstr::Call { func_idx: callee, .. } - if needs_host_snapshot - .get(callee.as_usize()) - .copied() - .unwrap_or(false)) - }) - }); - - if calls_host_needing { - to_mark.push(func_idx); - } - } - - // Apply all changes from this iteration - for idx in to_mark { - ir_functions[idx].needs_host = true; - changed = true; + // Set type_idx for all IR functions + for (func_idx, func) in parsed.functions.iter().enumerate() { + if let Some(ir_func) = ir_functions.get_mut(func_idx) { + ir_func.type_idx = TypeIdx::new(canonical_type[func.type_idx as usize]); } } @@ -179,63 +139,6 @@ fn build_function_exports(parsed: &ParsedModule, num_imported_functions: usize) .collect() } -/// Enriches IR functions with signature metadata (type_idx and needs_host). -/// -/// This iterates through the parsed functions and sets the type_idx and needs_host -/// fields in the corresponding IR functions. -fn enrich_ir_functions( - parsed: &ParsedModule, - canonical_type: &[usize], - ir_functions: &mut [IrFunction], - imported_globals: &[ImportedGlobalDef], -) -> Result<()> { - let num_imported_globals = imported_globals.len(); - let has_func_imports = parsed - .imports - .iter() - .any(|i| matches!(i.kind, ImportKind::Function(_))); - for (func_idx, func) in parsed.functions.iter().enumerate() { - if let Some(ir_func) = ir_functions.get_mut(func_idx) { - ir_func.type_idx = TypeIdx::new(canonical_type[func.type_idx as usize]); - ir_func.needs_host = - function_calls_imports(ir_func, num_imported_globals, has_func_imports); - } else { - return Err(anyhow::anyhow!( - "IR function missing for parsed function index {}", - func_idx - )); - } - } - Ok(()) -} - -/// Determines if a function calls imports or accesses imported globals. -/// -/// Returns true if the function: -/// - Has a direct CallImport instruction, OR -/// - Accesses an imported global, OR -/// - Uses CallIndirect when the module has any function imports -/// (because call_indirect may dispatch to functions that need the host parameter) -fn function_calls_imports( - ir_func: &IrFunction, - num_imported_globals: usize, - has_func_imports: bool, -) -> bool { - ir_func.blocks.iter().any(|block| { - block.instructions.iter().any(|instr| { - matches!(instr, IrInstr::CallImport { .. }) - || (num_imported_globals > 0 - && matches!( - instr, - IrInstr::GlobalGet { index, .. } - | IrInstr::GlobalSet { index, .. } - if index.as_usize() < num_imported_globals - )) - || (has_func_imports && matches!(instr, IrInstr::CallIndirect { .. })) - }) - }) -} - /// Builds type signatures for call_indirect type checking. fn build_call_indirect_signatures(parsed: &ParsedModule) -> Vec { parsed @@ -255,7 +158,6 @@ fn build_call_indirect_signatures(parsed: &ParsedModule) -> Vec { params, return_type, type_idx: TypeIdx::new(0), - needs_host: false, } }) .collect() diff --git a/crates/herkos-core/src/ir/builder/core.rs b/crates/herkos-core/src/ir/builder/core.rs index 490bf2b..19da37a 100644 --- a/crates/herkos-core/src/ir/builder/core.rs +++ b/crates/herkos-core/src/ir/builder/core.rs @@ -619,7 +619,6 @@ impl IrBuilder { entry_block: entry, return_type, type_idx: TypeIdx::new(0), // Set by enrich_ir_functions during assembly - needs_host: false, // Set by enrich_ir_functions during assembly }) } diff --git a/crates/herkos-core/src/ir/lower_phis.rs b/crates/herkos-core/src/ir/lower_phis.rs index fcd7b8e..9d04bcd 100644 --- a/crates/herkos-core/src/ir/lower_phis.rs +++ b/crates/herkos-core/src/ir/lower_phis.rs @@ -304,7 +304,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, }], wasm_version: 1, } diff --git a/crates/herkos-core/src/ir/types.rs b/crates/herkos-core/src/ir/types.rs index d5b75c4..08fef80 100644 --- a/crates/herkos-core/src/ir/types.rs +++ b/crates/herkos-core/src/ir/types.rs @@ -203,9 +203,6 @@ pub struct IrFunction { /// Index into the Wasm type section (needed for call_indirect dispatch). pub type_idx: TypeIdx, - - /// Whether this function calls imported functions or accesses imported globals (needs host parameter). - pub needs_host: bool, } /// A basic block — sequence of instructions with a single entry and exit. @@ -880,8 +877,6 @@ pub struct FuncSignature { /// Index into the Wasm type section (needed for call_indirect dispatch). /// Note: This field is currently always set to 0 and not used in codegen. pub type_idx: TypeIdx, - /// Whether this function calls imported functions (needs host parameter). - pub needs_host: bool, } /// An element segment to initialize a table. @@ -1513,7 +1508,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, }; assert!(!has_import_calls(&ir_func_no_imports)); @@ -1541,7 +1535,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: true, }; assert!(has_import_calls(&ir_func_with_imports)); } @@ -1562,7 +1555,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, }; // No imported globals @@ -1592,7 +1584,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: true, }; assert!(has_global_import_access(&ir_func_with_global_get, 2)); } @@ -1613,7 +1604,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: true, }; assert!(has_global_import_access(&ir_func, 2)); diff --git a/crates/herkos-core/src/optimizer/dead_blocks.rs b/crates/herkos-core/src/optimizer/dead_blocks.rs index 8db587d..bbe6b01 100644 --- a/crates/herkos-core/src/optimizer/dead_blocks.rs +++ b/crates/herkos-core/src/optimizer/dead_blocks.rs @@ -73,7 +73,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, } } @@ -409,7 +408,6 @@ mod tests { entry_block: BlockId(0), return_type: Some(WasmType::I32), type_idx: TypeIdx::new(0), - needs_host: false, }; eliminate(&mut func).unwrap(); assert_eq!(block_ids(&func), vec![0, 1]); diff --git a/crates/herkos-core/src/optimizer/mod.rs b/crates/herkos-core/src/optimizer/mod.rs index ae69a9e..654382f 100644 --- a/crates/herkos-core/src/optimizer/mod.rs +++ b/crates/herkos-core/src/optimizer/mod.rs @@ -53,7 +53,6 @@ mod tests { entry_block: BlockId(0), return_type: None, type_idx: TypeIdx::new(0), - needs_host: false, }; let module = ModuleInfo { diff --git a/crates/herkos-runtime/src/lib.rs b/crates/herkos-runtime/src/lib.rs index 02e722b..843b99c 100644 --- a/crates/herkos-runtime/src/lib.rs +++ b/crates/herkos-runtime/src/lib.rs @@ -51,6 +51,13 @@ pub enum WasmTrap { /// Result type for Wasm operations — `Result`. pub type WasmResult = Result; +/// Sentinel type for modules with no host imports. +/// +/// Zero-sized — the compiler eliminates it entirely. Used as the generic parameter `H` +/// in `Env` for modules that have no host imports. +#[derive(Clone, Copy)] +pub struct NoHost; + /// Errors that occur during module/memory/table construction. /// /// These are programming errors in the transpiler, not runtime Wasm traps. diff --git a/crates/herkos-tests/tests/call_import_transitive.rs b/crates/herkos-tests/tests/call_import_transitive.rs index 28cb7ac..8d46bca 100644 --- a/crates/herkos-tests/tests/call_import_transitive.rs +++ b/crates/herkos-tests/tests/call_import_transitive.rs @@ -18,7 +18,7 @@ impl MockHost { } // Implement the generated EnvImports trait -impl call_import_transitive::EnvImports for MockHost { +impl call_import_transitive::ModuleHostTrait for MockHost { fn log(&mut self, value: i32) -> WasmResult<()> { self.last_logged = Some(value); Ok(()) diff --git a/crates/herkos-tests/tests/import_memory.rs b/crates/herkos-tests/tests/import_memory.rs index 507957e..f35bfdf 100644 --- a/crates/herkos-tests/tests/import_memory.rs +++ b/crates/herkos-tests/tests/import_memory.rs @@ -20,8 +20,8 @@ impl MemoryProvidingHost { } } -// Implement the EnvImports trait for the host -impl import_memory::EnvImports for MemoryProvidingHost { +// Implement the ModuleHostTrait for the host +impl import_memory::ModuleHostTrait for MemoryProvidingHost { fn print_i32(&mut self, value: i32) -> WasmResult<()> { self.last_logged = Some(value); Ok(()) @@ -31,7 +31,7 @@ impl import_memory::EnvImports for MemoryProvidingHost { #[test] fn test_library_module_memory_lending() { // Create a host with memory that will be lent to the module - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); // Create LibraryModule - it doesn't own memory, borrows from host @@ -43,18 +43,18 @@ fn test_library_module_memory_lending() { memory.store_i32(8, 200).unwrap(); // Module reads from the borrowed memory - let value = module.read_at(0, &mut memory).unwrap(); + let value = module.read_at(0, &mut memory, &mut host).unwrap(); assert_eq!(value, 42, "Should read value written by host"); } #[test] fn test_library_module_write_to_borrowed_memory() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut module = import_memory::new().unwrap(); // Module writes to the borrowed memory - module.write_at(0, 123, &mut memory).unwrap(); + module.write_at(0, 123, &mut memory, &mut host).unwrap(); // Host reads back what module wrote let value = memory.load_i32(0).unwrap(); @@ -66,7 +66,7 @@ fn test_library_module_write_to_borrowed_memory() { #[test] fn test_library_module_roundtrip() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut module = import_memory::new().unwrap(); @@ -76,11 +76,13 @@ fn test_library_module_roundtrip() { memory.store_i32(0, test_value).unwrap(); // Module reads the value - let read_value = module.read_at(0, &mut memory).unwrap(); + let read_value = module.read_at(0, &mut memory, &mut host).unwrap(); assert_eq!(read_value, test_value); // Module writes different value - module.write_at(4, test_value + 1, &mut memory).unwrap(); + module + .write_at(4, test_value + 1, &mut memory, &mut host) + .unwrap(); // Host reads back what module wrote let stored_value = memory.load_i32(4).unwrap(); @@ -89,7 +91,7 @@ fn test_library_module_roundtrip() { #[test] fn test_library_module_multiple_offsets() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut module = import_memory::new().unwrap(); @@ -104,7 +106,7 @@ fn test_library_module_multiple_offsets() { // Module reads values back for (i, &expected) in values.iter().enumerate() { let offset = (i * 4) as i32; - let value = module.read_at(offset, &mut memory).unwrap(); + let value = module.read_at(offset, &mut memory, &mut host).unwrap(); assert_eq!(value, expected, "Value at offset {}", offset); } } @@ -134,33 +136,33 @@ fn test_library_module_imports_and_memory() { #[test] fn test_memory_size_with_import() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut module = import_memory::new().unwrap(); // memory.size should return current page count (2 in this case) - let size = module.memory_size(&mut memory).unwrap(); + let size = module.memory_size(&mut memory, &mut host).unwrap(); assert_eq!(size, 2, "memory.size should return current page count"); } #[test] fn test_memory_grow_borrowed() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut module = import_memory::new().unwrap(); // Grow memory by 1 page - let prev_size = module.try_grow(1, &mut memory).unwrap(); + let prev_size = module.try_grow(1, &mut memory, &mut host).unwrap(); assert_eq!(prev_size, 2, "Should return previous size"); // Verify new size - let new_size = module.memory_size(&mut memory).unwrap(); + let new_size = module.memory_size(&mut memory, &mut host).unwrap(); assert_eq!(new_size, 3, "Size should increase after grow"); } #[test] fn test_memory_isolation_different_modules() { - let _host = MemoryProvidingHost::new(); + let mut host = MemoryProvidingHost::new(); // Module 1: borrows this memory let mut memory1 = Box::new(IsolatedMemory::<4>::try_new(1).unwrap()); @@ -177,11 +179,11 @@ fn test_memory_isolation_different_modules() { memory2.store_i32(0, 222).unwrap(); // Module 1 reads its own memory - let val1 = module1.read_at(0, &mut memory1).unwrap(); + let val1 = module1.read_at(0, &mut memory1, &mut host).unwrap(); assert_eq!(val1, 111); // Module 2 reads its own memory (should be different) - let val2 = module2.read_at(0, &mut memory2).unwrap(); + let val2 = module2.read_at(0, &mut memory2, &mut host).unwrap(); assert_eq!(val2, 222); // Verify isolation: memory1 and memory2 are separate diff --git a/crates/herkos-tests/tests/import_multi.rs b/crates/herkos-tests/tests/import_multi.rs index 9dc4dc9..6ae17a1 100644 --- a/crates/herkos-tests/tests/import_multi.rs +++ b/crates/herkos-tests/tests/import_multi.rs @@ -29,8 +29,8 @@ impl MultiImportHost { } } -// Implement EnvImports trait -impl import_multi::EnvImports for MultiImportHost { +// Implement ModuleHostTrait with all import methods +impl import_multi::ModuleHostTrait for MultiImportHost { fn add(&mut self, a: i32, b: i32) -> WasmResult { self.add_calls += 1; Ok(a + b) @@ -45,10 +45,7 @@ impl import_multi::EnvImports for MultiImportHost { self.log_value = Some(value); Ok(()) } -} -// Implement WasiSnapshotPreview1Imports trait -impl import_multi::WasiSnapshotPreview1Imports for MultiImportHost { fn fd_write(&mut self, _fd: i32, _iov: i32, _iovlen: i32, _nwritten: i32) -> WasmResult { Ok(self.fd_write_result) } @@ -118,12 +115,13 @@ fn test_call_all_imports() { #[test] fn test_call_local_functions_only() { + let mut host = MultiImportHost::new(); let mut module = import_multi::new().unwrap(); // call_local_only(2, 3) should: // 1. Call local add(2, 3) = 5 // 2. Call local mul(5, 3) = 15 - let result = module.call_local_only(2, 3).unwrap(); + let result = module.call_local_only(2, 3, &mut host).unwrap(); assert_eq!(result, 15); } @@ -146,14 +144,14 @@ fn test_counter_management() { let mut module = import_multi::new().unwrap(); // Get counter (should start at 0) - let counter = module.get_counter().unwrap(); + let counter = module.get_counter(&mut host).unwrap(); assert_eq!(counter, 0); // Call mixed_calls which increments counter let _ = module.mixed_calls(1, 1, &mut host).unwrap(); // Check counter was incremented - let counter = module.get_counter().unwrap(); + let counter = module.get_counter(&mut host).unwrap(); assert_eq!(counter, 1); } @@ -210,7 +208,7 @@ fn test_multiple_hosts_with_different_implementations() { struct Host1; struct Host2; - impl import_multi::EnvImports for Host1 { + impl import_multi::ModuleHostTrait for Host1 { fn add(&mut self, a: i32, b: i32) -> WasmResult { Ok(a + b) } @@ -220,9 +218,6 @@ fn test_multiple_hosts_with_different_implementations() { fn log(&mut self, _value: i32) -> WasmResult<()> { Ok(()) } - } - - impl import_multi::WasiSnapshotPreview1Imports for Host1 { fn fd_write( &mut self, _fd: i32, @@ -234,7 +229,7 @@ fn test_multiple_hosts_with_different_implementations() { } } - impl import_multi::EnvImports for Host2 { + impl import_multi::ModuleHostTrait for Host2 { fn add(&mut self, a: i32, b: i32) -> WasmResult { Ok(a - b) // Different implementation } @@ -244,9 +239,6 @@ fn test_multiple_hosts_with_different_implementations() { fn log(&mut self, _value: i32) -> WasmResult<()> { Ok(()) } - } - - impl import_multi::WasiSnapshotPreview1Imports for Host2 { fn fd_write( &mut self, _fd: i32, diff --git a/crates/herkos-tests/tests/import_traits.rs b/crates/herkos-tests/tests/import_traits.rs index 52887a5..bec9f99 100644 --- a/crates/herkos-tests/tests/import_traits.rs +++ b/crates/herkos-tests/tests/import_traits.rs @@ -26,8 +26,8 @@ impl MockHost { } } -// Implement the EnvImports trait that was generated -impl import_basic::EnvImports for MockHost { +// Implement the ModuleHostTrait with all import methods +impl import_basic::ModuleHostTrait for MockHost { fn print_i32(&mut self, arg0: i32) -> WasmResult<()> { self.last_printed = Some(arg0); Ok(()) @@ -36,10 +36,7 @@ impl import_basic::EnvImports for MockHost { fn read_i32(&mut self) -> WasmResult { Ok(self.read_value) } -} -// Implement the WasiSnapshotPreview1Imports trait that was generated -impl import_basic::WasiSnapshotPreview1Imports for MockHost { fn fd_write(&mut self, _arg0: i32, _arg1: i32, _arg2: i32, _arg3: i32) -> WasmResult { Ok(self.fd_write_result) } @@ -52,7 +49,7 @@ fn test_trait_generation() { let mut module = import_basic::new().unwrap(); // Counter should start at 0 - assert_eq!(module.get_counter().unwrap(), 0); + assert_eq!(module.get_counter(&mut host).unwrap(), 0); // Call exported function that uses imports let result = module.test_imports(100, &mut host).unwrap(); @@ -68,7 +65,7 @@ fn test_trait_generation() { assert_eq!(result, 52, "Should return read_i32() + 10"); // Counter should have been incremented - assert_eq!(module.get_counter().unwrap(), 1); + assert_eq!(module.get_counter(&mut host).unwrap(), 1); } #[test] @@ -88,7 +85,7 @@ fn test_multiple_trait_bounds() { // This test verifies that a host implementing multiple traits can be used struct MultiHost; - impl import_basic::EnvImports for MultiHost { + impl import_basic::ModuleHostTrait for MultiHost { fn print_i32(&mut self, _arg0: i32) -> WasmResult<()> { Ok(()) } @@ -96,9 +93,7 @@ fn test_multiple_trait_bounds() { fn read_i32(&mut self) -> WasmResult { Ok(99) } - } - impl import_basic::WasiSnapshotPreview1Imports for MultiHost { fn fd_write(&mut self, _arg0: i32, _arg1: i32, _arg2: i32, _arg3: i32) -> WasmResult { Ok(77) } diff --git a/crates/herkos-tests/tests/indirect_call_import.rs b/crates/herkos-tests/tests/indirect_call_import.rs index 73ed717..51bc1e0 100644 --- a/crates/herkos-tests/tests/indirect_call_import.rs +++ b/crates/herkos-tests/tests/indirect_call_import.rs @@ -17,8 +17,8 @@ impl MockHost { } } -// Implement the generated EnvImports trait -impl indirect_call_import::EnvImports for MockHost { +// Implement the generated ModuleHostTrait +impl indirect_call_import::ModuleHostTrait for MockHost { fn log(&mut self, value: i32) -> WasmResult<()> { self.last_logged = Some(value); Ok(()) diff --git a/crates/herkos-tests/tests/inter_module_lending.rs b/crates/herkos-tests/tests/inter_module_lending.rs index 7b41a83..84fdc45 100644 --- a/crates/herkos-tests/tests/inter_module_lending.rs +++ b/crates/herkos-tests/tests/inter_module_lending.rs @@ -32,7 +32,7 @@ impl InterModuleHost { // -- import_memory traits -- -impl import_memory::EnvImports for InterModuleHost { +impl import_memory::ModuleHostTrait for InterModuleHost { fn print_i32(&mut self, value: i32) -> WasmResult<()> { self.logged_values.push(value); Ok(()) @@ -41,7 +41,7 @@ impl import_memory::EnvImports for InterModuleHost { // -- import_basic traits -- -impl import_basic::EnvImports for InterModuleHost { +impl import_basic::ModuleHostTrait for InterModuleHost { fn print_i32(&mut self, value: i32) -> WasmResult<()> { self.logged_values.push(value); Ok(()) @@ -50,9 +50,7 @@ impl import_basic::EnvImports for InterModuleHost { fn read_i32(&mut self) -> WasmResult { Ok(self.read_value) } -} -impl import_basic::WasiSnapshotPreview1Imports for InterModuleHost { fn fd_write(&mut self, _: i32, _: i32, _: i32, _: i32) -> WasmResult { Ok(0) } @@ -60,7 +58,7 @@ impl import_basic::WasiSnapshotPreview1Imports for InterModuleHost { // -- import_multi traits -- -impl import_multi::EnvImports for InterModuleHost { +impl import_multi::ModuleHostTrait for InterModuleHost { fn add(&mut self, a: i32, b: i32) -> WasmResult { self.add_call_count += 1; Ok(a + b) @@ -74,9 +72,7 @@ impl import_multi::EnvImports for InterModuleHost { self.logged_values.push(value); Ok(()) } -} -impl import_multi::WasiSnapshotPreview1Imports for InterModuleHost { fn fd_write(&mut self, _: i32, _: i32, _: i32, _: i32) -> WasmResult { Ok(0) } @@ -95,19 +91,21 @@ fn test_host_writes_library_reads() { memory.store_i32(8, 300).unwrap(); // Library borrows memory and reads back - assert_eq!(lib.read_at(0, &mut memory).unwrap(), 100); - assert_eq!(lib.read_at(4, &mut memory).unwrap(), 200); - assert_eq!(lib.read_at(8, &mut memory).unwrap(), 300); + let mut host = InterModuleHost::new(); + assert_eq!(lib.read_at(0, &mut memory, &mut host).unwrap(), 100); + assert_eq!(lib.read_at(4, &mut memory, &mut host).unwrap(), 200); + assert_eq!(lib.read_at(8, &mut memory, &mut host).unwrap(), 300); } #[test] fn test_library_writes_host_reads() { let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut lib = import_memory::new().unwrap(); + let mut host = InterModuleHost::new(); // Library writes via borrowed memory - lib.write_at(0, 999, &mut memory).unwrap(); - lib.write_at(4, 888, &mut memory).unwrap(); + lib.write_at(0, 999, &mut memory, &mut host).unwrap(); + lib.write_at(4, 888, &mut memory, &mut host).unwrap(); // Host reads directly from its own memory assert_eq!(memory.load_i32(0).unwrap(), 999); @@ -118,16 +116,17 @@ fn test_library_writes_host_reads() { fn test_roundtrip_through_library() { let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut lib = import_memory::new().unwrap(); + let mut host = InterModuleHost::new(); // Host writes initial data memory.store_i32(0, 50).unwrap(); // Library reads the value - let val = lib.read_at(0, &mut memory).unwrap(); + let val = lib.read_at(0, &mut memory, &mut host).unwrap(); assert_eq!(val, 50); // Library writes a transformed value at a different offset - lib.write_at(4, val * 3, &mut memory).unwrap(); + lib.write_at(4, val * 3, &mut memory, &mut host).unwrap(); // Host reads the transformed value assert_eq!(memory.load_i32(4).unwrap(), 150); @@ -158,20 +157,21 @@ fn test_two_libraries_same_memory() { let mut memory = Box::new(IsolatedMemory::<4>::try_new(2).unwrap()); let mut lib_a = import_memory::new().unwrap(); let mut lib_b = import_memory::new().unwrap(); + let mut host = InterModuleHost::new(); // Library A writes to the shared memory - lib_a.write_at(0, 111, &mut memory).unwrap(); - lib_a.write_at(4, 222, &mut memory).unwrap(); + lib_a.write_at(0, 111, &mut memory, &mut host).unwrap(); + lib_a.write_at(4, 222, &mut memory, &mut host).unwrap(); // Library B reads from the same memory — should see A's writes - assert_eq!(lib_b.read_at(0, &mut memory).unwrap(), 111); - assert_eq!(lib_b.read_at(4, &mut memory).unwrap(), 222); + assert_eq!(lib_b.read_at(0, &mut memory, &mut host).unwrap(), 111); + assert_eq!(lib_b.read_at(4, &mut memory, &mut host).unwrap(), 222); // Library B overwrites - lib_b.write_at(0, 333, &mut memory).unwrap(); + lib_b.write_at(0, 333, &mut memory, &mut host).unwrap(); // Library A sees B's change - assert_eq!(lib_a.read_at(0, &mut memory).unwrap(), 333); + assert_eq!(lib_a.read_at(0, &mut memory, &mut host).unwrap(), 333); } #[test] @@ -194,7 +194,7 @@ fn test_multiple_module_types_shared_host() { assert_eq!(host.add_call_count, 1); // Use import_multi: call_local_only uses no imports at all - let result = multi_mod.call_local_only(2, 5).unwrap(); + let result = multi_mod.call_local_only(2, 5, &mut host).unwrap(); // local_add(2,5)=7, local_mul(7,3)=21 assert_eq!(result, 21); } @@ -204,18 +204,19 @@ fn test_memory_grow_visible_across_modules() { let mut memory = Box::new(IsolatedMemory::<4>::try_new(1).unwrap()); let mut lib_a = import_memory::new().unwrap(); let mut lib_b = import_memory::new().unwrap(); + let mut host = InterModuleHost::new(); // Initial size: 1 page - assert_eq!(lib_a.memory_size(&mut memory).unwrap(), 1); + assert_eq!(lib_a.memory_size(&mut memory, &mut host).unwrap(), 1); // Library A grows memory by 1 page - let prev = lib_a.try_grow(1, &mut memory).unwrap(); + let prev = lib_a.try_grow(1, &mut memory, &mut host).unwrap(); assert_eq!(prev, 1, "previous size should be 1"); // Library B sees the new size - assert_eq!(lib_b.memory_size(&mut memory).unwrap(), 2); + assert_eq!(lib_b.memory_size(&mut memory, &mut host).unwrap(), 2); // Library B can write to the grown region (page 1 = offset 65536) - lib_b.write_at(65536, 42, &mut memory).unwrap(); - assert_eq!(lib_a.read_at(65536, &mut memory).unwrap(), 42); + lib_b.write_at(65536, 42, &mut memory, &mut host).unwrap(); + assert_eq!(lib_a.read_at(65536, &mut memory, &mut host).unwrap(), 42); } diff --git a/crates/herkos/tests/e2e.rs b/crates/herkos/tests/e2e.rs index 87b2f28..2ddfd22 100644 --- a/crates/herkos/tests/e2e.rs +++ b/crates/herkos/tests/e2e.rs @@ -539,8 +539,8 @@ fn test_module_with_mutable_global() -> Result<()> { assert!(rust_code.contains("pub fn new() -> WasmResult")); assert!(rust_code.contains("Globals { g0: 0i32 }")); // Internal functions should be private - assert!(rust_code.contains("fn func_0(")); - assert!(!rust_code.contains("pub fn func_0(")); + assert!(rust_code.contains("fn func_0<") || rust_code.contains("fn func_0(")); + assert!(!rust_code.contains("pub fn func_0<") && !rust_code.contains("pub fn func_0(")); // Export methods assert!(rust_code.contains("impl WasmModule")); assert!(rust_code.contains("pub fn increment(&mut self) -> WasmResult")); @@ -568,10 +568,10 @@ fn test_module_with_data_segment() -> Result<()> { println!("Generated Rust code:\n{}", rust_code); // Should generate module wrapper (data segment triggers it) - assert!(rust_code.contains("pub struct WasmModule(pub Module<(), MAX_PAGES, 0>)")); + assert!(rust_code.contains("pub struct WasmModule(pub Module)")); assert!(rust_code.contains("pub fn new() -> WasmResult")); assert!(rust_code.contains( - "Module::try_init(&mut __slot, 1, (), Table::try_new(0)?).map_err(|_| WasmTrap::OutOfBounds)?" + "Module::try_init(&mut __slot, 1, Globals {}, Table::try_new(0)?).map_err(|_| WasmTrap::OutOfBounds)?" )); // Data segment initialization — bulk call assert!(rust_code.contains("module.memory.init_data(0,")); @@ -987,9 +987,10 @@ fn test_module_with_globals_and_memory() -> Result<()> { // Constructor initializes both assert!(rust_code.contains("Globals { g0: 100i32 }")); assert!(rust_code.contains("module.memory.init_data(")); - // Function gets both globals and memory params - assert!(rust_code.contains("globals: &mut Globals")); + // Function gets env and memory params (globals is in env) + assert!(rust_code.contains("env: &mut Env")); assert!(rust_code.contains("memory: &mut IsolatedMemory")); + assert!(rust_code.contains("pub globals: &")); // Export forwards both assert!(rust_code.contains("&mut self.0.globals")); assert!(rust_code.contains("&mut self.0.memory")); diff --git a/examples/inter-module-lending/src/main.rs b/examples/inter-module-lending/src/main.rs index 3779719..a274092 100644 --- a/examples/inter-module-lending/src/main.rs +++ b/examples/inter-module-lending/src/main.rs @@ -28,7 +28,7 @@ impl MathHost { } } -impl math_library_wasm::EnvImports for MathHost { +impl math_library_wasm::ModuleHostTrait for MathHost { fn log_result(&mut self, value: i32) -> WasmResult<()> { self.results_log.push(value); println!(" [library logged: {}]", value); @@ -53,14 +53,14 @@ fn main() { // Step 3: Library borrows memory to compute the sum let sum = library - .sum_array(0, data.len() as i32, &mut *memory) + .sum_array(0, data.len() as i32, &mut *memory, &mut host) .expect("sum_array trapped"); println!("Library computed sum: {}", sum); // Step 4: Library doubles all values in-place println!("\nLibrary doubles array in-place..."); library - .double_array(0, data.len() as i32, &mut *memory) + .double_array(0, data.len() as i32, &mut *memory, &mut host) .expect("double_array trapped"); // Step 5: Host reads back the modified values @@ -83,7 +83,7 @@ fn main() { // Step 7: Show memory info let pages = library - .memory_info(&mut *memory) + .memory_info(&mut *memory, &mut host) .expect("memory_info trapped"); println!("\nMemory size: {} pages ({} bytes)", pages, pages * 65536);