Skip to main content

pepl_codegen/
test_codegen.rs

1//! Test block codegen — compiles PEPL `tests { }` blocks to WASM functions.
2//!
3//! Each test case `test "description" { ... }` becomes a WASM function
4//! `__test_N()` that:
5//! 1. Re-initialises state (calls init() internally)
6//! 2. Executes the test body (action dispatches, assertions, let bindings)
7//! 3. Returns void on success, or traps on assertion failure
8//!
9//! The host calls `__test_count()` to discover how many tests exist,
10//! then `__test_N()` (N = 0, 1, ...) to run each.
11
12use std::collections::HashMap;
13use wasm_encoder::{Function, Instruction};
14
15use crate::compiler::FuncContext;
16use crate::error::CodegenResult;
17use crate::runtime::memarg;
18use crate::types::*;
19
20use pepl_types::ast::*;
21
22/// Compile a single test body into WASM instructions.
23///
24/// Emits:
25/// 1. `call init` — reset state to defaults
26/// 2. Compiled test body statements
27pub fn emit_test_body(
28    body: &Block,
29    actions: &HashMap<String, u32>,
30    dispatch_func_idx: u32,
31    init_func_idx: u32,
32    ctx: &mut FuncContext,
33    f: &mut Function,
34) -> CodegenResult<()> {
35    // Re-initialise state
36    f.instruction(&Instruction::Call(init_func_idx));
37
38    // Compile each test statement
39    for stmt in &body.stmts {
40        emit_test_stmt(stmt, actions, dispatch_func_idx, ctx, f)?;
41    }
42    Ok(())
43}
44
45/// Compile a single test statement.
46fn emit_test_stmt(
47    stmt: &Stmt,
48    actions: &HashMap<String, u32>,
49    dispatch_func_idx: u32,
50    ctx: &mut FuncContext,
51    f: &mut Function,
52) -> CodegenResult<()> {
53    match stmt {
54        Stmt::Expr(expr_stmt) => {
55            if is_action_call(&expr_stmt.expr, actions) {
56                emit_action_dispatch(&expr_stmt.expr, actions, dispatch_func_idx, f)
57            } else {
58                crate::expr::emit_expr(&expr_stmt.expr, ctx, f)?;
59                f.instruction(&Instruction::Drop);
60                Ok(())
61            }
62        }
63        Stmt::Assert(assert_stmt) => emit_test_assert(assert_stmt, ctx, f),
64        Stmt::Let(binding) => {
65            crate::expr::emit_expr(&binding.value, ctx, f)?;
66            if let Some(name) = &binding.name {
67                let local = ctx.alloc_local(wasm_encoder::ValType::I32);
68                f.instruction(&Instruction::LocalSet(local));
69                ctx.push_local(&name.name, local);
70            } else {
71                f.instruction(&Instruction::Drop);
72            }
73            Ok(())
74        }
75        _ => crate::stmt::emit_stmt(stmt, ctx, f),
76    }
77}
78
79/// Check if an expression is an action dispatch call.
80fn is_action_call(expr: &Expr, actions: &HashMap<String, u32>) -> bool {
81    matches!(&expr.kind, ExprKind::Call { name, .. } if actions.contains_key(&name.name))
82}
83
84/// Emit action dispatch call.
85fn emit_action_dispatch(
86    expr: &Expr,
87    actions: &HashMap<String, u32>,
88    dispatch_func_idx: u32,
89    f: &mut Function,
90) -> CodegenResult<()> {
91    if let ExprKind::Call { name, .. } = &expr.kind {
92        let action_id = actions[&name.name];
93        f.instruction(&Instruction::I32Const(action_id as i32));
94        f.instruction(&Instruction::I32Const(0)); // payload_ptr
95        f.instruction(&Instruction::I32Const(0)); // payload_len
96        f.instruction(&Instruction::Call(dispatch_func_idx));
97    }
98    Ok(())
99}
100
101/// Compile `assert condition [, "message"]` — traps if condition is false.
102fn emit_test_assert(
103    assert: &AssertStmt,
104    ctx: &mut FuncContext,
105    f: &mut Function,
106) -> CodegenResult<()> {
107    crate::expr::emit_expr(&assert.condition, ctx, f)?;
108
109    let val_local = ctx.alloc_local(wasm_encoder::ValType::I32);
110    f.instruction(&Instruction::LocalSet(val_local));
111    f.instruction(&Instruction::LocalGet(val_local));
112    f.instruction(&Instruction::I32Load(memarg(4, 2))); // w1: bool payload
113    f.instruction(&Instruction::I32Eqz); // true → assertion failed
114
115    f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
116    let msg = assert
117        .message
118        .clone()
119        .unwrap_or_else(|| "assertion failed".to_string());
120    let (msg_ptr, msg_len) = ctx.intern_string(&msg);
121    f.instruction(&Instruction::I32Const(msg_ptr as i32));
122    f.instruction(&Instruction::I32Const(msg_len as i32));
123    f.instruction(&Instruction::Call(IMPORT_TRAP));
124    f.instruction(&Instruction::End);
125
126    Ok(())
127}
128
129/// Emit `__test_count() -> i32`.
130pub fn emit_test_count(count: usize) -> Function {
131    let mut f = Function::new(vec![]);
132    f.instruction(&Instruction::I32Const(count as i32));
133    f.instruction(&Instruction::End);
134    f
135}