Skip to main content

pepl_codegen/
expr.rs

1//! Expression code generation.
2//!
3//! Every expression evaluates to an i32 *value pointer* left on the WASM
4//! operand stack.  The caller can then store it, pass it to another function,
5//! or ignore it.
6
7use pepl_types::ast::*;
8use wasm_encoder::{BlockType, Function, Instruction, ValType};
9
10use crate::compiler::FuncContext;
11use crate::error::CodegenResult;
12use crate::gas;
13use crate::runtime::*;
14use crate::stmt::emit_stmts;
15use crate::types::*;
16
17/// Emit instructions for an expression.  Leaves one i32 (value ptr) on stack.
18pub fn emit_expr(expr: &Expr, ctx: &mut FuncContext, f: &mut Function) -> CodegenResult<()> {
19    match &expr.kind {
20        // ── Literals ──────────────────────────────────────────────────────
21        ExprKind::NumberLit(n) => emit_number_lit(*n, ctx, f),
22        ExprKind::StringLit(s) => emit_string_lit(s, ctx, f),
23        ExprKind::BoolLit(b) => emit_bool_lit(*b, f),
24        ExprKind::NilLit => emit_nil_lit(f),
25        ExprKind::ListLit(elems) => emit_list_lit(elems, ctx, f),
26        ExprKind::RecordLit(entries) => emit_record_lit(entries, ctx, f),
27        ExprKind::StringInterpolation(parts) => emit_string_interpolation(parts, ctx, f),
28
29        // ── Identifiers ──────────────────────────────────────────────────
30        ExprKind::Identifier(name) => emit_identifier(name, ctx, f),
31
32        // ── Calls ────────────────────────────────────────────────────────
33        ExprKind::Call { name, args } => emit_call(&name.name, args, ctx, f),
34        ExprKind::QualifiedCall {
35            module,
36            function,
37            args,
38        } => emit_qualified_call(&module.name, &function.name, args, ctx, f),
39        ExprKind::FieldAccess { object, field } => {
40            emit_field_access(object, &field.name, ctx, f)
41        }
42        ExprKind::MethodCall {
43            object,
44            method,
45            args,
46        } => emit_method_call(object, &method.name, args, ctx, f),
47
48        // ── Operators ────────────────────────────────────────────────────
49        ExprKind::Binary { left, op, right } => emit_binary(left, *op, right, ctx, f),
50        ExprKind::Unary { op, operand } => emit_unary(*op, operand, ctx, f),
51        ExprKind::ResultUnwrap(inner) => emit_result_unwrap(inner, ctx, f),
52        ExprKind::NilCoalesce { left, right } => emit_nil_coalesce(left, right, ctx, f),
53
54        // ── Control Flow ─────────────────────────────────────────────────
55        ExprKind::If(if_expr) => emit_if_expr(if_expr, ctx, f),
56        ExprKind::For(for_expr) => emit_for_expr(for_expr, ctx, f),
57        ExprKind::Match(match_expr) => emit_match_expr(match_expr, ctx, f),
58
59        // ── Lambda ───────────────────────────────────────────────────────
60        ExprKind::Lambda(lambda) => {
61            emit_lambda(lambda, ctx, f)
62        }
63
64        // ── Grouping ─────────────────────────────────────────────────────
65        ExprKind::Paren(inner) => emit_expr(inner, ctx, f),
66    }
67}
68
69// ══════════════════════════════════════════════════════════════════════════════
70// Literal emission
71// ══════════════════════════════════════════════════════════════════════════════
72
73fn emit_number_lit(n: f64, _ctx: &mut FuncContext, f: &mut Function) -> CodegenResult<()> {
74    // Allocate a VALUE_SIZE cell, write tag + f64 directly.
75    f.instruction(&Instruction::I32Const(VALUE_SIZE as i32));
76    f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
77    // duplicate ptr for tag store and f64 store
78    let local = _ctx.alloc_local(ValType::I32);
79    f.instruction(&Instruction::LocalTee(local));
80    f.instruction(&Instruction::I32Const(TAG_NUMBER));
81    f.instruction(&Instruction::I32Store(memarg(0, 2)));
82    f.instruction(&Instruction::LocalGet(local));
83    f.instruction(&Instruction::F64Const(n));
84    f.instruction(&Instruction::F64Store(memarg(4, 3)));
85    f.instruction(&Instruction::LocalGet(local));
86    Ok(())
87}
88
89fn emit_string_lit(s: &str, ctx: &mut FuncContext, f: &mut Function) -> CodegenResult<()> {
90    let (ptr, len) = ctx.intern_string(s);
91    f.instruction(&Instruction::I32Const(ptr as i32));
92    f.instruction(&Instruction::I32Const(len as i32));
93    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_STRING)));
94    Ok(())
95}
96
97fn emit_bool_lit(b: bool, f: &mut Function) -> CodegenResult<()> {
98    f.instruction(&Instruction::I32Const(if b { 1 } else { 0 }));
99    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_BOOL)));
100    Ok(())
101}
102
103fn emit_nil_lit(f: &mut Function) -> CodegenResult<()> {
104    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
105    Ok(())
106}
107
108fn emit_lambda(
109    lambda: &LambdaExpr,
110    ctx: &mut FuncContext,
111    f: &mut Function,
112) -> CodegenResult<()> {
113    // Determine captured variables: all locals currently in scope
114    // that are NOT lambda parameters.
115    let param_names: Vec<String> = lambda.params.iter().map(|p| p.name.name.clone()).collect();
116    let mut captured: Vec<String> = ctx
117        .local_names
118        .keys()
119        .filter(|name| !param_names.contains(name))
120        .cloned()
121        .collect();
122    captured.sort(); // deterministic ordering
123
124    // Build closure environment record from captured variables
125    let env_local = ctx.alloc_local(ValType::I32);
126    if captured.is_empty() {
127        // No captures — env is nil
128        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
129        f.instruction(&Instruction::LocalSet(env_local));
130    } else {
131        // Build a RECORD with captured variable values
132        let cap_entries = ctx.alloc_local(ValType::I32);
133        f.instruction(&Instruction::I32Const((captured.len() * 12) as i32));
134        f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
135        f.instruction(&Instruction::LocalSet(cap_entries));
136
137        for (ci, cap_name) in captured.iter().enumerate() {
138            let (cap_key_ptr, cap_key_len) = ctx.intern_string(cap_name);
139            let cap_val_local = ctx.get_local(cap_name).unwrap_or(0);
140            let base = (ci * 12) as u64;
141            // key_offset
142            f.instruction(&Instruction::LocalGet(cap_entries));
143            f.instruction(&Instruction::I32Const(cap_key_ptr as i32));
144            f.instruction(&Instruction::I32Store(memarg(base, 2)));
145            // key_len
146            f.instruction(&Instruction::LocalGet(cap_entries));
147            f.instruction(&Instruction::I32Const(cap_key_len as i32));
148            f.instruction(&Instruction::I32Store(memarg(base + 4, 2)));
149            // value
150            f.instruction(&Instruction::LocalGet(cap_entries));
151            f.instruction(&Instruction::LocalGet(cap_val_local));
152            f.instruction(&Instruction::I32Store(memarg(base + 8, 2)));
153        }
154
155        f.instruction(&Instruction::LocalGet(cap_entries));
156        f.instruction(&Instruction::I32Const(captured.len() as i32));
157        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD)));
158        f.instruction(&Instruction::LocalSet(env_local));
159    }
160
161    // Register lambda body for deferred compilation, get table index
162    let table_idx = ctx.register_lambda(
163        lambda.params.clone(),
164        lambda.body.clone(),
165        captured,
166    );
167
168    // Create LAMBDA value: tag=7, w1=table_idx, w2=env_ptr
169    let lambda_val = ctx.alloc_local(ValType::I32);
170    f.instruction(&Instruction::I32Const(VALUE_SIZE as i32));
171    f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
172    f.instruction(&Instruction::LocalSet(lambda_val));
173    // tag = TAG_LAMBDA
174    f.instruction(&Instruction::LocalGet(lambda_val));
175    f.instruction(&Instruction::I32Const(TAG_LAMBDA));
176    f.instruction(&Instruction::I32Store(memarg(0, 2)));
177    // w1 = table_idx (index into the indirect function table)
178    f.instruction(&Instruction::LocalGet(lambda_val));
179    f.instruction(&Instruction::I32Const(table_idx as i32));
180    f.instruction(&Instruction::I32Store(memarg(4, 2)));
181    // w2 = env_ptr
182    f.instruction(&Instruction::LocalGet(lambda_val));
183    f.instruction(&Instruction::LocalGet(env_local));
184    f.instruction(&Instruction::I32Store(memarg(8, 2)));
185    // Leave lambda_val on stack
186    f.instruction(&Instruction::LocalGet(lambda_val));
187    Ok(())
188}
189
190fn emit_list_lit(
191    elems: &[Expr],
192    ctx: &mut FuncContext,
193    f: &mut Function,
194) -> CodegenResult<()> {
195    let count = elems.len() as i32;
196    if count == 0 {
197        // Empty list: arr_ptr = 0, count = 0
198        f.instruction(&Instruction::I32Const(0));
199        f.instruction(&Instruction::I32Const(0));
200        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST)));
201        return Ok(());
202    }
203
204    // Allocate array of i32 pointers
205    let arr_local = ctx.alloc_local(ValType::I32);
206    f.instruction(&Instruction::I32Const(count * 4));
207    f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
208    f.instruction(&Instruction::LocalSet(arr_local));
209
210    // Evaluate each element and store its pointer
211    for (i, elem) in elems.iter().enumerate() {
212        let elem_local = ctx.alloc_local(ValType::I32);
213        emit_expr(elem, ctx, f)?;
214        f.instruction(&Instruction::LocalSet(elem_local));
215        // arr[i] = elem_ptr
216        f.instruction(&Instruction::LocalGet(arr_local));
217        f.instruction(&Instruction::LocalGet(elem_local));
218        f.instruction(&Instruction::I32Store(memarg(i as u64 * 4, 2)));
219    }
220
221    // Create list value
222    f.instruction(&Instruction::LocalGet(arr_local));
223    f.instruction(&Instruction::I32Const(count));
224    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST)));
225    Ok(())
226}
227
228fn emit_record_lit(
229    entries: &[RecordEntry],
230    ctx: &mut FuncContext,
231    f: &mut Function,
232) -> CodegenResult<()> {
233    let explicit_count = entries
234        .iter()
235        .filter(|e| matches!(e, RecordEntry::Field { .. }))
236        .count();
237
238    let has_spread = entries
239        .iter()
240        .any(|e| matches!(e, RecordEntry::Spread(_)));
241
242    if !has_spread {
243        // No spread — simple static allocation
244        if explicit_count == 0 {
245            f.instruction(&Instruction::I32Const(0));
246            f.instruction(&Instruction::I32Const(0));
247            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD)));
248            return Ok(());
249        }
250
251        let entries_local = ctx.alloc_local(ValType::I32);
252        f.instruction(&Instruction::I32Const((explicit_count * 12) as i32));
253        f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
254        f.instruction(&Instruction::LocalSet(entries_local));
255
256        let mut idx = 0usize;
257        for entry in entries {
258            if let RecordEntry::Field { name, value } = entry {
259                let (key_ptr, key_len) = ctx.intern_string(&name.name);
260                let val_local = ctx.alloc_local(ValType::I32);
261                emit_expr(value, ctx, f)?;
262                f.instruction(&Instruction::LocalSet(val_local));
263
264                let base_offset = (idx * 12) as u64;
265                f.instruction(&Instruction::LocalGet(entries_local));
266                f.instruction(&Instruction::I32Const(key_ptr as i32));
267                f.instruction(&Instruction::I32Store(memarg(base_offset, 2)));
268                f.instruction(&Instruction::LocalGet(entries_local));
269                f.instruction(&Instruction::I32Const(key_len as i32));
270                f.instruction(&Instruction::I32Store(memarg(base_offset + 4, 2)));
271                f.instruction(&Instruction::LocalGet(entries_local));
272                f.instruction(&Instruction::LocalGet(val_local));
273                f.instruction(&Instruction::I32Store(memarg(base_offset + 8, 2)));
274                idx += 1;
275            }
276        }
277
278        f.instruction(&Instruction::LocalGet(entries_local));
279        f.instruction(&Instruction::I32Const(explicit_count as i32));
280        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD)));
281        return Ok(());
282    }
283
284    // ── Spread path: dynamic record construction ─────────────────────────
285    // Strategy: copy spread source entries, then overlay explicit fields
286    // (overwrite matching key or append new entry).
287
288    let new_entries = ctx.alloc_local(ValType::I32);
289    let final_count = ctx.alloc_local(ValType::I32);
290
291    // Evaluate first spread expression
292    let spread_expr = entries
293        .iter()
294        .find_map(|e| match e {
295            RecordEntry::Spread(expr) => Some(expr),
296            _ => None,
297        })
298        .unwrap();
299
300    let spread_local = ctx.alloc_local(ValType::I32);
301    emit_expr(spread_expr, ctx, f)?;
302    f.instruction(&Instruction::LocalSet(spread_local));
303
304    // Read spread record's entries and field count
305    let spread_entries_ptr = ctx.alloc_local(ValType::I32);
306    let spread_count_local = ctx.alloc_local(ValType::I32);
307    f.instruction(&Instruction::LocalGet(spread_local));
308    f.instruction(&Instruction::I32Load(memarg(4, 2)));
309    f.instruction(&Instruction::LocalSet(spread_entries_ptr));
310    f.instruction(&Instruction::LocalGet(spread_local));
311    f.instruction(&Instruction::I32Load(memarg(8, 2)));
312    f.instruction(&Instruction::LocalSet(spread_count_local));
313
314    // Allocate max entries: spread_count + explicit_count
315    f.instruction(&Instruction::LocalGet(spread_count_local));
316    f.instruction(&Instruction::I32Const(explicit_count as i32));
317    f.instruction(&Instruction::I32Add);
318    f.instruction(&Instruction::I32Const(12));
319    f.instruction(&Instruction::I32Mul);
320    f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
321    f.instruction(&Instruction::LocalSet(new_entries));
322
323    // Copy all spread entries via loop
324    let copy_i = ctx.alloc_local(ValType::I32);
325    let src_entry = ctx.alloc_local(ValType::I32);
326    let dst_entry = ctx.alloc_local(ValType::I32);
327    f.instruction(&Instruction::I32Const(0));
328    f.instruction(&Instruction::LocalSet(copy_i));
329
330    f.instruction(&Instruction::Block(BlockType::Empty));
331    f.instruction(&Instruction::Loop(BlockType::Empty));
332    f.instruction(&Instruction::LocalGet(copy_i));
333    f.instruction(&Instruction::LocalGet(spread_count_local));
334    f.instruction(&Instruction::I32GeU);
335    f.instruction(&Instruction::BrIf(1));
336
337    f.instruction(&Instruction::LocalGet(spread_entries_ptr));
338    f.instruction(&Instruction::LocalGet(copy_i));
339    f.instruction(&Instruction::I32Const(12));
340    f.instruction(&Instruction::I32Mul);
341    f.instruction(&Instruction::I32Add);
342    f.instruction(&Instruction::LocalSet(src_entry));
343    f.instruction(&Instruction::LocalGet(new_entries));
344    f.instruction(&Instruction::LocalGet(copy_i));
345    f.instruction(&Instruction::I32Const(12));
346    f.instruction(&Instruction::I32Mul);
347    f.instruction(&Instruction::I32Add);
348    f.instruction(&Instruction::LocalSet(dst_entry));
349
350    // Copy key_offset, key_len, value_ptr
351    f.instruction(&Instruction::LocalGet(dst_entry));
352    f.instruction(&Instruction::LocalGet(src_entry));
353    f.instruction(&Instruction::I32Load(memarg(0, 2)));
354    f.instruction(&Instruction::I32Store(memarg(0, 2)));
355    f.instruction(&Instruction::LocalGet(dst_entry));
356    f.instruction(&Instruction::LocalGet(src_entry));
357    f.instruction(&Instruction::I32Load(memarg(4, 2)));
358    f.instruction(&Instruction::I32Store(memarg(4, 2)));
359    f.instruction(&Instruction::LocalGet(dst_entry));
360    f.instruction(&Instruction::LocalGet(src_entry));
361    f.instruction(&Instruction::I32Load(memarg(8, 2)));
362    f.instruction(&Instruction::I32Store(memarg(8, 2)));
363
364    f.instruction(&Instruction::LocalGet(copy_i));
365    f.instruction(&Instruction::I32Const(1));
366    f.instruction(&Instruction::I32Add);
367    f.instruction(&Instruction::LocalSet(copy_i));
368    f.instruction(&Instruction::Br(0));
369    f.instruction(&Instruction::End); // end loop
370    f.instruction(&Instruction::End); // end block
371
372    // Initialize final_count = spread_count
373    f.instruction(&Instruction::LocalGet(spread_count_local));
374    f.instruction(&Instruction::LocalSet(final_count));
375
376    // For each explicit field: scan for matching key → overwrite or append
377    for entry in entries {
378        if let RecordEntry::Field { name, value } = entry {
379            let (key_ptr, key_len) = ctx.intern_string(&name.name);
380            let val_local = ctx.alloc_local(ValType::I32);
381            emit_expr(value, ctx, f)?;
382            f.instruction(&Instruction::LocalSet(val_local));
383
384            let scan_i = ctx.alloc_local(ValType::I32);
385            let found = ctx.alloc_local(ValType::I32);
386            let scan_entry = ctx.alloc_local(ValType::I32);
387            f.instruction(&Instruction::I32Const(0));
388            f.instruction(&Instruction::LocalSet(scan_i));
389            f.instruction(&Instruction::I32Const(0));
390            f.instruction(&Instruction::LocalSet(found));
391
392            f.instruction(&Instruction::Block(BlockType::Empty));
393            f.instruction(&Instruction::Loop(BlockType::Empty));
394            f.instruction(&Instruction::LocalGet(scan_i));
395            f.instruction(&Instruction::LocalGet(final_count));
396            f.instruction(&Instruction::I32GeU);
397            f.instruction(&Instruction::BrIf(1));
398
399            f.instruction(&Instruction::LocalGet(new_entries));
400            f.instruction(&Instruction::LocalGet(scan_i));
401            f.instruction(&Instruction::I32Const(12));
402            f.instruction(&Instruction::I32Mul);
403            f.instruction(&Instruction::I32Add);
404            f.instruction(&Instruction::LocalSet(scan_entry));
405
406            // Compare key_len then memcmp
407            f.instruction(&Instruction::LocalGet(scan_entry));
408            f.instruction(&Instruction::I32Load(memarg(4, 2)));
409            f.instruction(&Instruction::I32Const(key_len as i32));
410            f.instruction(&Instruction::I32Eq);
411            f.instruction(&Instruction::If(BlockType::Empty));
412            f.instruction(&Instruction::LocalGet(scan_entry));
413            f.instruction(&Instruction::I32Load(memarg(0, 2)));
414            f.instruction(&Instruction::I32Const(key_ptr as i32));
415            f.instruction(&Instruction::I32Const(key_len as i32));
416            f.instruction(&Instruction::Call(rt_func_idx(RT_MEMCMP)));
417            f.instruction(&Instruction::If(BlockType::Empty));
418            // Found — overwrite value
419            f.instruction(&Instruction::LocalGet(scan_entry));
420            f.instruction(&Instruction::LocalGet(val_local));
421            f.instruction(&Instruction::I32Store(memarg(8, 2)));
422            f.instruction(&Instruction::I32Const(1));
423            f.instruction(&Instruction::LocalSet(found));
424            f.instruction(&Instruction::Br(3)); // break loop+block
425            f.instruction(&Instruction::End); // end memcmp if
426            f.instruction(&Instruction::End); // end key_len if
427
428            f.instruction(&Instruction::LocalGet(scan_i));
429            f.instruction(&Instruction::I32Const(1));
430            f.instruction(&Instruction::I32Add);
431            f.instruction(&Instruction::LocalSet(scan_i));
432            f.instruction(&Instruction::Br(0));
433            f.instruction(&Instruction::End); // end loop
434            f.instruction(&Instruction::End); // end block
435
436            // If not found, append new entry
437            f.instruction(&Instruction::LocalGet(found));
438            f.instruction(&Instruction::I32Eqz);
439            f.instruction(&Instruction::If(BlockType::Empty));
440            let append_entry = ctx.alloc_local(ValType::I32);
441            f.instruction(&Instruction::LocalGet(new_entries));
442            f.instruction(&Instruction::LocalGet(final_count));
443            f.instruction(&Instruction::I32Const(12));
444            f.instruction(&Instruction::I32Mul);
445            f.instruction(&Instruction::I32Add);
446            f.instruction(&Instruction::LocalSet(append_entry));
447            f.instruction(&Instruction::LocalGet(append_entry));
448            f.instruction(&Instruction::I32Const(key_ptr as i32));
449            f.instruction(&Instruction::I32Store(memarg(0, 2)));
450            f.instruction(&Instruction::LocalGet(append_entry));
451            f.instruction(&Instruction::I32Const(key_len as i32));
452            f.instruction(&Instruction::I32Store(memarg(4, 2)));
453            f.instruction(&Instruction::LocalGet(append_entry));
454            f.instruction(&Instruction::LocalGet(val_local));
455            f.instruction(&Instruction::I32Store(memarg(8, 2)));
456            f.instruction(&Instruction::LocalGet(final_count));
457            f.instruction(&Instruction::I32Const(1));
458            f.instruction(&Instruction::I32Add);
459            f.instruction(&Instruction::LocalSet(final_count));
460            f.instruction(&Instruction::End); // end not-found if
461        }
462    }
463
464    // Build final record
465    f.instruction(&Instruction::LocalGet(new_entries));
466    f.instruction(&Instruction::LocalGet(final_count));
467    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD)));
468    Ok(())
469}
470
471fn emit_string_interpolation(
472    parts: &[StringPart],
473    ctx: &mut FuncContext,
474    f: &mut Function,
475) -> CodegenResult<()> {
476    // Build string by concatenating parts left to right.
477    // Start with empty string, concat each part.
478    let (empty_ptr, empty_len) = ctx.intern_string("");
479    f.instruction(&Instruction::I32Const(empty_ptr as i32));
480    f.instruction(&Instruction::I32Const(empty_len as i32));
481    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_STRING)));
482
483    for part in parts {
484        match part {
485            StringPart::Literal(s) => {
486                if !s.is_empty() {
487                    emit_string_lit(s, ctx, f)?;
488                    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_STRING_CONCAT)));
489                }
490            }
491            StringPart::Expr(expr) => {
492                emit_expr(expr, ctx, f)?;
493                f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_TO_STRING)));
494                f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_STRING_CONCAT)));
495            }
496        }
497    }
498    Ok(())
499}
500
501// ══════════════════════════════════════════════════════════════════════════════
502// Identifiers & Calls
503// ══════════════════════════════════════════════════════════════════════════════
504
505fn emit_identifier(name: &str, ctx: &mut FuncContext, f: &mut Function) -> CodegenResult<()> {
506    // Look up in locals first, then state fields, then action names
507    if let Some(local_idx) = ctx.get_local(name) {
508        f.instruction(&Instruction::LocalGet(local_idx));
509        return Ok(());
510    }
511
512    // State field access: record_get(state_ptr, key)
513    if ctx.is_state_field(name) {
514        let (key_ptr, key_len) = ctx.intern_string(name);
515        f.instruction(&Instruction::GlobalGet(GLOBAL_STATE_PTR));
516        f.instruction(&Instruction::I32Const(key_ptr as i32));
517        f.instruction(&Instruction::I32Const(key_len as i32));
518        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD_GET)));
519        return Ok(());
520    }
521
522    // Action reference
523    if let Some(action_id) = ctx.get_action_id(name) {
524        f.instruction(&Instruction::I32Const(action_id as i32));
525        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_ACTION_REF)));
526        return Ok(());
527    }
528
529    // Unknown — return nil with a note
530    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
531    Ok(())
532}
533
534fn emit_call(
535    name: &str,
536    args: &[Expr],
537    ctx: &mut FuncContext,
538    f: &mut Function,
539) -> CodegenResult<()> {
540    // Gas tick at every call site
541    gas::emit_gas_tick(f, ctx.data.gas_exhausted_ptr, ctx.data.gas_exhausted_len);
542
543    // Check if this is a locally-defined function (action call, etc.)
544    if let Some(func_idx) = ctx.get_function(name) {
545        // Push args
546        for arg in args {
547            emit_expr(arg, ctx, f)?;
548        }
549        f.instruction(&Instruction::Call(func_idx));
550        return Ok(());
551    }
552
553    // Unknown function — eval args and discard, return nil
554    for arg in args {
555        emit_expr(arg, ctx, f)?;
556        f.instruction(&Instruction::Drop);
557    }
558    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
559    Ok(())
560}
561
562fn emit_qualified_call(
563    module: &str,
564    function: &str,
565    args: &[Expr],
566    ctx: &mut FuncContext,
567    f: &mut Function,
568) -> CodegenResult<()> {
569    gas::emit_gas_tick(f, ctx.data.gas_exhausted_ptr, ctx.data.gas_exhausted_len);
570
571    // Stdlib calls are dispatched via host_call for capability modules,
572    // or handled inline for pure modules (math, string, list, etc.).
573    // For now, we lower all qualified calls to host_call with serialized args.
574
575    // Evaluate args into a list value
576    let args_local = ctx.alloc_local(ValType::I32);
577    if args.is_empty() {
578        f.instruction(&Instruction::I32Const(0));
579        f.instruction(&Instruction::I32Const(0));
580        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST)));
581    } else {
582        // Build args list
583        let arr_local = ctx.alloc_local(ValType::I32);
584        let count = args.len() as i32;
585        f.instruction(&Instruction::I32Const(count * 4));
586        f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
587        f.instruction(&Instruction::LocalSet(arr_local));
588        for (i, arg) in args.iter().enumerate() {
589            let tmp = ctx.alloc_local(ValType::I32);
590            emit_expr(arg, ctx, f)?;
591            f.instruction(&Instruction::LocalSet(tmp));
592            f.instruction(&Instruction::LocalGet(arr_local));
593            f.instruction(&Instruction::LocalGet(tmp));
594            f.instruction(&Instruction::I32Store(memarg(i as u64 * 4, 2)));
595        }
596        f.instruction(&Instruction::LocalGet(arr_local));
597        f.instruction(&Instruction::I32Const(count));
598        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST)));
599    }
600    f.instruction(&Instruction::LocalSet(args_local));
601
602    // Intern module and function names to resolve cap_id/fn_id at compile time
603    let (mod_id, fn_id) = ctx.resolve_qualified_call(module, function);
604
605    // host_call(cap_id, fn_id, args_ptr) -> result_ptr
606    f.instruction(&Instruction::I32Const(mod_id as i32));
607    f.instruction(&Instruction::I32Const(fn_id as i32));
608    f.instruction(&Instruction::LocalGet(args_local));
609    f.instruction(&Instruction::Call(IMPORT_HOST_CALL));
610
611    Ok(())
612}
613
614fn emit_field_access(
615    object: &Expr,
616    field: &str,
617    ctx: &mut FuncContext,
618    f: &mut Function,
619) -> CodegenResult<()> {
620    emit_expr(object, ctx, f)?;
621    let (key_ptr, key_len) = ctx.intern_string(field);
622    f.instruction(&Instruction::I32Const(key_ptr as i32));
623    f.instruction(&Instruction::I32Const(key_len as i32));
624    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD_GET)));
625    Ok(())
626}
627
628fn emit_method_call(
629    object: &Expr,
630    method: &str,
631    args: &[Expr],
632    ctx: &mut FuncContext,
633    f: &mut Function,
634) -> CodegenResult<()> {
635    // Method calls in PEPL are sugar for qualified calls on the receiver type.
636    // E.g., `items.length()` → `list.length(items)`
637    // We emit as host_call with the receiver as the first arg.
638    gas::emit_gas_tick(f, ctx.data.gas_exhausted_ptr, ctx.data.gas_exhausted_len);
639
640    let total_args = 1 + args.len();
641    let arr_local = ctx.alloc_local(ValType::I32);
642    let count = total_args as i32;
643
644    f.instruction(&Instruction::I32Const(count * 4));
645    f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
646    f.instruction(&Instruction::LocalSet(arr_local));
647
648    // Store receiver as arg[0]
649    let recv_local = ctx.alloc_local(ValType::I32);
650    emit_expr(object, ctx, f)?;
651    f.instruction(&Instruction::LocalSet(recv_local));
652    f.instruction(&Instruction::LocalGet(arr_local));
653    f.instruction(&Instruction::LocalGet(recv_local));
654    f.instruction(&Instruction::I32Store(memarg(0, 2)));
655
656    // Store remaining args
657    for (i, arg) in args.iter().enumerate() {
658        let tmp = ctx.alloc_local(ValType::I32);
659        emit_expr(arg, ctx, f)?;
660        f.instruction(&Instruction::LocalSet(tmp));
661        f.instruction(&Instruction::LocalGet(arr_local));
662        f.instruction(&Instruction::LocalGet(tmp));
663        f.instruction(&Instruction::I32Store(memarg((i as u64 + 1) * 4, 2)));
664    }
665
666    // Determine module from method name (heuristic: number methods → math, etc.)
667    // For now, dispatch all method calls via host_call module=0 fn=method_id
668    let (mod_id, fn_id) = ctx.resolve_method_call(method);
669
670    f.instruction(&Instruction::I32Const(mod_id as i32));
671    f.instruction(&Instruction::I32Const(fn_id as i32));
672    f.instruction(&Instruction::LocalGet(arr_local));
673    f.instruction(&Instruction::I32Const(count));
674    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST)));
675    f.instruction(&Instruction::Call(IMPORT_HOST_CALL));
676
677    Ok(())
678}
679
680// ══════════════════════════════════════════════════════════════════════════════
681// Operators
682// ══════════════════════════════════════════════════════════════════════════════
683
684fn emit_binary(
685    left: &Expr,
686    op: BinOp,
687    right: &Expr,
688    ctx: &mut FuncContext,
689    f: &mut Function,
690) -> CodegenResult<()> {
691    match op {
692        BinOp::And => {
693            // Short-circuit: if left is falsy, return left; else return right
694            let left_local = ctx.alloc_local(ValType::I32);
695            emit_expr(left, ctx, f)?;
696            f.instruction(&Instruction::LocalTee(left_local));
697            // Check truthy (for bools: w1 != 0)
698            f.instruction(&Instruction::I32Load(memarg(4, 2)));
699            f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
700            emit_expr(right, ctx, f)?;
701            f.instruction(&Instruction::Else);
702            f.instruction(&Instruction::LocalGet(left_local));
703            f.instruction(&Instruction::End);
704            return Ok(());
705        }
706        BinOp::Or => {
707            // Short-circuit: if left is truthy, return left; else return right
708            let left_local = ctx.alloc_local(ValType::I32);
709            emit_expr(left, ctx, f)?;
710            f.instruction(&Instruction::LocalTee(left_local));
711            f.instruction(&Instruction::I32Load(memarg(4, 2)));
712            f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
713            f.instruction(&Instruction::LocalGet(left_local));
714            f.instruction(&Instruction::Else);
715            emit_expr(right, ctx, f)?;
716            f.instruction(&Instruction::End);
717            return Ok(());
718        }
719        _ => {}
720    }
721
722    // Evaluate both sides
723    let a = ctx.alloc_local(ValType::I32);
724    let b = ctx.alloc_local(ValType::I32);
725    emit_expr(left, ctx, f)?;
726    f.instruction(&Instruction::LocalSet(a));
727    emit_expr(right, ctx, f)?;
728    f.instruction(&Instruction::LocalSet(b));
729
730    match op {
731        BinOp::Add => {
732            // Add can be number + number or string + string
733            // Check if both are numbers: tag check
734            // For now, assume numeric addition (the type checker should have validated)
735            f.instruction(&Instruction::LocalGet(a));
736            f.instruction(&Instruction::LocalGet(b));
737            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_ADD)));
738        }
739        BinOp::Sub => {
740            f.instruction(&Instruction::LocalGet(a));
741            f.instruction(&Instruction::LocalGet(b));
742            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_SUB)));
743        }
744        BinOp::Mul => {
745            f.instruction(&Instruction::LocalGet(a));
746            f.instruction(&Instruction::LocalGet(b));
747            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_MUL)));
748        }
749        BinOp::Div => {
750            f.instruction(&Instruction::LocalGet(a));
751            f.instruction(&Instruction::LocalGet(b));
752            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_DIV)));
753        }
754        BinOp::Mod => {
755            f.instruction(&Instruction::LocalGet(a));
756            f.instruction(&Instruction::LocalGet(b));
757            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_MOD)));
758        }
759        BinOp::Eq => {
760            f.instruction(&Instruction::LocalGet(a));
761            f.instruction(&Instruction::LocalGet(b));
762            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_EQ)));
763        }
764        BinOp::NotEq => {
765            // not(eq(a, b))
766            f.instruction(&Instruction::LocalGet(a));
767            f.instruction(&Instruction::LocalGet(b));
768            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_EQ)));
769            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NOT)));
770        }
771        BinOp::Less => {
772            f.instruction(&Instruction::LocalGet(a));
773            f.instruction(&Instruction::LocalGet(b));
774            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LT)));
775        }
776        BinOp::LessEq => {
777            f.instruction(&Instruction::LocalGet(a));
778            f.instruction(&Instruction::LocalGet(b));
779            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LE)));
780        }
781        BinOp::Greater => {
782            f.instruction(&Instruction::LocalGet(a));
783            f.instruction(&Instruction::LocalGet(b));
784            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_GT)));
785        }
786        BinOp::GreaterEq => {
787            f.instruction(&Instruction::LocalGet(a));
788            f.instruction(&Instruction::LocalGet(b));
789            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_GE)));
790        }
791        BinOp::And | BinOp::Or => unreachable!("handled above"),
792    }
793    Ok(())
794}
795
796fn emit_unary(
797    op: UnaryOp,
798    operand: &Expr,
799    ctx: &mut FuncContext,
800    f: &mut Function,
801) -> CodegenResult<()> {
802    emit_expr(operand, ctx, f)?;
803    match op {
804        UnaryOp::Neg => {
805            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NEG)));
806        }
807        UnaryOp::Not => {
808            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NOT)));
809        }
810    }
811    Ok(())
812}
813
814fn emit_result_unwrap(
815    inner: &Expr,
816    ctx: &mut FuncContext,
817    f: &mut Function,
818) -> CodegenResult<()> {
819    // `expr?` — if the result is an Err variant, trap; otherwise unwrap Ok's data.
820    emit_expr(inner, ctx, f)?;
821    let result_local = ctx.alloc_local(ValType::I32);
822    f.instruction(&Instruction::LocalSet(result_local));
823
824    // Get variant_id for "Err"
825    let err_id = ctx.get_variant_id("Err");
826
827    // Check: tag == TAG_VARIANT && variant_id == err_id → trap
828    f.instruction(&Instruction::LocalGet(result_local));
829    f.instruction(&Instruction::I32Load(memarg(0, 2))); // tag
830    f.instruction(&Instruction::I32Const(TAG_VARIANT));
831    f.instruction(&Instruction::I32Eq);
832    f.instruction(&Instruction::If(BlockType::Empty));
833    // It's a variant — check if it's Err
834    f.instruction(&Instruction::LocalGet(result_local));
835    f.instruction(&Instruction::I32Load(memarg(4, 2))); // variant_id (w1)
836    f.instruction(&Instruction::I32Const(err_id as i32));
837    f.instruction(&Instruction::I32Eq);
838    f.instruction(&Instruction::If(BlockType::Empty));
839    // Err → trap with "unwrap on Err!"
840    f.instruction(&Instruction::I32Const(ctx.data.unwrap_failed_ptr as i32));
841    f.instruction(&Instruction::I32Const(ctx.data.unwrap_failed_len as i32));
842    f.instruction(&Instruction::Call(IMPORT_TRAP));
843    f.instruction(&Instruction::Unreachable);
844    f.instruction(&Instruction::End); // end err check
845    f.instruction(&Instruction::End); // end variant check
846
847    // Ok → extract data payload at w2
848    f.instruction(&Instruction::LocalGet(result_local));
849    f.instruction(&Instruction::I32Load(memarg(8, 2))); // data_ptr (w2)
850    Ok(())
851}
852
853fn emit_nil_coalesce(
854    left: &Expr,
855    right: &Expr,
856    ctx: &mut FuncContext,
857    f: &mut Function,
858) -> CodegenResult<()> {
859    // `a ?? b` — if a is nil, return b; else return a
860    let left_local = ctx.alloc_local(ValType::I32);
861    emit_expr(left, ctx, f)?;
862    f.instruction(&Instruction::LocalTee(left_local));
863    // Check tag == TAG_NIL
864    f.instruction(&Instruction::I32Load(memarg(0, 2)));
865    f.instruction(&Instruction::I32Const(TAG_NIL));
866    f.instruction(&Instruction::I32Eq);
867    f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
868    emit_expr(right, ctx, f)?;
869    f.instruction(&Instruction::Else);
870    f.instruction(&Instruction::LocalGet(left_local));
871    f.instruction(&Instruction::End);
872    Ok(())
873}
874
875// ══════════════════════════════════════════════════════════════════════════════
876// Control Flow Expressions
877// ══════════════════════════════════════════════════════════════════════════════
878
879fn emit_if_expr(
880    if_expr: &IfExpr,
881    ctx: &mut FuncContext,
882    f: &mut Function,
883) -> CodegenResult<()> {
884    // Evaluate condition
885    emit_expr(&if_expr.condition, ctx, f)?;
886    // Extract bool value: load w1 (i32)
887    f.instruction(&Instruction::I32Load(memarg(4, 2)));
888
889    f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
890
891    // Then branch — execute stmts, the last expr is the value
892    emit_block_as_expr(&if_expr.then_block, ctx, f)?;
893
894    f.instruction(&Instruction::Else);
895
896    // Else branch
897    match &if_expr.else_branch {
898        Some(ElseBranch::Block(block)) => {
899            emit_block_as_expr(block, ctx, f)?;
900        }
901        Some(ElseBranch::ElseIf(elif)) => {
902            emit_if_expr(elif, ctx, f)?;
903        }
904        None => {
905            // No else → nil
906            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
907        }
908    }
909
910    f.instruction(&Instruction::End);
911    Ok(())
912}
913
914fn emit_for_expr(
915    for_expr: &ForExpr,
916    ctx: &mut FuncContext,
917    f: &mut Function,
918) -> CodegenResult<()> {
919    // Evaluate iterable → should be a list value
920    let list_local = ctx.alloc_local(ValType::I32);
921    let arr_local = ctx.alloc_local(ValType::I32);
922    let count_local = ctx.alloc_local(ValType::I32);
923    let i_local = ctx.alloc_local(ValType::I32);
924
925    emit_expr(&for_expr.iterable, ctx, f)?;
926    f.instruction(&Instruction::LocalSet(list_local));
927
928    // arr_ptr = list.w1
929    f.instruction(&Instruction::LocalGet(list_local));
930    f.instruction(&Instruction::I32Load(memarg(4, 2)));
931    f.instruction(&Instruction::LocalSet(arr_local));
932
933    // count = list.w2
934    f.instruction(&Instruction::LocalGet(list_local));
935    f.instruction(&Instruction::I32Load(memarg(8, 2)));
936    f.instruction(&Instruction::LocalSet(count_local));
937
938    // i = 0
939    f.instruction(&Instruction::I32Const(0));
940    f.instruction(&Instruction::LocalSet(i_local));
941
942    // The "item" local for each iteration
943    let item_local = ctx.alloc_local(ValType::I32);
944    let index_local = if for_expr.index.is_some() {
945        Some(ctx.alloc_local(ValType::I32))
946    } else {
947        None
948    };
949
950    // Register item binding
951    ctx.push_local(&for_expr.item.name, item_local);
952    if let (Some(idx_ident), Some(idx_local)) = (&for_expr.index, index_local) {
953        ctx.push_local(&idx_ident.name, idx_local);
954    }
955
956    f.instruction(&Instruction::Block(BlockType::Empty));
957    f.instruction(&Instruction::Loop(BlockType::Empty));
958
959    // Gas tick at loop boundary
960    gas::emit_gas_tick(f, ctx.data.gas_exhausted_ptr, ctx.data.gas_exhausted_len);
961
962    // break if i >= count
963    f.instruction(&Instruction::LocalGet(i_local));
964    f.instruction(&Instruction::LocalGet(count_local));
965    f.instruction(&Instruction::I32GeU);
966    f.instruction(&Instruction::BrIf(1));
967
968    // item = arr[i]
969    f.instruction(&Instruction::LocalGet(arr_local));
970    f.instruction(&Instruction::LocalGet(i_local));
971    f.instruction(&Instruction::I32Const(4));
972    f.instruction(&Instruction::I32Mul);
973    f.instruction(&Instruction::I32Add);
974    f.instruction(&Instruction::I32Load(memarg(0, 2)));
975    f.instruction(&Instruction::LocalSet(item_local));
976
977    // index = i (as number value)
978    if let Some(idx_local) = index_local {
979        // Create a number value from i
980        f.instruction(&Instruction::I32Const(VALUE_SIZE as i32));
981        f.instruction(&Instruction::Call(rt_func_idx(RT_ALLOC)));
982        f.instruction(&Instruction::LocalTee(idx_local));
983        f.instruction(&Instruction::I32Const(TAG_NUMBER));
984        f.instruction(&Instruction::I32Store(memarg(0, 2)));
985        f.instruction(&Instruction::LocalGet(idx_local));
986        f.instruction(&Instruction::LocalGet(i_local));
987        f.instruction(&Instruction::F64ConvertI32U);
988        f.instruction(&Instruction::F64Store(memarg(4, 3)));
989    }
990
991    // Execute body
992    emit_stmts(&for_expr.body.stmts, ctx, f)?;
993
994    // i += 1
995    f.instruction(&Instruction::LocalGet(i_local));
996    f.instruction(&Instruction::I32Const(1));
997    f.instruction(&Instruction::I32Add);
998    f.instruction(&Instruction::LocalSet(i_local));
999    f.instruction(&Instruction::Br(0));
1000
1001    f.instruction(&Instruction::End); // end loop
1002    f.instruction(&Instruction::End); // end block
1003
1004    // Pop bindings
1005    ctx.pop_local(&for_expr.item.name);
1006    if let Some(idx_ident) = &for_expr.index {
1007        ctx.pop_local(&idx_ident.name);
1008    }
1009
1010    // For-expr evaluates to nil
1011    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
1012    Ok(())
1013}
1014
1015fn emit_match_expr(
1016    match_expr: &MatchExpr,
1017    ctx: &mut FuncContext,
1018    f: &mut Function,
1019) -> CodegenResult<()> {
1020    // Evaluate subject
1021    let subj_local = ctx.alloc_local(ValType::I32);
1022    emit_expr(&match_expr.subject, ctx, f)?;
1023    f.instruction(&Instruction::LocalSet(subj_local));
1024
1025    // For now, emit a simple if/else chain testing each arm's pattern.
1026    // Each arm: if pattern matches → execute body, else try next.
1027    // We wrap in a block so we can br out when a match is found.
1028
1029    let result_local = ctx.alloc_local(ValType::I32);
1030    // Default: nil
1031    f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
1032    f.instruction(&Instruction::LocalSet(result_local));
1033
1034    // Wrap all arms in a block for early exit
1035    f.instruction(&Instruction::Block(BlockType::Empty));
1036
1037    for arm in &match_expr.arms {
1038        match &arm.pattern {
1039            Pattern::Wildcard(_) => {
1040                // Always matches — execute body and break
1041                match &arm.body {
1042                    MatchArmBody::Expr(expr) => {
1043                        emit_expr(expr, ctx, f)?;
1044                    }
1045                    MatchArmBody::Block(block) => {
1046                        emit_block_as_expr(block, ctx, f)?;
1047                    }
1048                }
1049                f.instruction(&Instruction::LocalSet(result_local));
1050                f.instruction(&Instruction::Br(0));
1051            }
1052            Pattern::Variant { name, bindings } => {
1053                // Check if subject is a VARIANT with matching variant_id
1054                let vid = ctx.get_variant_id(&name.name);
1055
1056                // Load subject tag
1057                f.instruction(&Instruction::LocalGet(subj_local));
1058                f.instruction(&Instruction::I32Load(memarg(0, 2)));
1059                f.instruction(&Instruction::I32Const(TAG_VARIANT));
1060                f.instruction(&Instruction::I32Eq);
1061                f.instruction(&Instruction::If(BlockType::Empty));
1062
1063                // Load variant_id (w1)
1064                f.instruction(&Instruction::LocalGet(subj_local));
1065                f.instruction(&Instruction::I32Load(memarg(4, 2)));
1066                f.instruction(&Instruction::I32Const(vid as i32));
1067                f.instruction(&Instruction::I32Eq);
1068                f.instruction(&Instruction::If(BlockType::Empty));
1069
1070                // Bind destructured fields
1071                // The variant data is a record at w2
1072                if !bindings.is_empty() {
1073                    let data_local = ctx.alloc_local(ValType::I32);
1074                    f.instruction(&Instruction::LocalGet(subj_local));
1075                    f.instruction(&Instruction::I32Load(memarg(8, 2)));
1076                    f.instruction(&Instruction::LocalSet(data_local));
1077
1078                    for (bi, binding) in bindings.iter().enumerate() {
1079                        let bind_local = ctx.alloc_local(ValType::I32);
1080                        // Access by index from the data record
1081                        f.instruction(&Instruction::LocalGet(data_local));
1082                        f.instruction(&Instruction::I32Const(bi as i32));
1083                        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST_GET)));
1084                        f.instruction(&Instruction::LocalSet(bind_local));
1085                        ctx.push_local(&binding.name, bind_local);
1086                    }
1087                }
1088
1089                // Execute body
1090                match &arm.body {
1091                    MatchArmBody::Expr(expr) => {
1092                        emit_expr(expr, ctx, f)?;
1093                    }
1094                    MatchArmBody::Block(block) => {
1095                        emit_block_as_expr(block, ctx, f)?;
1096                    }
1097                }
1098                f.instruction(&Instruction::LocalSet(result_local));
1099
1100                // Pop bindings
1101                for binding in bindings.iter().rev() {
1102                    ctx.pop_local(&binding.name);
1103                }
1104
1105                f.instruction(&Instruction::Br(2)); // break outer block
1106
1107                f.instruction(&Instruction::End); // end variant_id check
1108                f.instruction(&Instruction::End); // end tag check
1109            }
1110        }
1111    }
1112
1113    f.instruction(&Instruction::End); // end outer block
1114
1115    f.instruction(&Instruction::LocalGet(result_local));
1116    Ok(())
1117}
1118
1119// ══════════════════════════════════════════════════════════════════════════════
1120// Helpers
1121// ══════════════════════════════════════════════════════════════════════════════
1122
1123/// Emit a block's statements and leave the last expression's value on the stack.
1124/// If the block has no statements, pushes nil.
1125pub fn emit_block_as_expr(
1126    block: &Block,
1127    ctx: &mut FuncContext,
1128    f: &mut Function,
1129) -> CodegenResult<()> {
1130    if block.stmts.is_empty() {
1131        f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
1132        return Ok(());
1133    }
1134
1135    // Emit all but the last statement normally
1136    let (last, rest) = block.stmts.split_last().unwrap();
1137    emit_stmts(rest, ctx, f)?;
1138
1139    // The last statement: if it's an Expr statement, leave value on stack
1140    match last {
1141        Stmt::Expr(expr_stmt) => {
1142            emit_expr(&expr_stmt.expr, ctx, f)?;
1143        }
1144        _ => {
1145            // Emit the statement normally, then push nil as the block value
1146            emit_stmts(std::slice::from_ref(last), ctx, f)?;
1147            f.instruction(&Instruction::Call(rt_func_idx(RT_VAL_NIL)));
1148        }
1149    }
1150    Ok(())
1151}
1152
1153/// Create a `MemArg`.
1154fn memarg(offset: u64, align: u32) -> wasm_encoder::MemArg {
1155    wasm_encoder::MemArg {
1156        offset,
1157        align,
1158        memory_index: 0,
1159    }
1160}