Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Execution functions use target's release optimization so proofs cover actual deployed code
- `OptLevel` is currently metadata only; optimization passes planned for future
- Add validation guards in `codegen()`: reject proof mode with non-Wasm32 targets, reject Soroban with non-det operations ([#97])
- Add function parameter lowering and function call support to WebAssembly codegen ([#136])
- Function parameters mapped to WASM local indices `0..n`; body locals start at `n`
- Pre-scan builds `func_name_to_idx` map for forward reference support
- `Expression::FunctionCall` lowered to `call` instruction with positional arguments
- Void function calls in expression-statement position correctly omit `Drop`
- Value-returning function calls in expression-statement position emit `Drop`
- Add local variable lowering (`let` bindings) to WebAssembly codegen ([#134])
- Emit `local.set` / `local.get` for variable definitions with literal, identifier, and uzumaki initializers
- Support all numeric types (i8, i16, i32, i64, u8, u16, u32, u64), bool, and uzumaki
Expand Down Expand Up @@ -304,3 +310,4 @@ Initial tagged release.
[#126]: https://github.com/Inferara/inference/pull/126
[#127]: https://github.com/Inferara/inference/pull/127
[#134]: https://github.com/Inferara/inference/pull/135
[#136]: https://github.com/Inferara/inference/pull/136
1 change: 1 addition & 0 deletions core/wasm-codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ inference-type-checker.workspace = true
anyhow.workspace = true
rustc-hash.workspace = true
cov-mark.workspace = true
thiserror.workspace = true
274 changes: 222 additions & 52 deletions core/wasm-codegen/src/compiler.rs

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions core/wasm-codegen/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use thiserror::Error;

/// Error returned when a function call expression cannot be lowered by the codegen pass.
///
/// This is an internal error type used by [`super::compiler::Compiler::lower_function_call`].
/// Callers convert it to a `todo!` or `panic!` depending on whether the case is planned
/// future work or indicates a type-checker inconsistency.
#[derive(Debug, Error)]
#[must_use = "errors must not be silently ignored"]
pub(crate) enum CodegenError {
/// The callee expression is not a plain identifier (e.g., method call, higher-order call).
/// This case is out of scope for the current implementation.
#[error("unsupported callee kind (only plain identifier calls are currently supported)")]
UnsupportedCalleeKind,
/// The function name was not found in the pre-built index map.
/// This should never happen if the type-checker ran successfully.
#[error(
"function '{0}' not found in module — the type-checker should have caught undefined functions"
)]
UnknownFunction(String),
}
7 changes: 6 additions & 1 deletion core/wasm-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use inference_type_checker::typed_context::TypedContext;
use crate::compiler::Compiler;

mod compiler;
mod errors;
pub mod output;
pub mod target;

Expand Down Expand Up @@ -150,7 +151,11 @@ pub fn codegen(
/// - Multi-file compilation is not fully tested (see `codegen` function)
fn traverse_t_ast_with_compiler(typed_context: &TypedContext, compiler: &mut Compiler) {
for source_file in &typed_context.source_files() {
for func_def in source_file.function_definitions() {
let func_defs = source_file.function_definitions();
// Pre-scan: build function name-to-index map so that forward references
// (callee defined after caller in source) resolve correctly at call sites.
compiler.build_func_name_to_idx(&func_defs);
for func_def in func_defs {
compiler.visit_function_definition(&func_def, typed_context);
}
}
Expand Down
198 changes: 198 additions & 0 deletions tests/src/codegen/wasm/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,166 @@ mod base_codegen_tests {
);
}

#[test]
fn fn_params_test() {
cov_mark::check_count!(wasm_codegen_emit_function_params, 7);
let test_name = "fn_params";
let test_file_path = get_test_file_path(module_path!(), test_name);
let source_code = std::fs::read_to_string(&test_file_path)
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
let actual = wasm_codegen(&source_code);
inf_wasmparser::validate(&actual)
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
let expected = get_test_wasm_path(module_path!(), test_name);
let expected = std::fs::read(&expected)
.unwrap_or_else(|_| panic!("Failed to read expected wasm file for test: {test_name}"));
assert_wasms_modules_equivalence(&expected, &actual);
}

#[test]
fn fn_params_execution_test() {
use wasmtime::{Engine, Module, Store, TypedFunc};

let test_name = "fn_params";
let test_file_path = get_test_file_path(module_path!(), test_name);
let source_code = std::fs::read_to_string(&test_file_path)
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
let wasm_bytes = wasm_codegen(&source_code);

let engine = Engine::default();
let module = Module::new(&engine, &wasm_bytes)
.unwrap_or_else(|e| panic!("Failed to create Wasm module: {e}"));

let mut store = Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[])
.unwrap_or_else(|e| panic!("Failed to instantiate Wasm module: {e}"));

let identity_i32: TypedFunc<i32, i32> = instance
.get_typed_func(&mut store, "identity_i32")
.unwrap_or_else(|e| panic!("Failed to get 'identity_i32': {e}"));
assert_eq!(
identity_i32.call(&mut store, 42).unwrap_or_else(|e| panic!("Call failed: {e}")),
42
);

let identity_i64: TypedFunc<i64, i64> = instance
.get_typed_func(&mut store, "identity_i64")
.unwrap_or_else(|e| panic!("Failed to get 'identity_i64': {e}"));
assert_eq!(
identity_i64
.call(&mut store, -9223372036854775808_i64)
.unwrap_or_else(|e| panic!("Call failed: {e}")),
-9223372036854775808_i64
);

let identity_bool: TypedFunc<i32, i32> = instance
.get_typed_func(&mut store, "identity_bool")
.unwrap_or_else(|e| panic!("Failed to get 'identity_bool': {e}"));
assert_eq!(
identity_bool.call(&mut store, 1).unwrap_or_else(|e| panic!("Call failed: {e}")),
1
);

let first_of_two: TypedFunc<(i32, i32), i32> = instance
.get_typed_func(&mut store, "first_of_two")
.unwrap_or_else(|e| panic!("Failed to get 'first_of_two': {e}"));
assert_eq!(
first_of_two
.call(&mut store, (10, 20))
.unwrap_or_else(|e| panic!("Call failed: {e}")),
10
);

let second_of_two: TypedFunc<(i32, i32), i32> = instance
.get_typed_func(&mut store, "second_of_two")
.unwrap_or_else(|e| panic!("Failed to get 'second_of_two': {e}"));
assert_eq!(
second_of_two
.call(&mut store, (10, 20))
.unwrap_or_else(|e| panic!("Call failed: {e}")),
20
);
}

#[test]
fn fn_calls_test() {
cov_mark::check_count!(wasm_codegen_emit_function_call, 5);
let test_name = "fn_calls";
let test_file_path = get_test_file_path(module_path!(), test_name);
let source_code = std::fs::read_to_string(&test_file_path)
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
let actual = wasm_codegen(&source_code);
inf_wasmparser::validate(&actual)
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
let expected = get_test_wasm_path(module_path!(), test_name);
let expected = std::fs::read(&expected)
.unwrap_or_else(|_| panic!("Failed to read expected wasm file for test: {test_name}"));
assert_wasms_modules_equivalence(&expected, &actual);
}

#[test]
fn fn_calls_execution_test() {
use wasmtime::{Engine, Module, Store, TypedFunc};

let test_name = "fn_calls";
let test_file_path = get_test_file_path(module_path!(), test_name);
let source_code = std::fs::read_to_string(&test_file_path)
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
let wasm_bytes = wasm_codegen(&source_code);

let engine = Engine::default();
let module = Module::new(&engine, &wasm_bytes)
.unwrap_or_else(|e| panic!("Failed to create Wasm module: {e}"));

let mut store = Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[])
.unwrap_or_else(|e| panic!("Failed to instantiate Wasm module: {e}"));

let call_zero: TypedFunc<(), i32> = instance
.get_typed_func(&mut store, "call_zero")
.unwrap_or_else(|e| panic!("Failed to get 'call_zero': {e}"));
assert_eq!(
call_zero.call(&mut store, ()).unwrap_or_else(|e| panic!("Call failed: {e}")),
0
);

let call_identity: TypedFunc<i32, i32> = instance
.get_typed_func(&mut store, "call_identity")
.unwrap_or_else(|e| panic!("Failed to get 'call_identity': {e}"));
assert_eq!(
call_identity
.call(&mut store, 77)
.unwrap_or_else(|e| panic!("Call failed: {e}")),
77
);

let call_first: TypedFunc<(i32, i32), i32> = instance
.get_typed_func(&mut store, "call_first")
.unwrap_or_else(|e| panic!("Failed to get 'call_first': {e}"));
assert_eq!(
call_first
.call(&mut store, (10, 20))
.unwrap_or_else(|e| panic!("Call failed: {e}")),
10
);

let let_from_call: TypedFunc<(), i32> = instance
.get_typed_func(&mut store, "let_from_call")
.unwrap_or_else(|e| panic!("Failed to get 'let_from_call': {e}"));
assert_eq!(
let_from_call.call(&mut store, ()).unwrap_or_else(|e| panic!("Call failed: {e}")),
0
);

let forward_call: TypedFunc<(), i32> = instance
.get_typed_func(&mut store, "forward_call")
.unwrap_or_else(|e| panic!("Failed to get 'forward_call': {e}"));
assert_eq!(
forward_call.call(&mut store, ()).unwrap_or_else(|e| panic!("Call failed: {e}")),
99
);
}

#[test]
fn soroban_produces_valid_wasm() {
let source = "pub fn hello_world() -> i32 { return 42; }";
Expand Down Expand Up @@ -570,4 +730,42 @@ mod regenerate {
actual.len()
);
}

#[test]
#[ignore]
fn regenerate_fn_params_wasm() {
let dir = base_test_dir().join("fn_params");
let source_code =
std::fs::read_to_string(dir.join("fn_params.inf")).expect("Failed to read fn_params.inf");
let actual = wasm_codegen(&source_code);
inf_wasmparser::validate(&actual)
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
let wasm_path = dir.join("fn_params.wasm");
std::fs::write(&wasm_path, &actual)
.unwrap_or_else(|e| panic!("Failed to write {}: {e}", wasm_path.display()));
println!(
"Regenerated: {} ({} bytes)",
wasm_path.display(),
actual.len()
);
}

#[test]
#[ignore]
fn regenerate_fn_calls_wasm() {
let dir = base_test_dir().join("fn_calls");
let source_code =
std::fs::read_to_string(dir.join("fn_calls.inf")).expect("Failed to read fn_calls.inf");
let actual = wasm_codegen(&source_code);
inf_wasmparser::validate(&actual)
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
let wasm_path = dir.join("fn_calls.wasm");
std::fs::write(&wasm_path, &actual)
.unwrap_or_else(|e| panic!("Failed to write {}: {e}", wasm_path.display()));
println!(
"Regenerated: {} ({} bytes)",
wasm_path.display(),
actual.len()
);
}
}
Loading