Skip to main content

pepl_codegen/
compiler.rs

1//! Main WASM module assembler.
2//!
3//! Orchestrates the code generation pipeline:
4//! 1. Analyse the AST and collect metadata (state fields, actions, views, etc.)
5//! 2. Build the data segment (intern string constants)
6//! 3. Emit runtime helper functions
7//! 4. Emit space-level functions (init, dispatch, render, get_state, …)
8//! 5. Assemble all WASM sections into a valid module
9//! 6. Validate with `wasmparser`
10
11use std::collections::HashMap;
12
13use pepl_types::ast::*;
14use wasm_encoder::{
15    CodeSection, ConstExpr, CustomSection, DataSection, ElementSection, Elements,
16    EntityType, ExportKind, ExportSection, Function, FunctionSection, GlobalSection,
17    GlobalType, ImportSection, Instruction, MemorySection, MemoryType, Module,
18    RefType, TableSection, TableType, TypeSection, ValType,
19};
20
21use crate::error::{CodegenError, CodegenResult};
22use crate::runtime::{
23    self, memarg, rt_func_idx, DataSegmentTracker, RT_FUNC_COUNT, RT_VAL_LIST_GET,
24    RT_VAL_RECORD_GET,
25};
26use crate::source_map::{FuncKind, SourceMap};
27use crate::types::*;
28
29// ══════════════════════════════════════════════════════════════════════════════
30// Public API
31// ══════════════════════════════════════════════════════════════════════════════
32
33/// Compile a validated PEPL [`Program`] into a `.wasm` binary.
34///
35/// Returns the raw bytes of a valid WebAssembly module on success, or a
36/// [`CodegenError`] describing what went wrong.
37pub fn compile(program: &Program) -> CodegenResult<Vec<u8>> {
38    let mut compiler = Compiler::new(program);
39    let (wasm, _source_map) = compiler.compile()?;
40    Ok(wasm)
41}
42
43/// Compile a validated PEPL [`Program`] and return both the WASM binary
44/// and a [`SourceMap`] mapping WASM function indices to PEPL source spans.
45pub fn compile_with_source_map(program: &Program) -> CodegenResult<(Vec<u8>, SourceMap)> {
46    let mut compiler = Compiler::new(program);
47    compiler.compile()
48}
49
50// ══════════════════════════════════════════════════════════════════════════════
51// Compiler
52// ══════════════════════════════════════════════════════════════════════════════
53
54/// The top-level compiler state.
55struct Compiler<'a> {
56    program: &'a Program,
57    /// Data segment tracker for string constants.
58    data: DataSegmentTracker,
59    /// Extra user string data (appended after well-known strings).
60    user_data: Vec<u8>,
61
62    // ── Metadata ─────────────────────────────────────────────────────────
63    /// State field names in declaration order.
64    state_field_names: Vec<String>,
65    /// Action names in declaration order (index = action_id).
66    action_names: Vec<String>,
67    /// View names in declaration order (index = view_id).
68    view_names: Vec<String>,
69    /// Variant names → numeric id (for match codegen).
70    variant_ids: HashMap<String, u32>,
71    /// Functions registered by name → absolute WASM function index.
72    function_table: HashMap<String, u32>,
73    /// Lambda bodies collected during codegen for deferred compilation.
74    /// Each entry: (params, body, captured_var_names)
75    lambda_bodies: Vec<LambdaBody>,
76    /// Number of space-level functions (init, dispatch, render, state, etc.)
77    num_space_funcs: u32,
78    /// Number of test functions emitted (__test_0, __test_1, … + __test_count).
79    num_test_funcs: u32,
80    /// Source map built during codegen.
81    source_map: SourceMap,
82}
83
84/// A lambda body collected during expression codegen for deferred compilation.
85#[derive(Clone)]
86pub struct LambdaBody {
87    pub params: Vec<pepl_types::ast::Param>,
88    pub body: pepl_types::ast::Block,
89    pub captured: Vec<String>,
90}
91
92impl<'a> Compiler<'a> {
93    fn new(program: &'a Program) -> Self {
94        Self {
95            program,
96            data: DataSegmentTracker::new(),
97            user_data: Vec::new(),
98            state_field_names: Vec::new(),
99            action_names: Vec::new(),
100            view_names: Vec::new(),
101            variant_ids: HashMap::new(),
102            function_table: HashMap::new(),
103            lambda_bodies: Vec::new(),
104            num_space_funcs: 0,
105            num_test_funcs: 0,
106            source_map: SourceMap::new(),
107        }
108    }
109
110    /// Run the full compilation pipeline.
111    fn compile(&mut self) -> CodegenResult<(Vec<u8>, SourceMap)> {
112        self.collect_metadata();
113
114        let mut module = Module::new();
115
116        // 1. Type section
117        let types = self.emit_types();
118        module.section(&types);
119
120        // 2. Import section
121        let imports = self.emit_imports();
122        module.section(&imports);
123
124        // 3. Function section + Code section (built together)
125        let (func_section, code_section) = self.emit_functions()?;
126        module.section(&func_section);
127
128        // 3b. Table section (for call_indirect / lambda support)
129        let table = self.emit_table();
130        module.section(&table);
131
132        // 4. Memory section
133        let memory = self.emit_memory();
134        module.section(&memory);
135
136        // 5. Global section
137        let globals = self.emit_globals();
138        module.section(&globals);
139
140        // 6. Export section
141        let exports = self.emit_exports();
142        module.section(&exports);
143
144        // 6b. Element section (populate table with lambda function refs)
145        let elements = self.emit_elements();
146        module.section(&elements);
147
148        // 7. Code section (must come after function/memory/global/export)
149        module.section(&code_section);
150
151        // 8. Data section
152        let data_sec = self.emit_data();
153        module.section(&data_sec);
154
155        // 9. Custom section (PEPL metadata)
156        let custom = self.emit_custom();
157        module.section(&custom);
158
159        // 9b. Custom section (source map)
160        let sm_bytes = self.source_map.to_json();
161        let sm_custom = CustomSection {
162            name: std::borrow::Cow::Borrowed("pepl_source_map"),
163            data: std::borrow::Cow::Owned(sm_bytes),
164        };
165        module.section(&sm_custom);
166
167        let wasm_bytes = module.finish();
168
169        // 10. Validate
170        wasmparser::validate(&wasm_bytes).map_err(|e| {
171            CodegenError::ValidationFailed(format!("{e}"))
172        })?;
173
174        Ok((wasm_bytes, self.source_map.clone()))
175    }
176
177    // ── Metadata collection ──────────────────────────────────────────────
178
179    fn collect_metadata(&mut self) {
180        let body = &self.program.space.body;
181
182        // State fields
183        for field in &body.state.fields {
184            self.state_field_names.push(field.name.name.clone());
185        }
186
187        // Also add derived fields as state fields (they live on the state record)
188        if let Some(derived) = &body.derived {
189            for field in &derived.fields {
190                self.state_field_names.push(field.name.name.clone());
191            }
192        }
193
194        // Actions
195        for action in &body.actions {
196            self.action_names.push(action.name.name.clone());
197        }
198
199        // Views
200        for view in &body.views {
201            self.view_names.push(view.name.name.clone());
202        }
203
204        // Variant IDs from type declarations
205        let mut vid = 0u32;
206        for type_decl in &body.types {
207            if let TypeDeclBody::SumType(variants) = &type_decl.body {
208                for variant in variants {
209                    self.variant_ids.insert(variant.name.name.clone(), vid);
210                    vid += 1;
211                }
212            }
213        }
214    }
215
216    // ── Type section ─────────────────────────────────────────────────────
217
218    fn emit_types(&self) -> TypeSection {
219        let mut types = TypeSection::new();
220
221        // TYPE_VOID_VOID: () -> ()
222        types.ty().function(vec![], vec![]);
223        // TYPE_VOID_I32: () -> i32
224        types.ty().function(vec![], vec![ValType::I32]);
225        // TYPE_I32_VOID: (i32) -> ()
226        types.ty().function(vec![ValType::I32], vec![]);
227        // TYPE_I32_I32: (i32) -> i32
228        types.ty().function(vec![ValType::I32], vec![ValType::I32]);
229        // TYPE_I32X2_VOID: (i32, i32) -> ()
230        types.ty().function(vec![ValType::I32, ValType::I32], vec![]);
231        // TYPE_I32X2_I32: (i32, i32) -> i32
232        types
233            .ty()
234            .function(vec![ValType::I32, ValType::I32], vec![ValType::I32]);
235        // TYPE_I32X3_I32: (i32, i32, i32) -> i32
236        types.ty().function(
237            vec![ValType::I32, ValType::I32, ValType::I32],
238            vec![ValType::I32],
239        );
240        // TYPE_F64_I32: (f64) -> i32
241        types.ty().function(vec![ValType::F64], vec![ValType::I32]);
242        // TYPE_I32_F64_VOID: (i32, f64) -> ()
243        types
244            .ty()
245            .function(vec![ValType::I32, ValType::F64], vec![]);
246        // TYPE_VOID_I64: () -> i64
247        types.ty().function(vec![], vec![ValType::I64]);
248        // TYPE_I32X3_VOID: (i32, i32, i32) -> ()
249        types.ty().function(
250            vec![ValType::I32, ValType::I32, ValType::I32],
251            vec![],
252        );
253
254        types
255    }
256
257    // ── Import section ───────────────────────────────────────────────────
258
259    fn emit_imports(&self) -> ImportSection {
260        let mut imports = ImportSection::new();
261
262        // IMPORT_HOST_CALL: env.host_call(cap_id, fn_id, args_ptr) -> i32
263        imports.import("env", "host_call", EntityType::Function(TYPE_I32X3_I32));
264        // IMPORT_LOG: env.log(ptr, len)
265        imports.import("env", "log", EntityType::Function(TYPE_I32X2_VOID));
266        // IMPORT_TRAP: env.trap(ptr, len)
267        imports.import("env", "trap", EntityType::Function(TYPE_I32X2_VOID));
268        // IMPORT_GET_TIMESTAMP: env.get_timestamp() -> i64
269        imports.import("env", "get_timestamp", EntityType::Function(TYPE_VOID_I64));
270
271        imports
272    }
273
274    // ── Memory section ───────────────────────────────────────────────────
275
276    fn emit_memory(&self) -> MemorySection {
277        let mut memory = MemorySection::new();
278        memory.memory(MemoryType {
279            minimum: INITIAL_MEMORY_PAGES,
280            maximum: Some(MAX_MEMORY_PAGES),
281            memory64: false,
282            shared: false,
283            page_size_log2: None,
284        });
285        memory
286    }
287
288    // ── Global section ───────────────────────────────────────────────────
289
290    fn emit_globals(&self) -> GlobalSection {
291        let mut globals = GlobalSection::new();
292
293        // GLOBAL_HEAP_PTR — starts after data segment
294        globals.global(
295            GlobalType {
296                val_type: ValType::I32,
297                mutable: true,
298                shared: false,
299            },
300            &ConstExpr::i32_const(HEAP_START as i32),
301        );
302
303        // GLOBAL_GAS
304        globals.global(
305            GlobalType {
306                val_type: ValType::I32,
307                mutable: true,
308                shared: false,
309            },
310            &ConstExpr::i32_const(0),
311        );
312
313        // GLOBAL_GAS_LIMIT
314        globals.global(
315            GlobalType {
316                val_type: ValType::I32,
317                mutable: true,
318                shared: false,
319            },
320            &ConstExpr::i32_const(1_000_000),
321        );
322
323        // GLOBAL_STATE_PTR
324        globals.global(
325            GlobalType {
326                val_type: ValType::I32,
327                mutable: true,
328                shared: false,
329            },
330            &ConstExpr::i32_const(0),
331        );
332
333        globals
334    }
335
336    // ── Function + Code sections ─────────────────────────────────────────
337
338    fn emit_functions(&mut self) -> CodegenResult<(FunctionSection, CodeSection)> {
339        let mut func_section = FunctionSection::new();
340        let mut code_section = CodeSection::new();
341
342        // ── Runtime helpers ──────────────────────────────────────────────
343        // Each runtime function is registered with its type index.
344
345        // RT_ALLOC
346        func_section.function(TYPE_I32_I32);
347        code_section.function(&runtime::emit_alloc(
348            self.data.oom_ptr,
349            self.data.oom_len,
350        ));
351
352        // RT_VAL_NIL
353        func_section.function(TYPE_VOID_I32);
354        code_section.function(&runtime::emit_val_nil());
355
356        // RT_VAL_NUMBER (i32, i32) -> i32
357        func_section.function(TYPE_I32X2_I32);
358        code_section.function(&runtime::emit_val_number());
359
360        // RT_VAL_BOOL (i32) -> i32
361        func_section.function(TYPE_I32_I32);
362        code_section.function(&runtime::emit_val_bool());
363
364        // RT_VAL_STRING (i32, i32) -> i32
365        func_section.function(TYPE_I32X2_I32);
366        code_section.function(&runtime::emit_val_string());
367
368        // RT_VAL_LIST (i32, i32) -> i32
369        func_section.function(TYPE_I32X2_I32);
370        code_section.function(&runtime::emit_val_list());
371
372        // RT_VAL_RECORD (i32, i32) -> i32
373        func_section.function(TYPE_I32X2_I32);
374        code_section.function(&runtime::emit_val_record());
375
376        // RT_VAL_VARIANT (i32, i32) -> i32
377        func_section.function(TYPE_I32X2_I32);
378        code_section.function(&runtime::emit_val_variant());
379
380        // RT_VAL_ACTION_REF (i32) -> i32
381        func_section.function(TYPE_I32_I32);
382        code_section.function(&runtime::emit_val_action_ref());
383
384        // RT_VAL_TAG (i32) -> i32
385        func_section.function(TYPE_I32_I32);
386        code_section.function(&runtime::emit_val_tag());
387
388        // RT_VAL_GET_NUMBER (i32) -> i32 (returns lo bits)
389        func_section.function(TYPE_I32_I32);
390        code_section.function(&runtime::emit_val_get_number());
391
392        // RT_VAL_GET_W1 (i32) -> i32
393        func_section.function(TYPE_I32_I32);
394        code_section.function(&runtime::emit_val_get_w1());
395
396        // RT_VAL_GET_W2 (i32) -> i32
397        func_section.function(TYPE_I32_I32);
398        code_section.function(&runtime::emit_val_get_w2());
399
400        // RT_VAL_EQ (i32, i32) -> i32
401        func_section.function(TYPE_I32X2_I32);
402        code_section.function(&runtime::emit_val_eq());
403
404        // RT_VAL_TO_STRING (i32) -> i32
405        func_section.function(TYPE_I32_I32);
406        code_section.function(&runtime::emit_val_to_string(&self.data));
407
408        // RT_VAL_STRING_CONCAT (i32, i32) -> i32
409        func_section.function(TYPE_I32X2_I32);
410        code_section.function(&runtime::emit_val_string_concat());
411
412        // RT_VAL_ADD through RT_VAL_MOD
413        func_section.function(TYPE_I32X2_I32);
414        code_section.function(&runtime::emit_val_add());
415        func_section.function(TYPE_I32X2_I32);
416        code_section.function(&runtime::emit_val_sub());
417        func_section.function(TYPE_I32X2_I32);
418        code_section.function(&runtime::emit_val_mul());
419        func_section.function(TYPE_I32X2_I32);
420        code_section.function(&runtime::emit_val_div(
421            self.data.div_by_zero_ptr,
422            self.data.div_by_zero_len,
423        ));
424        func_section.function(TYPE_I32X2_I32);
425        code_section.function(&runtime::emit_val_mod());
426
427        // RT_VAL_NEG (i32) -> i32
428        func_section.function(TYPE_I32_I32);
429        code_section.function(&runtime::emit_val_neg());
430
431        // RT_VAL_NOT (i32) -> i32
432        func_section.function(TYPE_I32_I32);
433        code_section.function(&runtime::emit_val_not());
434
435        // RT_VAL_LT, LE, GT, GE (i32, i32) -> i32
436        func_section.function(TYPE_I32X2_I32);
437        code_section.function(&runtime::emit_val_lt());
438        func_section.function(TYPE_I32X2_I32);
439        code_section.function(&runtime::emit_val_le());
440        func_section.function(TYPE_I32X2_I32);
441        code_section.function(&runtime::emit_val_gt());
442        func_section.function(TYPE_I32X2_I32);
443        code_section.function(&runtime::emit_val_ge());
444
445        // RT_VAL_RECORD_GET (i32, i32, i32) -> i32
446        func_section.function(TYPE_I32X3_I32);
447        code_section.function(&runtime::emit_val_record_get());
448
449        // RT_VAL_LIST_GET (i32, i32) -> i32
450        func_section.function(TYPE_I32X2_I32);
451        code_section.function(&runtime::emit_val_list_get());
452
453        // RT_CHECK_NAN (i32) -> i32
454        func_section.function(TYPE_I32_I32);
455        code_section.function(&runtime::emit_check_nan(
456            self.data.nan_ptr,
457            self.data.nan_len,
458        ));
459
460        // RT_MEMCMP (i32, i32, i32) -> i32
461        func_section.function(TYPE_I32X3_I32);
462        code_section.function(&runtime::emit_memcmp());
463
464        // ── Space-level functions ────────────────────────────────────────
465        let body = &self.program.space.body;
466
467        // init() -> void  (parameterless — gas limit set to default constant)
468        let init_idx = IMPORT_COUNT + RT_FUNC_COUNT;
469        func_section.function(TYPE_VOID_VOID);
470        let mut init_scratch = Function::new(vec![]);
471        let mut init_ctx = self.make_func_context(0); // 0 params
472        crate::space::emit_init(
473            &body.state,
474            body.derived.as_ref(),
475            &mut init_ctx,
476            &mut init_scratch,
477        )?;
478        self.merge_user_data(&init_ctx);
479        code_section.function(&Self::finalize_function(init_scratch, &init_ctx));
480        self.source_map.push(init_idx, "init", FuncKind::SpaceInfra, body.state.span);
481
482        // dispatch_action(action_id: i32, payload_ptr: i32, payload_len: i32) -> void
483        let dispatch_idx = init_idx + 1;
484        func_section.function(TYPE_I32X3_VOID);
485        let mut dispatch_scratch = Function::new(vec![]);
486        let mut dispatch_ctx = self.make_func_context(3); // 3 params
487        crate::space::emit_dispatch_action(
488            &body.actions,
489            &body.invariants,
490            body.derived.as_ref(),
491            &mut dispatch_ctx,
492            &mut dispatch_scratch,
493        )?;
494        self.merge_user_data(&dispatch_ctx);
495        code_section.function(&Self::finalize_function(dispatch_scratch, &dispatch_ctx));
496        self.source_map.push(dispatch_idx, "dispatch_action", FuncKind::SpaceInfra, body.span);
497        // Also map individual actions by name
498        for (ai, action) in body.actions.iter().enumerate() {
499            self.source_map.push(dispatch_idx, &action.name.name, FuncKind::Action, action.span);
500            let _ = ai; // action_id = ai, embedded in dispatch
501        }
502
503        // render(view_id: i32) -> i32
504        let render_idx = dispatch_idx + 1;
505        func_section.function(TYPE_I32_I32);
506        let mut render_scratch = Function::new(vec![]);
507        let mut render_ctx = self.make_func_context(1);
508        crate::space::emit_render(&body.views, &mut render_ctx, &mut render_scratch)?;
509        self.merge_user_data(&render_ctx);
510        code_section.function(&Self::finalize_function(render_scratch, &render_ctx));
511        self.source_map.push(render_idx, "render", FuncKind::SpaceInfra, body.span);
512        for view in &body.views {
513            self.source_map.push(render_idx, &view.name.name, FuncKind::View, view.span);
514        }
515
516        // get_state() -> i32
517        let get_state_idx = render_idx + 1;
518        func_section.function(TYPE_VOID_I32);
519        let mut get_state_func = Function::new(vec![]);
520        crate::space::emit_get_state(&mut get_state_func);
521        // get_state has no extra locals, so no finalization needed
522        code_section.function(&get_state_func);
523
524        // alloc(size: i32) -> i32 (re-export of RT_ALLOC for host use)
525        // Already part of runtime, we'll just export RT_ALLOC directly.
526
527        // dealloc(ptr: i32, size: i32) -> void (no-op for bump allocator)
528        let dealloc_idx = get_state_idx + 1;
529        func_section.function(TYPE_I32X2_VOID);
530        let mut dealloc_func = Function::new(vec![]);
531        // Bump allocator — dealloc is intentionally a no-op.
532        // ptr (param 0) and size (param 1) are accepted but ignored.
533        dealloc_func.instruction(&Instruction::End);
534        code_section.function(&dealloc_func);
535
536        // Conditionally: update(dt_ptr: i32)
537        let mut next_idx = dealloc_idx + 1;
538        if let Some(update_decl) = &body.update {
539            self.function_table
540                .insert("update".to_string(), next_idx);
541            func_section.function(TYPE_I32_VOID);
542            let mut update_scratch = Function::new(vec![]);
543            let mut update_ctx = self.make_func_context(1);
544            crate::space::emit_update(
545                update_decl,
546                body.derived.as_ref(),
547                &mut update_ctx,
548                &mut update_scratch,
549            )?;
550            self.merge_user_data(&update_ctx);
551            code_section.function(&Self::finalize_function(update_scratch, &update_ctx));
552            self.source_map.push(next_idx, "update", FuncKind::Update, update_decl.span);
553            next_idx += 1;
554        }
555
556        // Conditionally: handle_event(event_ptr: i32)
557        if let Some(handle_event_decl) = &body.handle_event {
558            self.function_table
559                .insert("handle_event".to_string(), next_idx);
560            func_section.function(TYPE_I32_VOID);
561            let mut he_scratch = Function::new(vec![]);
562            let mut he_ctx = self.make_func_context(1);
563            crate::space::emit_handle_event(
564                handle_event_decl,
565                body.derived.as_ref(),
566                &mut he_ctx,
567                &mut he_scratch,
568            )?;
569            self.merge_user_data(&he_ctx);
570            code_section.function(&Self::finalize_function(he_scratch, &he_ctx));
571            self.source_map.push(next_idx, "handle_event", FuncKind::HandleEvent, handle_event_decl.span);
572            next_idx += 1;
573        }
574
575        // Track how many space-level functions we emitted
576        self.num_space_funcs = next_idx - (IMPORT_COUNT + RT_FUNC_COUNT);
577
578        // invoke_lambda(lambda_ptr: i32, arg_ptr: i32) -> i32
579        // This function reads the LAMBDA value, extracts table index + env,
580        // and does call_indirect to execute the lambda body.
581        let invoke_lambda_idx = next_idx;
582        func_section.function(TYPE_I32X2_I32);
583        let mut invoke_func = Function::new(vec![
584            (1, ValType::I32), // local 2: table_idx (from lambda.w1)
585            (1, ValType::I32), // local 3: env_ptr   (from lambda.w2)
586        ]);
587        // table_idx = lambda_ptr.w1
588        invoke_func.instruction(&Instruction::LocalGet(0));
589        invoke_func.instruction(&Instruction::I32Load(memarg(4, 2)));
590        invoke_func.instruction(&Instruction::LocalSet(2));
591        // env_ptr = lambda_ptr.w2
592        invoke_func.instruction(&Instruction::LocalGet(0));
593        invoke_func.instruction(&Instruction::I32Load(memarg(8, 2)));
594        invoke_func.instruction(&Instruction::LocalSet(3));
595        // call_indirect(env_ptr, arg_ptr, table_idx)
596        // Function type for lambda: (env_ptr: i32, arg_ptr: i32) -> i32 = TYPE_I32X2_I32
597        invoke_func.instruction(&Instruction::LocalGet(3)); // env_ptr
598        invoke_func.instruction(&Instruction::LocalGet(1)); // arg_ptr
599        invoke_func.instruction(&Instruction::LocalGet(2)); // table_idx
600        invoke_func.instruction(&Instruction::CallIndirect { type_index: TYPE_I32X2_I32, table_index: 0 });
601        invoke_func.instruction(&Instruction::End);
602        code_section.function(&invoke_func);
603        self.function_table
604            .insert("invoke_lambda".to_string(), invoke_lambda_idx);
605        self.source_map.push(invoke_lambda_idx, "invoke_lambda", FuncKind::InvokeLambda, self.program.space.body.span);
606        let mut _next_idx = next_idx + 1;
607
608        // Now compile deferred lambda bodies
609        // Each lambda has signature: (env_ptr: i32, arg_ptr: i32) -> i32
610        let lambda_bodies = self.lambda_bodies.clone();
611        for lb in &lambda_bodies {
612            func_section.function(TYPE_I32X2_I32);
613            let mut lam_scratch = Function::new(vec![]);
614            // Lambda function params: local 0 = env_ptr, local 1 = arg_ptr
615            let mut lam_ctx = self.make_func_context(2);
616
617            // Bind captured variables from env (a RECORD at local 0)
618            for cap_name in &lb.captured {
619                let cap_local = lam_ctx.alloc_local(ValType::I32);
620                let (cap_key_ptr, cap_key_len) = lam_ctx.intern_string(cap_name);
621                lam_scratch.instruction(&Instruction::LocalGet(0)); // env_ptr
622                lam_scratch.instruction(&Instruction::I32Const(cap_key_ptr as i32));
623                lam_scratch.instruction(&Instruction::I32Const(cap_key_len as i32));
624                lam_scratch.instruction(&Instruction::Call(rt_func_idx(RT_VAL_RECORD_GET)));
625                lam_scratch.instruction(&Instruction::LocalSet(cap_local));
626                lam_ctx.push_local(cap_name, cap_local);
627            }
628
629            // Bind lambda parameters from arg_ptr
630            // For single-param lambda: arg_ptr IS the argument value
631            // For multi-param: arg_ptr is a LIST, unpack via RT_VAL_LIST_GET
632            if lb.params.len() == 1 {
633                let param_local = lam_ctx.alloc_local(ValType::I32);
634                lam_scratch.instruction(&Instruction::LocalGet(1)); // arg_ptr
635                lam_scratch.instruction(&Instruction::LocalSet(param_local));
636                lam_ctx.push_local(&lb.params[0].name.name, param_local);
637            } else {
638                for (pi, param) in lb.params.iter().enumerate() {
639                    let param_local = lam_ctx.alloc_local(ValType::I32);
640                    lam_scratch.instruction(&Instruction::LocalGet(1)); // args list
641                    lam_scratch.instruction(&Instruction::I32Const(pi as i32));
642                    lam_scratch.instruction(&Instruction::Call(rt_func_idx(RT_VAL_LIST_GET)));
643                    lam_scratch.instruction(&Instruction::LocalSet(param_local));
644                    lam_ctx.push_local(&param.name.name, param_local);
645                }
646            }
647
648            // Emit lambda body (block of statements → last expr value)
649            crate::expr::emit_block_as_expr(&lb.body, &mut lam_ctx, &mut lam_scratch)?;
650            lam_scratch.instruction(&Instruction::End);
651
652            self.merge_user_data(&lam_ctx);
653            code_section.function(&Self::finalize_function(lam_scratch, &lam_ctx));
654            _next_idx += 1;
655        }
656
657        // ── Test functions ───────────────────────────────────────────────
658        // Flatten all test cases across all tests { } blocks.
659        let all_cases: Vec<&TestCase> = self
660            .program
661            .tests
662            .iter()
663            .flat_map(|tb| &tb.cases)
664            .collect();
665
666        if !all_cases.is_empty() {
667            // Build action name → action_id map
668            let actions_map: std::collections::HashMap<String, u32> = self
669                .action_names
670                .iter()
671                .enumerate()
672                .map(|(i, name)| (name.clone(), i as u32))
673                .collect();
674
675            let init_func_idx = IMPORT_COUNT + RT_FUNC_COUNT; // init is the first space func
676            let dispatch_func_idx = init_func_idx + 1;
677
678            for (ti, tc) in all_cases.iter().enumerate() {
679                let test_func_idx = _next_idx;
680                func_section.function(TYPE_VOID_VOID);
681                let mut test_scratch = Function::new(vec![]);
682                let mut test_ctx = self.make_func_context(0);
683                crate::test_codegen::emit_test_body(
684                    &tc.body,
685                    &actions_map,
686                    dispatch_func_idx,
687                    init_func_idx,
688                    &mut test_ctx,
689                    &mut test_scratch,
690                )?;
691                test_scratch.instruction(&Instruction::End);
692                self.merge_user_data(&test_ctx);
693                code_section.function(&Self::finalize_function(test_scratch, &test_ctx));
694                self.source_map.push(test_func_idx, format!("__test_{ti}"), FuncKind::Test, tc.span);
695                _next_idx += 1;
696            }
697
698            // __test_count() -> i32
699            func_section.function(TYPE_VOID_I32);
700            code_section.function(&crate::test_codegen::emit_test_count(all_cases.len()));
701            self.source_map.push(_next_idx, "__test_count", FuncKind::TestCount, self.program.space.span);
702            _next_idx += 1;
703
704            self.num_test_funcs = all_cases.len() as u32 + 1; // +1 for __test_count
705        }
706
707        Ok((func_section, code_section))
708    }
709
710    // ── Export section ────────────────────────────────────────────────────
711
712    fn emit_exports(&self) -> ExportSection {
713        let mut exports = ExportSection::new();
714        let base = IMPORT_COUNT + RT_FUNC_COUNT;
715
716        exports.export("init", ExportKind::Func, base);
717        exports.export("dispatch_action", ExportKind::Func, base + 1);
718        exports.export("render", ExportKind::Func, base + 2);
719        exports.export("get_state", ExportKind::Func, base + 3);
720        exports.export("dealloc", ExportKind::Func, base + 4);
721        exports.export("alloc", ExportKind::Func, IMPORT_COUNT + runtime::RT_ALLOC);
722        exports.export("memory", ExportKind::Memory, 0);
723
724        // Conditional exports
725        if self.function_table.contains_key("update") {
726            exports.export(
727                "update",
728                ExportKind::Func,
729                *self.function_table.get("update").unwrap(),
730            );
731        }
732        if self.function_table.contains_key("handle_event") {
733            exports.export(
734                "handle_event",
735                ExportKind::Func,
736                *self.function_table.get("handle_event").unwrap(),
737            );
738        }
739
740        // Always export invoke_lambda for host callback support
741        if self.function_table.contains_key("invoke_lambda") {
742            exports.export(
743                "invoke_lambda",
744                ExportKind::Func,
745                *self.function_table.get("invoke_lambda").unwrap(),
746            );
747        }
748        // Export the function table for call_indirect
749        if !self.lambda_bodies.is_empty() {
750            exports.export("__indirect_function_table", ExportKind::Table, 0);
751        }
752
753        // Test function exports: __test_0, __test_1, … and __test_count
754        if self.num_test_funcs > 0 {
755            let test_base = IMPORT_COUNT
756                + RT_FUNC_COUNT
757                + self.num_space_funcs
758                + 1 // invoke_lambda
759                + self.lambda_bodies.len() as u32;
760            let num_cases = self.num_test_funcs - 1; // last one is __test_count
761            for i in 0..num_cases {
762                exports.export(
763                    &format!("__test_{i}"),
764                    ExportKind::Func,
765                    test_base + i,
766                );
767            }
768            exports.export(
769                "__test_count",
770                ExportKind::Func,
771                test_base + num_cases,
772            );
773        }
774
775        exports
776    }
777
778    // ── Data section ─────────────────────────────────────────────────────
779
780    fn emit_data(&self) -> DataSection {
781        let mut data_sec = DataSection::new();
782        let mut all_data = self.data.data_bytes();
783        all_data.extend_from_slice(&self.user_data);
784        data_sec.active(0, &ConstExpr::i32_const(0), all_data);
785        data_sec
786    }
787
788    // ── Table section ────────────────────────────────────────────────────
789
790    fn emit_table(&self) -> TableSection {
791        let mut table_sec = TableSection::new();
792        // Always add a funcref table (minimum size = max(1, lambda count))
793        // Even if no lambdas, the table is needed for call_indirect validation
794        let table_size = std::cmp::max(1, self.lambda_bodies.len() as u64);
795        table_sec.table(TableType {
796            element_type: RefType::FUNCREF,
797            minimum: table_size,
798            maximum: Some(table_size),
799            table64: false,
800            shared: false,
801        });
802        table_sec
803    }
804
805    // ── Element section ──────────────────────────────────────────────────
806
807    fn emit_elements(&self) -> ElementSection {
808        let mut elem_sec = ElementSection::new();
809        if !self.lambda_bodies.is_empty() {
810            // Populate table at offset 0 with lambda function indices
811            let lambda_base =
812                IMPORT_COUNT + RT_FUNC_COUNT + self.num_space_funcs + 1; // +1 for invoke_lambda
813            let func_indices: Vec<u32> = (0..self.lambda_bodies.len() as u32)
814                .map(|i| lambda_base + i)
815                .collect();
816            elem_sec.active(
817                Some(0),
818                &ConstExpr::i32_const(0),
819                Elements::Functions(std::borrow::Cow::Owned(func_indices)),
820            );
821        }
822        elem_sec
823    }
824
825    // ── Custom section ───────────────────────────────────────────────────
826
827    fn emit_custom(&self) -> CustomSection<'_> {
828        CustomSection {
829            name: std::borrow::Cow::Borrowed(CUSTOM_SECTION_NAME),
830            data: std::borrow::Cow::Borrowed(COMPILER_VERSION.as_bytes()),
831        }
832    }
833
834    // ── Helpers ──────────────────────────────────────────────────────────
835
836    /// Create a [`FuncContext`] for code-generating a function body.
837    fn make_func_context(&self, param_count: u32) -> FuncContext {
838        FuncContext {
839            locals: Vec::new(),
840            local_names: HashMap::new(),
841            next_local: param_count,
842            state_field_names: self.state_field_names.clone(),
843            action_names: self.action_names.clone(),
844            variant_ids: self.variant_ids.clone(),
845            function_table: self.function_table.clone(),
846            data: self.data.clone_tracker(),
847            user_data: Vec::new(),
848            string_cache: HashMap::new(),
849            lambda_bodies: Vec::new(),
850            lambda_base_idx: IMPORT_COUNT + RT_FUNC_COUNT + self.num_space_funcs
851                + 1 // +1 for invoke_lambda
852                + self.lambda_bodies.len() as u32,
853        }
854    }
855
856    /// Merge user data from a function context back into the compiler.
857    fn merge_user_data(&mut self, ctx: &FuncContext) {
858        self.user_data.extend_from_slice(&ctx.user_data);
859        // Update data tracker offset
860        self.data.next_offset = ctx.data.next_offset;
861        // Collect lambda bodies registered during this function's codegen
862        self.lambda_bodies.extend(ctx.lambda_bodies.clone());
863    }
864
865    /// Finalize a scratch function: rebuild with correct local declarations.
866    ///
867    /// `Function::new(vec![])` declares 0 locals, so its raw body starts with
868    /// a single 0x00 byte (LEB128 zero).  We strip that byte and prepend the
869    /// actual locals from `ctx`.
870    fn finalize_function(scratch: Function, ctx: &FuncContext) -> Function {
871        let raw = scratch.into_raw_body();
872        // raw[0] == 0x00 (the "0 local declarations" byte).  Everything after
873        // that is instruction bytes we want to keep.
874        let instr_bytes = &raw[1..];
875        let mut f = Function::new(ctx.locals.clone());
876        f.raw(instr_bytes.iter().copied());
877        f
878    }
879}
880
881// ══════════════════════════════════════════════════════════════════════════════
882// FuncContext — per-function codegen state
883// ══════════════════════════════════════════════════════════════════════════════
884
885/// State maintained while generating code for a single function body.
886pub struct FuncContext {
887    /// Additional locals declared during codegen: (count, type).
888    pub locals: Vec<(u32, ValType)>,
889    /// Name → local index stack (for scoped let bindings).
890    pub local_names: HashMap<String, Vec<u32>>,
891    /// Next available local index.
892    pub next_local: u32,
893    /// State field names for identifier resolution.
894    pub state_field_names: Vec<String>,
895    /// Action names for action-ref resolution.
896    pub action_names: Vec<String>,
897    /// Variant name → id.
898    pub variant_ids: HashMap<String, u32>,
899    /// Known functions.
900    pub function_table: HashMap<String, u32>,
901    /// Data segment tracker (for interning strings).
902    pub data: DataSegmentTrackerClone,
903    /// User string data accumulated during codegen.
904    pub user_data: Vec<u8>,
905    /// String deduplication cache: content → (offset, length).
906    pub string_cache: HashMap<String, (u32, u32)>,
907    /// Lambda bodies collected during codegen (deferred compilation).
908    pub lambda_bodies: Vec<LambdaBody>,
909    /// Base function index for lambda functions in the WASM table.
910    pub lambda_base_idx: u32,
911}
912
913impl FuncContext {
914    /// Allocate a new local of the given type. Returns the local index.
915    pub fn alloc_local(&mut self, ty: ValType) -> u32 {
916        let idx = self.next_local;
917        self.next_local += 1;
918        self.locals.push((1, ty));
919        idx
920    }
921
922    /// Push a named local binding (for `let` and `for` scopes).
923    pub fn push_local(&mut self, name: &str, idx: u32) {
924        self.local_names
925            .entry(name.to_string())
926            .or_default()
927            .push(idx);
928    }
929
930    /// Pop a named local binding.
931    pub fn pop_local(&mut self, name: &str) {
932        if let Some(stack) = self.local_names.get_mut(name) {
933            stack.pop();
934        }
935    }
936
937    /// Get the local index for a named binding.
938    pub fn get_local(&self, name: &str) -> Option<u32> {
939        self.local_names
940            .get(name)
941            .and_then(|stack| stack.last().copied())
942    }
943
944    /// Check if a name is a state field.
945    pub fn is_state_field(&self, name: &str) -> bool {
946        self.state_field_names.iter().any(|s| s == name)
947    }
948
949    /// Get the action_id for a name.
950    pub fn get_action_id(&self, name: &str) -> Option<usize> {
951        self.action_names.iter().position(|a| a == name)
952    }
953
954    /// Get verbose function index by name.
955    pub fn get_function(&self, name: &str) -> Option<u32> {
956        self.function_table.get(name).copied()
957    }
958
959    /// Get variant id by name.
960    pub fn get_variant_id(&self, name: &str) -> u32 {
961        *self.variant_ids.get(name).unwrap_or(&0)
962    }
963
964    /// Intern a string constant, returning (offset, length).
965    /// Deduplicates: if the same string was already interned, reuses it.
966    pub fn intern_string(&mut self, s: &str) -> (u32, u32) {
967        if let Some(&cached) = self.string_cache.get(s) {
968            return cached;
969        }
970        let (ptr, len) = self.data.intern_string(s);
971        self.user_data.extend_from_slice(s.as_bytes());
972        self.string_cache.insert(s.to_string(), (ptr, len));
973        (ptr, len)
974    }
975
976    /// Register a lambda body for deferred compilation.
977    /// Returns the WASM function index for this lambda.
978    pub fn register_lambda(
979        &mut self,
980        params: Vec<pepl_types::ast::Param>,
981        body: pepl_types::ast::Block,
982        captured: Vec<String>,
983    ) -> u32 {
984        let lambda_idx = self.lambda_bodies.len() as u32;
985        self.lambda_bodies.push(LambdaBody {
986            params,
987            body,
988            captured,
989        });
990        // Function index = lambda_base_idx + lambda_idx
991        self.lambda_base_idx + lambda_idx
992    }
993
994    /// Resolve a qualified call to (module_id, function_id).
995    ///
996    /// For capability modules this returns the capability/function IDs.
997    /// For pure stdlib modules, we use synthetic IDs starting at 100.
998    pub fn resolve_qualified_call(&self, module: &str, function: &str) -> (u32, u32) {
999        match module {
1000            "http" => {
1001                let fn_id = match function {
1002                    "get" => 1,
1003                    "post" => 2,
1004                    "put" => 3,
1005                    "patch" => 4,
1006                    "delete" => 5,
1007                    _ => 0,
1008                };
1009                (1, fn_id)
1010            }
1011            "storage" => {
1012                let fn_id = match function {
1013                    "get" => 1,
1014                    "set" => 2,
1015                    "delete" => 3,
1016                    "keys" => 4,
1017                    _ => 0,
1018                };
1019                (2, fn_id)
1020            }
1021            "location" => (3, if function == "current" { 1 } else { 0 }),
1022            "notifications" => (4, if function == "send" { 1 } else { 0 }),
1023            "credential" => (5, if function == "get" { 1 } else { 0 }),
1024            // Pure stdlib: use IDs 100+
1025            "math" => (100, self.stdlib_fn_id(function)),
1026            "string" => (101, self.stdlib_fn_id(function)),
1027            "list" => (102, self.stdlib_fn_id(function)),
1028            "record" => (103, self.stdlib_fn_id(function)),
1029            "json" => (104, self.stdlib_fn_id(function)),
1030            "convert" => (105, self.stdlib_fn_id(function)),
1031            "time" => (106, self.stdlib_fn_id(function)),
1032            "timer" => (107, self.stdlib_fn_id(function)),
1033            "core" => (108, self.stdlib_fn_id(function)),
1034            _ => (0, 0),
1035        }
1036    }
1037
1038    /// Resolve a method call to (module_id, function_id).
1039    pub fn resolve_method_call(&self, method: &str) -> (u32, u32) {
1040        // Method calls map to stdlib modules based on common patterns
1041        let fn_id = self.stdlib_fn_id(method);
1042        // Module 0 = generic method dispatch
1043        (0, fn_id)
1044    }
1045
1046    /// Assign a numeric ID to a stdlib function name.
1047    fn stdlib_fn_id(&self, function: &str) -> u32 {
1048        // Simple hash-based ID assignment for stdlib functions.
1049        // This is deterministic across compilations.
1050        let mut hash: u32 = 5381;
1051        for b in function.bytes() {
1052            hash = hash.wrapping_mul(33).wrapping_add(b as u32);
1053        }
1054        hash & 0xFFFF
1055    }
1056}
1057
1058// ══════════════════════════════════════════════════════════════════════════════
1059// DataSegmentTrackerClone — clonable view of DataSegmentTracker for FuncContext
1060// ══════════════════════════════════════════════════════════════════════════════
1061
1062/// A clonable wrapper around key fields from [`DataSegmentTracker`].
1063#[derive(Clone)]
1064pub struct DataSegmentTrackerClone {
1065    pub true_ptr: u32,
1066    pub true_len: u32,
1067    pub false_ptr: u32,
1068    pub false_len: u32,
1069    pub nil_ptr: u32,
1070    pub nil_len: u32,
1071    pub value_ptr: u32,
1072    pub value_len: u32,
1073    pub gas_exhausted_ptr: u32,
1074    pub gas_exhausted_len: u32,
1075    pub div_by_zero_ptr: u32,
1076    pub div_by_zero_len: u32,
1077    pub nan_ptr: u32,
1078    pub nan_len: u32,
1079    pub assert_failed_ptr: u32,
1080    pub assert_failed_len: u32,
1081    pub invariant_failed_ptr: u32,
1082    pub invariant_failed_len: u32,
1083    pub unwrap_failed_ptr: u32,
1084    pub unwrap_failed_len: u32,
1085    pub oom_ptr: u32,
1086    pub oom_len: u32,
1087    pub next_offset: u32,
1088}
1089
1090impl DataSegmentTrackerClone {
1091    /// Intern a string in the clone tracker.
1092    pub fn intern_string(&mut self, s: &str) -> (u32, u32) {
1093        let ptr = self.next_offset;
1094        let len = s.len() as u32;
1095        self.next_offset += len;
1096        (ptr, len)
1097    }
1098}
1099
1100impl DataSegmentTracker {
1101    /// Create a clonable snapshot of this tracker.
1102    pub fn clone_tracker(&self) -> DataSegmentTrackerClone {
1103        DataSegmentTrackerClone {
1104            true_ptr: self.true_ptr,
1105            true_len: self.true_len,
1106            false_ptr: self.false_ptr,
1107            false_len: self.false_len,
1108            nil_ptr: self.nil_ptr,
1109            nil_len: self.nil_len,
1110            value_ptr: self.value_ptr,
1111            value_len: self.value_len,
1112            gas_exhausted_ptr: self.gas_exhausted_ptr,
1113            gas_exhausted_len: self.gas_exhausted_len,
1114            div_by_zero_ptr: self.div_by_zero_ptr,
1115            div_by_zero_len: self.div_by_zero_len,
1116            nan_ptr: self.nan_ptr,
1117            nan_len: self.nan_len,
1118            assert_failed_ptr: self.assert_failed_ptr,
1119            assert_failed_len: self.assert_failed_len,
1120            invariant_failed_ptr: self.invariant_failed_ptr,
1121            invariant_failed_len: self.invariant_failed_len,
1122            unwrap_failed_ptr: self.unwrap_failed_ptr,
1123            unwrap_failed_len: self.unwrap_failed_len,
1124            oom_ptr: self.oom_ptr,
1125            oom_len: self.oom_len,
1126            next_offset: self.next_offset,
1127        }
1128    }
1129}