use crate::frontend::ast::{Expr, ExprKind, Literal};
use crate::wasm::component::ComponentConfig;
use anyhow::Result;
use wasm_encoder::{
CodeSection, ExportKind, ExportSection, Function, FunctionSection, Instruction, Module,
TypeSection, ValType,
};
pub struct WasmCompiler {
optimization_level: u8,
config: ComponentConfig,
}
impl WasmCompiler {
pub fn new() -> Self {
Self {
optimization_level: 0,
config: ComponentConfig::default(),
}
}
pub fn set_optimization_level(&mut self, level: u8) {
self.optimization_level = level.min(3);
}
pub fn compile(&self, ast: &Expr) -> Result<WasmModule> {
let mut module = Module::new();
let mut exports = vec![];
let mut types = TypeSection::new();
let mut functions = FunctionSection::new();
let mut export_section = ExportSection::new();
let mut code = CodeSection::new();
match &ast.kind {
ExprKind::Function {
name, params, body, ..
} => {
let param_types: Vec<ValType> = params
.iter()
.map(|_| ValType::I32) .collect();
let result_types = vec![ValType::I32];
types.function(param_types, result_types);
functions.function(0);
let mut func = Function::new(vec![]);
Self::compile_expr(body, &mut func)?;
if !self.has_return(body) {
func.instruction(&Instruction::I32Const(0));
}
func.instruction(&Instruction::End);
code.function(&func);
export_section.export(name, ExportKind::Func, 0);
exports.push(name.clone());
}
ExprKind::Block(exprs) => {
for expr in exprs {
if let ExprKind::Function { name, .. } = &expr.kind {
exports.push(name.clone());
}
}
}
_ => {
types.function(vec![], vec![ValType::I32]);
functions.function(0);
let mut func = Function::new(vec![]);
Self::compile_expr(ast, &mut func)?;
func.instruction(&Instruction::End);
code.function(&func);
}
}
if !types.is_empty() {
module.section(&types);
}
if !functions.is_empty() {
module.section(&functions);
}
if !export_section.is_empty() {
module.section(&export_section);
}
if !code.is_empty() {
module.section(&code);
}
let bytes = module.finish();
Ok(WasmModule { bytes, exports })
}
fn compile_expr(expr: &Expr, func: &mut Function) -> Result<()> {
match &expr.kind {
ExprKind::Literal(lit) => match lit {
Literal::Integer(n, _) => {
func.instruction(&Instruction::I32Const(*n as i32));
}
Literal::Float(f) => {
func.instruction(&Instruction::F64Const(*f));
}
Literal::Bool(b) => {
func.instruction(&Instruction::I32Const(i32::from(*b)));
}
_ => {
func.instruction(&Instruction::I32Const(0));
}
},
ExprKind::Binary { left, op, right } => {
use crate::frontend::ast::BinaryOp;
Self::compile_expr(left, func)?;
Self::compile_expr(right, func)?;
match op {
BinaryOp::Add => func.instruction(&Instruction::I32Add),
BinaryOp::Subtract => func.instruction(&Instruction::I32Sub),
BinaryOp::Multiply => func.instruction(&Instruction::I32Mul),
BinaryOp::Divide => func.instruction(&Instruction::I32DivS),
_ => func.instruction(&Instruction::I32Add), };
}
_ => {
func.instruction(&Instruction::I32Const(0));
}
}
Ok(())
}
fn has_return(&self, expr: &Expr) -> bool {
matches!(expr.kind, ExprKind::Return { .. })
}
}
impl Default for WasmCompiler {
fn default() -> Self {
Self::new()
}
}
pub struct WasmModule {
bytes: Vec<u8>,
exports: Vec<String>,
}
impl WasmModule {
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn has_export(&self, name: &str) -> bool {
self.exports.contains(&name.to_string())
}
pub fn validate(&self) -> Result<()> {
if self.bytes.len() >= 4 && self.bytes[0..4] == [0x00, 0x61, 0x73, 0x6d] {
Ok(())
} else {
anyhow::bail!("Invalid WASM module")
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::frontend::ast::{BinaryOp, Span};
fn make_int(n: i64) -> Expr {
Expr {
kind: ExprKind::Literal(Literal::Integer(n, None)),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
}
}
fn make_float(f: f64) -> Expr {
Expr {
kind: ExprKind::Literal(Literal::Float(f)),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
}
}
fn make_bool(b: bool) -> Expr {
Expr {
kind: ExprKind::Literal(Literal::Bool(b)),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
}
}
fn make_binary(left: Expr, op: BinaryOp, right: Expr) -> Expr {
Expr {
kind: ExprKind::Binary {
left: Box::new(left),
op,
right: Box::new(right),
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
}
}
#[test]
fn test_compiler_new() {
let compiler = WasmCompiler::new();
assert_eq!(compiler.optimization_level, 0);
}
#[test]
fn test_compiler_default() {
let compiler = WasmCompiler::default();
assert_eq!(compiler.optimization_level, 0);
}
#[test]
fn test_set_optimization_level_valid() {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(2);
assert_eq!(compiler.optimization_level, 2);
}
#[test]
fn test_set_optimization_level_clamps_high() {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(10);
assert_eq!(
compiler.optimization_level, 3,
"Should clamp to maximum of 3"
);
}
#[test]
fn test_set_optimization_level_zero() {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(0);
assert_eq!(compiler.optimization_level, 0);
}
#[test]
fn test_compile_integer_literal() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile integer literal");
}
#[test]
fn test_compile_float_literal() {
let compiler = WasmCompiler::new();
let ast = make_float(3.15);
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile float literal");
}
#[test]
fn test_compile_bool_literal() {
let compiler = WasmCompiler::new();
let ast = make_bool(true);
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile bool literal");
}
#[test]
fn test_compile_binary_add() {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(2), BinaryOp::Add, make_int(3));
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile addition");
}
#[test]
fn test_compile_binary_subtract() {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(5), BinaryOp::Subtract, make_int(2));
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile subtraction");
}
#[test]
fn test_compile_binary_multiply() {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(3), BinaryOp::Multiply, make_int(4));
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile multiplication");
}
#[test]
fn test_compile_binary_divide() {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(10), BinaryOp::Divide, make_int(2));
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile division");
}
#[test]
fn test_has_return_false() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
assert!(
!compiler.has_return(&ast),
"Integer literal should not have return"
);
}
#[test]
fn test_has_return_true() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Return {
value: Some(Box::new(make_int(42))),
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
assert!(
compiler.has_return(&ast),
"Return expression should have return"
);
}
#[test]
fn test_module_bytes() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
let bytes = module.bytes();
assert!(!bytes.is_empty(), "Module should have bytecode");
}
#[test]
fn test_module_validate_valid() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
let result = module.validate();
assert!(result.is_ok(), "Valid WASM module should pass validation");
}
#[test]
fn test_module_validate_invalid() {
let module = WasmModule {
bytes: vec![0xFF, 0xFF, 0xFF, 0xFF],
exports: vec![],
};
let result = module.validate();
assert!(
result.is_err(),
"Invalid WASM module should fail validation"
);
}
#[test]
fn test_module_validate_empty() {
let module = WasmModule {
bytes: vec![],
exports: vec![],
};
let result = module.validate();
assert!(result.is_err(), "Empty module should fail validation");
}
#[test]
fn test_module_has_magic_number() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
let bytes = module.bytes();
assert!(bytes.len() >= 4, "Module should have at least 4 bytes");
assert_eq!(
&bytes[0..4],
&[0x00, 0x61, 0x73, 0x6d],
"Should have WASM magic number"
);
}
#[test]
fn test_module_has_export_false() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
assert!(
!module.has_export("nonexistent"),
"Should not have nonexistent export"
);
}
#[test]
fn test_compile_nested_arithmetic() {
let compiler = WasmCompiler::new();
let inner = make_binary(make_int(2), BinaryOp::Add, make_int(3));
let outer = make_binary(inner, BinaryOp::Multiply, make_int(4));
let result = compiler.compile(&outer);
assert!(result.is_ok(), "Should compile nested arithmetic");
let module = result.unwrap();
assert!(
module.validate().is_ok(),
"Nested arithmetic should produce valid WASM"
);
}
#[test]
fn test_compile_different_optimization_levels() {
let ast = make_binary(make_int(2), BinaryOp::Add, make_int(3));
for level in 0..=3 {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(level);
let result = compiler.compile(&ast);
assert!(
result.is_ok(),
"Should compile at optimization level {level}"
);
}
}
#[test]
fn test_compile_preserves_bytecode() {
let compiler = WasmCompiler::new();
let ast = make_int(123);
let module = compiler.compile(&ast).expect("Should compile");
let bytes1 = module.bytes();
let bytes2 = module.bytes();
assert_eq!(
bytes1, bytes2,
"Multiple calls to bytes() should return same data"
);
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(10000))]
#[test]
fn prop_compile_integer_never_panics(n in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_int(i64::from(n));
let _ = compiler.compile(&ast);
}
#[test]
fn prop_compile_float_never_panics(f in any::<f64>()) {
let compiler = WasmCompiler::new();
let ast = make_float(f);
let _ = compiler.compile(&ast);
}
#[test]
fn prop_compiled_modules_have_magic_number(n in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_int(i64::from(n));
if let Ok(module) = compiler.compile(&ast) {
let bytes = module.bytes();
prop_assert!(bytes.len() >= 4, "Module should have at least 4 bytes");
prop_assert_eq!(&bytes[0..4], &[0x00, 0x61, 0x73, 0x6d], "Should have WASM magic number");
}
}
#[test]
fn prop_compilation_is_deterministic(n in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_int(i64::from(n));
if let Ok(module1) = compiler.compile(&ast) {
if let Ok(module2) = compiler.compile(&ast) {
prop_assert_eq!(module1.bytes(), module2.bytes(), "Same AST should produce same bytecode");
}
}
}
#[test]
fn prop_optimization_level_clamped(level in any::<u8>()) {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(level);
prop_assert!(compiler.optimization_level <= 3, "Optimization level should be clamped to max 3");
}
#[test]
fn prop_valid_modules_pass_validation(n in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_int(i64::from(n));
if let Ok(module) = compiler.compile(&ast) {
prop_assert!(module.validate().is_ok(), "Valid module should pass validation");
}
}
#[test]
fn prop_binary_add_compiles_consistently(a in any::<i32>(), b in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(i64::from(a)), BinaryOp::Add, make_int(i64::from(b)));
if let Ok(module) = compiler.compile(&ast) {
prop_assert!(module.validate().is_ok(), "Binary operation should produce valid WASM");
}
}
#[test]
fn prop_bytes_call_idempotent(n in any::<i32>()) {
let compiler = WasmCompiler::new();
let ast = make_int(i64::from(n));
if let Ok(module) = compiler.compile(&ast) {
let bytes1 = module.bytes();
let bytes2 = module.bytes();
let bytes3 = module.bytes();
prop_assert_eq!(bytes1, bytes2);
prop_assert_eq!(bytes2, bytes3);
}
}
}
}
#[test]
fn test_compile_bool_false() {
let compiler = WasmCompiler::new();
let ast = make_bool(false);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_string_literal() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Literal(Literal::String("hello".to_string())),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_char_literal() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Literal(Literal::Char('x')),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_unit_literal() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Literal(Literal::Unit),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_negative_integer() {
let compiler = WasmCompiler::new();
let ast = make_int(-42);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_large_integer() {
let compiler = WasmCompiler::new();
let ast = make_int(i32::MAX as i64);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_small_integer() {
let compiler = WasmCompiler::new();
let ast = make_int(i32::MIN as i64);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_negative_float() {
let compiler = WasmCompiler::new();
let ast = make_float(-3.14);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_zero_float() {
let compiler = WasmCompiler::new();
let ast = make_float(0.0);
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_block_empty() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Block(vec![]),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_block_single_expr() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Block(vec![make_int(42)]),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_identifier() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Identifier("x".to_string()),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_module_has_export_empty_name() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
assert!(!module.has_export(""));
}
#[test]
fn test_set_optimization_level_all_valid() {
for level in 0..=3 {
let mut compiler = WasmCompiler::new();
compiler.set_optimization_level(level);
assert_eq!(compiler.optimization_level, level);
}
}
#[test]
fn test_compile_binary_default_op() {
let compiler = WasmCompiler::new();
let ast = make_binary(make_int(2), BinaryOp::Less, make_int(3));
let result = compiler.compile(&ast);
assert!(result.is_ok());
}
#[test]
fn test_compile_deeply_nested_binary() {
let compiler = WasmCompiler::new();
let add = make_binary(make_int(1), BinaryOp::Add, make_int(2));
let sub = make_binary(add, BinaryOp::Subtract, make_int(3));
let mul = make_binary(sub, BinaryOp::Multiply, make_int(4));
let div = make_binary(mul, BinaryOp::Divide, make_int(5));
let result = compiler.compile(&div);
assert!(result.is_ok());
}
#[test]
fn test_module_bytecode_length() {
let compiler = WasmCompiler::new();
let ast = make_int(42);
let module = compiler.compile(&ast).expect("Should compile");
let bytes = module.bytes();
assert!(bytes.len() >= 8, "WASM module should have header + section");
}
#[test]
fn test_compile_function_no_params() {
use crate::frontend::ast::Type as AstType;
use crate::frontend::ast::TypeKind;
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Function {
name: "add".to_string(),
type_params: vec![],
params: vec![],
body: Box::new(make_int(42)),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile function: {:?}", result.err());
let module = result.unwrap();
assert!(module.has_export("add"), "Should export 'add' function");
assert!(module.validate().is_ok(), "Should produce valid WASM");
}
#[test]
fn test_compile_function_with_params() {
use crate::frontend::ast::{Param, Pattern, Type as AstType, TypeKind};
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Function {
name: "add_two".to_string(),
type_params: vec![],
params: vec![
Param {
pattern: Pattern::Identifier("a".to_string()),
ty: AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
},
span: Span::default(),
is_mutable: false,
default_value: None,
},
Param {
pattern: Pattern::Identifier("b".to_string()),
ty: AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
},
span: Span::default(),
is_mutable: false,
default_value: None,
},
],
body: Box::new(make_binary(make_int(1), BinaryOp::Add, make_int(2))),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile function with params");
let module = result.unwrap();
assert!(module.has_export("add_two"));
assert!(module.validate().is_ok());
}
#[test]
fn test_compile_function_with_return() {
use crate::frontend::ast::{Type as AstType, TypeKind};
let compiler = WasmCompiler::new();
let return_expr = Expr {
kind: ExprKind::Return {
value: Some(Box::new(make_int(99))),
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let ast = Expr {
kind: ExprKind::Function {
name: "get_val".to_string(),
type_params: vec![],
params: vec![],
body: Box::new(return_expr),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile function with return");
let module = result.unwrap();
assert!(module.has_export("get_val"));
}
#[test]
fn test_compile_block_with_functions() {
use crate::frontend::ast::{Type as AstType, TypeKind};
let compiler = WasmCompiler::new();
let func1 = Expr {
kind: ExprKind::Function {
name: "func_a".to_string(),
type_params: vec![],
params: vec![],
body: Box::new(make_int(1)),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let func2 = Expr {
kind: ExprKind::Function {
name: "func_b".to_string(),
type_params: vec![],
params: vec![],
body: Box::new(make_int(2)),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let ast = Expr {
kind: ExprKind::Block(vec![func1, func2]),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok(), "Should compile block with functions");
let module = result.unwrap();
assert!(module.has_export("func_a"));
assert!(module.has_export("func_b"));
}
#[test]
fn test_compile_block_with_non_function() {
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Block(vec![make_int(42), make_int(99)]),
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
let module = result.unwrap();
assert!(!module.has_export("main"));
}
#[test]
fn test_compile_function_without_return_appends_i32_const() {
use crate::frontend::ast::{Type as AstType, TypeKind};
let compiler = WasmCompiler::new();
let ast = Expr {
kind: ExprKind::Function {
name: "no_ret".to_string(),
type_params: vec![],
params: vec![],
body: Box::new(make_int(7)),
is_async: false,
return_type: Some(AstType {
kind: TypeKind::Named("i32".to_string()),
span: Span::default(),
}),
is_pub: true,
},
span: Span::default(),
attributes: vec![],
leading_comments: vec![],
trailing_comment: None,
};
let result = compiler.compile(&ast);
assert!(result.is_ok());
let module = result.unwrap();
assert!(module.validate().is_ok());
assert!(module.has_export("no_ret"));
}
}