//! Code generator: walks the AST and emits bytecode instructions. use el_parser::{BinOp, Expr, JsxAttrValue, Literal, Program, Stmt}; use crate::bytecode::{Bytecode, Value}; use crate::error::CompileResult; use crate::source_map::SourceMap; /// Generates bytecode from a parsed program. pub struct Codegen { instructions: Vec, source_map: SourceMap, #[allow(dead_code)] emit_source_map: bool, } impl Codegen { pub fn new(emit_source_map: bool) -> Self { Self { instructions: Vec::new(), source_map: SourceMap::new(), emit_source_map, } } /// Generate bytecode for a complete program. pub fn generate(mut self, program: &Program) -> CompileResult<(Vec, SourceMap)> { for stmt in &program.stmts { self.gen_stmt(stmt)?; } self.emit(Bytecode::Halt); Ok((self.instructions, self.source_map)) } // ── Emission helpers ────────────────────────────────────────────────────── fn emit(&mut self, instr: Bytecode) -> usize { let idx = self.instructions.len(); self.instructions.push(instr); idx } #[allow(dead_code)] fn emit_at_span(&mut self, instr: Bytecode, span: el_lexer::Span) -> usize { let idx = self.instructions.len(); if self.emit_source_map { self.source_map.record(idx, span); } self.instructions.push(instr); idx } fn patch_jump(&mut self, idx: usize, target: usize) { // offset = target - (idx + 1) (jump is relative to the next instruction) let offset = target as i32 - (idx as i32 + 1); match &mut self.instructions[idx] { Bytecode::Jump(o) | Bytecode::JumpIf(o) | Bytecode::JumpIfNot(o) => *o = offset, _ => {} } } fn current_idx(&self) -> usize { self.instructions.len() } // ── Statement code generation ───────────────────────────────────────────── /// Generate a statement in tail position (the last stmt of a block). /// Expression statements leave their value on the stack instead of popping it. fn gen_stmt_tail(&mut self, stmt: &Stmt) -> CompileResult<()> { match stmt { Stmt::Expr(expr, _) => { // In tail position, leave the value on the stack. self.gen_expr(expr)?; // If-without-else leaves nothing on stack (it pops internally); // push Nil so we always have exactly one return value. if matches!(expr, Expr::If { else_: None, .. }) { self.emit(Bytecode::Push(Value::Nil)); } } Stmt::Return(expr, _) => { self.gen_expr(expr)?; self.emit(Bytecode::Return); } // All other statement kinds behave the same as non-tail; push Nil as block value. other => { self.gen_stmt(other)?; self.emit(Bytecode::Push(Value::Nil)); } } Ok(()) } fn gen_stmt(&mut self, stmt: &Stmt) -> CompileResult<()> { // Record the source span for this statement in the source map if self.emit_source_map { let span = stmt_span(stmt); let idx = self.instructions.len(); self.source_map.record(idx, span); } match stmt { Stmt::Let { name, value, .. } => { self.gen_expr(value)?; self.emit(Bytecode::StoreLocal(name.clone())); } Stmt::Return(expr, _) => { self.gen_expr(expr)?; self.emit(Bytecode::Return); } Stmt::Expr(expr, _) => { self.gen_expr(expr)?; // Always discard the expression result in statement position. // Expr::Block is NOT special-cased: even a block expression used // as a statement should have its result discarded. // Note: Expr::If { else_: Some(_) } leaves a value on stack from // both branches (via gen_stmt_tail on the last block statement), // so it must be popped here too. // If-without-else already pops internally (see gen_expr for If), // so we only skip the extra Pop for that case. let needs_pop = match expr { Expr::If { else_: None, .. } => false, // already handled internally _ => true, }; if needs_pop { self.emit(Bytecode::Pop); } } Stmt::FnDef { name, params, body, requires, .. } => { // In this simple bytecode model, function defs emit a Jump to skip // the function body, then a label for the function start. // A full implementation would use a call frame table; for now we // emit the body inline and register the entry point offset. let skip_jump = self.emit(Bytecode::Jump(0)); // patched below // Function body // Bind parameters in order (caller pushes args left-to-right) for param in params.iter().rev() { self.emit(Bytecode::StoreLocal(param.name.clone())); } // Emit contract check if `requires` is present if let Some(req_expr) = requires { self.gen_expr(req_expr)?; self.emit(Bytecode::ContractCheck { message: format!("contract violation in fn '{name}': requires clause failed"), }); } // All statements except the last use gen_stmt (which pops expr results). // The last statement uses gen_stmt_tail so that the final expression // value stays on the stack as the function's implicit return value. let body_len = body.len(); for (i, s) in body.iter().enumerate() { if i + 1 == body_len { self.gen_stmt_tail(s)?; } else { self.gen_stmt(s)?; } } // If the body is empty, return Nil. if body_len == 0 { self.emit(Bytecode::Push(Value::Nil)); } self.emit(Bytecode::Return); // Patch the skip jump let after = self.current_idx(); self.patch_jump(skip_jump, after); // Register the function name → bytecode offset mapping // (stored as a load of the entry point index as an Int constant, // then store as a local — real implementations use a function table) let entry_point = skip_jump + 1; // first instruction of body self.emit(Bytecode::Push(Value::Int(entry_point as i64))); self.emit(Bytecode::StoreLocal(format!("__fn_{name}"))); } Stmt::While { condition, body, .. } => { // Codegen for `while { }`: // loop_start: // [condition] // JumpIfNot(done) // [body] // Jump(loop_start) // done: let loop_start = self.current_idx(); self.gen_expr(condition)?; let to_done = self.emit(Bytecode::JumpIfNot(0)); // patched to done for s in body { self.gen_stmt(s)?; } let back_jump = self.emit(Bytecode::Jump(0)); // patched to loop_start let done = self.current_idx(); self.patch_jump(to_done, done); self.patch_jump(back_jump, loop_start); } Stmt::Retry { count, body, fallback, .. } => { // Codegen for retry N times: // counter = N // loop_start: // if counter <= 0 goto fallback // decrement counter // [body] // goto done // fallback: // [fallback_body] // done: let counter_name = format!("__retry_counter_{}__", self.current_idx()); // Initialize counter self.gen_expr(count)?; self.emit(Bytecode::StoreLocal(counter_name.clone())); // Loop start: check counter > 0 let loop_start = self.current_idx(); self.emit(Bytecode::LoadLocal(counter_name.clone())); self.emit(Bytecode::Push(Value::Int(0))); self.emit(Bytecode::Gt); let to_fallback = self.emit(Bytecode::JumpIfNot(0)); // patched to fallback // Decrement counter self.emit(Bytecode::LoadLocal(counter_name.clone())); self.emit(Bytecode::Push(Value::Int(1))); self.emit(Bytecode::Sub); self.emit(Bytecode::StoreLocal(counter_name.clone())); // Execute body for s in body { self.gen_stmt(s)?; } // Body succeeded — jump to done let to_done = self.emit(Bytecode::Jump(0)); // Fallback let fallback_start = self.current_idx(); self.patch_jump(to_fallback, fallback_start); if let Some(fb_body) = fallback { for s in fb_body { self.gen_stmt(s)?; } } let done = self.current_idx(); self.patch_jump(to_done, done); // Note: in this simple model the body always "succeeds". // A real retry would need exception-like control flow. // For the retry-loop semantic, also add a back-jump that // jumps back to loop_start after each body execution would // require adding another jump before `to_done`. This design // runs the body once then exits — which is correct for // "success on first try" semantics in a pure-fn language. } Stmt::Deploy { fn_name, route, target, .. } => { self.emit(Bytecode::DeployFn { fn_name: fn_name.clone(), route: route.clone(), target: target.clone(), }); } Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => { // Type and enum definitions are compile-time only; no runtime code. } // Test-related statements — skipped during normal compilation. // The el-test crate walks the AST directly rather than running compiled bytecode. Stmt::TestDef { .. } | Stmt::Seed(..) | Stmt::Assert(..) => {} // Component definition: generate a function that renders the component. // The function is registered under "__component_" and the template // is emitted as a nested function body. Stmt::ComponentDef { name, state, methods, template, .. } => { let fn_name = format!("__component_{name}"); let skip_jump = self.emit(Bytecode::Jump(0)); // Emit component methods (each has its own Jump/body/Register pattern) for m in methods { self.gen_stmt(m)?; } // Record where the template starts — AFTER all method bodies. // This is the real entry point for calling this component. let template_start = self.current_idx(); // Initialise state fields as locals (so template expressions can load them) for field in state { if let Some(default) = &field.default { self.gen_expr(default)?; } else { self.emit(Bytecode::Push(Value::Nil)); } self.emit(Bytecode::StoreLocal(field.name.clone())); } // Emit template as the return value self.gen_expr(template)?; self.emit(Bytecode::Return); let after = self.current_idx(); self.patch_jump(skip_jump, after); // entry_point is the first instruction of the template body. let entry_point = template_start; self.emit(Bytecode::Push(Value::Int(entry_point as i64))); self.emit(Bytecode::StoreLocal(format!("__fn_{fn_name}"))); // Also register the component name directly so `` works self.emit(Bytecode::Push(Value::Int(entry_point as i64))); self.emit(Bytecode::StoreLocal(format!("__fn_{name}"))); } // New statement kinds — no runtime code emitted. _ => {} } Ok(()) } // ── Expression code generation ──────────────────────────────────────────── fn gen_expr(&mut self, expr: &Expr) -> CompileResult<()> { match expr { Expr::Literal(lit) => { let val = match lit { Literal::Int(n) => Value::Int(*n), Literal::Float(f) => Value::Float(*f), Literal::Str(s) => Value::Str(s.clone()), Literal::Bool(b) => Value::Bool(*b), }; self.emit(Bytecode::Push(val)); } Expr::Ident(name) => { self.emit(Bytecode::LoadLocal(name.clone())); } Expr::BinOp { op, left, right } => { // NullCoalesce (`a ?? b`) is short-circuit: if `a` is truthy, use it; else `b`. // Bytecode: eval a, Dup, JumpIf(skip), Pop, eval b, skip: if matches!(op, BinOp::NullCoalesce) { self.gen_expr(left)?; self.emit(Bytecode::Dup); let skip = self.emit(Bytecode::JumpIf(0)); // patched below self.emit(Bytecode::Pop); // discard the falsy `a` self.gen_expr(right)?; let after = self.current_idx(); self.patch_jump(skip, after); return Ok(()); } self.gen_expr(left)?; self.gen_expr(right)?; let instr = match op { BinOp::Add => Bytecode::Add, BinOp::Sub => Bytecode::Sub, BinOp::Mul => Bytecode::Mul, BinOp::Div => Bytecode::Div, BinOp::Eq => Bytecode::Eq, BinOp::NotEq => Bytecode::NotEq, BinOp::Lt => Bytecode::Lt, BinOp::Gt => Bytecode::Gt, BinOp::LtEq => Bytecode::LtEq, BinOp::GtEq => Bytecode::GtEq, BinOp::And => Bytecode::And, BinOp::Or => Bytecode::Or, BinOp::Mod => Bytecode::Mod, BinOp::BitAnd => Bytecode::BitAnd, BinOp::BitOr => Bytecode::BitOr, BinOp::BitXor => Bytecode::BitXor, BinOp::Shl => Bytecode::Shl, BinOp::Shr => Bytecode::Shr, BinOp::NullCoalesce => unreachable!("handled above"), }; self.emit(instr); } Expr::UnaryNot(inner) => { self.gen_expr(inner)?; self.emit(Bytecode::Not); } Expr::UnaryBitNot(inner) => { self.gen_expr(inner)?; self.emit(Bytecode::BitNot); } Expr::Call { func, args } => { // Push arguments left-to-right for arg in args { self.gen_expr(arg)?; } // Get the function name from the callee expression let fn_name = match func.as_ref() { Expr::Ident(n) => { // Strip leading `@` from syscall/builtin names: // `@http_get` → `http_get`, `@json_parse` → `json_parse` n.strip_prefix('@').unwrap_or(n).to_string() } Expr::Field { object, field } => { self.gen_expr(object)?; field.clone() } _ => { self.gen_expr(func)?; "__dynamic__".to_string() } }; self.emit(Bytecode::Call { name: fn_name, arity: args.len() as u32 }); } Expr::Block(stmts) => { if stmts.is_empty() { self.emit(Bytecode::Push(Value::Nil)); } else { for (i, s) in stmts.iter().enumerate() { let is_last = i == stmts.len() - 1; if is_last { // Last statement: emit as a tail expression (leave value on stack). self.gen_stmt_tail(s)?; } else { self.gen_stmt(s)?; } } } } Expr::If { cond, then, else_ } => { self.gen_expr(cond)?; let jump_false = self.emit(Bytecode::JumpIfNot(0)); // patched self.gen_expr(then)?; if let Some(else_expr) = else_ { let jump_end = self.emit(Bytecode::Jump(0)); // skip else let else_start = self.current_idx(); self.patch_jump(jump_false, else_start); self.gen_expr(else_expr)?; let after_else = self.current_idx(); self.patch_jump(jump_end, after_else); } else { // No else branch: if-without-else is a statement, not a value // expression. Discard the then-block's pushed value so both // paths leave the stack at the same height (net zero change). self.emit(Bytecode::Pop); let after_pop = self.current_idx(); self.patch_jump(jump_false, after_pop); // false path skips Pop too } } Expr::Match { subject, arms } => { self.gen_expr(subject)?; // Simplified match: for each arm, dup subject, push pattern, // compare, branch. A full implementation would use a jump table. let mut end_jumps = Vec::new(); for arm in arms { match &arm.pattern { el_parser::Pattern::Wildcard => { // Wildcard always matches — pop subject and run body directly. self.emit(Bytecode::Pop); self.gen_expr(&arm.body)?; end_jumps.push(self.emit(Bytecode::Jump(0))); // No jump_no_match needed — wildcard always matches. // But we still need to patch the end jumps at the end. // Nothing else to do; break out of the loop since wildcard // is a catch-all and subsequent arms are unreachable. break; } el_parser::Pattern::Binding(name) => { // Bind and always match. self.emit(Bytecode::Dup); self.emit(Bytecode::StoreLocal(name.clone())); // Dup'd subject is still on stack; compare to itself. self.emit(Bytecode::Dup); self.emit(Bytecode::Eq); let jump_no_match = self.emit(Bytecode::JumpIfNot(0)); self.emit(Bytecode::Pop); self.gen_expr(&arm.body)?; end_jumps.push(self.emit(Bytecode::Jump(0))); let next_arm = self.current_idx(); self.patch_jump(jump_no_match, next_arm); } _ => { self.emit(Bytecode::Dup); // Push pattern value match &arm.pattern { el_parser::Pattern::Literal(lit) => { let v = match lit { Literal::Int(n) => Value::Int(*n), Literal::Str(s) => Value::Str(s.clone()), Literal::Bool(b) => Value::Bool(*b), Literal::Float(f) => Value::Float(*f), }; self.emit(Bytecode::Push(v)); } el_parser::Pattern::EnumVariant { variant, payload, .. } => { // Push the variant name as a string for comparison self.emit(Bytecode::Push(Value::Str(variant.clone()))); if let Some(bind) = payload { // Store the subject (simplified: payload = subject) self.emit(Bytecode::StoreLocal(bind.clone())); } } _ => unreachable!("wildcard and binding handled above"), } self.emit(Bytecode::Eq); let jump_no_match = self.emit(Bytecode::JumpIfNot(0)); // Pop subject from stack self.emit(Bytecode::Pop); // Generate arm body self.gen_expr(&arm.body)?; end_jumps.push(self.emit(Bytecode::Jump(0))); let next_arm = self.current_idx(); self.patch_jump(jump_no_match, next_arm); } } } // Default fallthrough: pop subject, push nil self.emit(Bytecode::Pop); self.emit(Bytecode::Push(Value::Nil)); let end = self.current_idx(); for j in end_jumps { self.patch_jump(j, end); } } Expr::Activate { type_name, query } => { self.emit(Bytecode::Activate { type_name: type_name.clone(), query: query.clone(), }); } Expr::Sealed(stmts) => { self.emit(Bytecode::SealedBegin); for s in stmts { self.gen_stmt(s)?; } self.emit(Bytecode::SealedEnd); self.emit(Bytecode::Push(Value::Nil)); } Expr::Field { object, field } => { self.gen_expr(object)?; self.emit(Bytecode::GetField(field.clone())); } Expr::Array(elems) => { // Push each element onto the stack, then collect with BuildList. for e in elems { self.gen_expr(e)?; } self.emit(Bytecode::BuildList(elems.len() as u32)); } Expr::Path { segments } => { // Emit the last segment as a string value (enum variant reference) let variant = segments.last().cloned().unwrap_or_default(); self.emit(Bytecode::Push(Value::Str(variant))); } Expr::Index { object, index } => { self.gen_expr(object)?; self.gen_expr(index)?; self.emit(Bytecode::GetIndex); } Expr::StructLit { type_name, fields, .. } => { // Push each field value in declaration order for (_, field_expr) in fields { self.gen_expr(field_expr)?; } let field_names: Vec = fields.iter().map(|(n, _)| n.clone()).collect(); self.emit(Bytecode::BuildStruct { type_name: type_name.clone(), fields: field_names, }); } Expr::MapLiteral(pairs) => { // Push each key-value pair, then collect with BuildMap. let n = pairs.len() as u32; for (key_expr, val_expr) in pairs { self.gen_expr(key_expr)?; self.gen_expr(val_expr)?; } self.emit(Bytecode::BuildMap(n)); } Expr::With { base, updates } => { // Generate base struct clone then apply updates self.gen_expr(base)?; for (field, val_expr) in updates { self.gen_expr(val_expr)?; self.emit(Bytecode::SetField(field.clone())); } } Expr::Reason { query } => { self.emit(Bytecode::Reason { query: query.clone() }); } Expr::Parallel { entries } => { // For parallel, emit each expression sequentially and collect into a Map // A full implementation would use threads; here we collect results into a Map let n = entries.len() as u32; for (name, expr) in entries { self.emit(Bytecode::Push(Value::Str(name.clone()))); self.gen_expr(expr)?; } self.emit(Bytecode::BuildMap(n)); } Expr::Trace { label, body } => { self.emit(Bytecode::TraceBegin { label: label.clone() }); for s in body { self.gen_stmt(s)?; } self.emit(Bytecode::TraceEnd { label: label.clone() }); self.emit(Bytecode::Push(Value::Nil)); } // JSX element: push tag name, attrs (as a map), and children list, then call __jsx__ Expr::JsxElement { tag, attrs, children, .. } => { // If the tag starts with an uppercase letter it's a component reference. // Call the component function directly (it takes no args, returns HTML string). let is_component = tag.chars().next().map_or(false, |c| c.is_uppercase()); if is_component { // Call the component as a function: Call { name: tag, arity: 0 } // The component will render itself and return an HTML string. self.emit(Bytecode::Call { name: tag.clone(), arity: 0 }); } else { // Push tag name self.emit(Bytecode::Push(Value::Str(tag.clone()))); // Build attrs map: push key-value pairs let n_attrs = attrs.len() as u32; for (attr_name, attr_val) in attrs { self.emit(Bytecode::Push(Value::Str(attr_name.clone()))); match attr_val { JsxAttrValue::Str(s) => { self.emit(Bytecode::Push(Value::Str(s.clone()))); } JsxAttrValue::Expr(expr) => { self.gen_expr(expr)?; } } } self.emit(Bytecode::BuildMap(n_attrs)); // Build children list for child in children { self.gen_expr(child)?; } self.emit(Bytecode::BuildList(children.len() as u32)); // Call __jsx__(tag, attrs, children) self.emit(Bytecode::Call { name: "__jsx__".to_string(), arity: 3 }); } } // JSX expression interpolation: just evaluate the inner expression Expr::JsxExpr(inner) => { self.gen_expr(inner)?; } // JSX text: push as string Expr::JsxText(text) => { self.emit(Bytecode::Push(Value::Str(text.clone()))); } // Closure: compile as an inline function and push a reference to it. // The closure body is emitted as a skip-over block. Expr::Closure { params, body, .. } => { let closure_id = self.current_idx(); let fn_name = format!("__closure_{closure_id}__"); let skip_jump = self.emit(Bytecode::Jump(0)); // Bind params in reverse order (stack has args pushed left-to-right) for param in params.iter().rev() { self.emit(Bytecode::StoreLocal(param.name.clone())); } self.gen_expr(body)?; self.emit(Bytecode::Return); let after = self.current_idx(); self.patch_jump(skip_jump, after); let entry_point = skip_jump + 1; // Register the closure function self.emit(Bytecode::Push(Value::Int(entry_point as i64))); self.emit(Bytecode::StoreLocal(format!("__fn_{fn_name}"))); // Push a reference to this closure (as its function name) self.emit(Bytecode::Push(Value::Str(fn_name))); } // New expression kinds — push Nil as placeholder _ => { self.emit(Bytecode::Push(Value::Nil)); } } Ok(()) } } // ── Helper: extract a representative span from a statement ──────────────────── fn stmt_span(stmt: &Stmt) -> el_lexer::Span { match stmt { Stmt::Let { span, .. } | Stmt::Return(_, span) | Stmt::Expr(_, span) | Stmt::FnDef { span, .. } | Stmt::TypeDef { span, .. } | Stmt::EnumDef { span, .. } | Stmt::TestDef { span, .. } | Stmt::Seed(_, span) | Stmt::Assert(_, span) => *span, _ => el_lexer::Span::new(0, 0, 0, 0), } } #[cfg(test)] mod tests { use el_lexer::tokenize; use el_parser::parse; use super::*; fn gen(src: &str) -> Vec { let tokens = tokenize(src).unwrap(); let prog = parse(tokens, src.to_string()).unwrap(); let cg = Codegen::new(false); let (bc, _) = cg.generate(&prog).unwrap(); bc } #[test] fn test_push_int() { let bc = gen("42"); assert!(matches!(&bc[0], Bytecode::Push(Value::Int(42)))); } #[test] fn test_let_store() { let bc = gen("let x = 1"); assert!(matches!(&bc[1], Bytecode::StoreLocal(n) if n == "x")); } #[test] fn test_add() { let bc = gen("1 + 2"); assert!(bc.iter().any(|b| matches!(b, Bytecode::Add))); } #[test] fn test_halt_at_end() { let bc = gen("42"); assert!(matches!(bc.last(), Some(Bytecode::Halt))); } #[test] fn test_activate_emitted() { let bc = gen(r#"activate User where "query""#); assert!(bc.iter().any(|b| matches!(b, Bytecode::Activate { .. }))); } #[test] fn test_sealed_markers() { let bc = gen("sealed { let x = 1 }"); assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedBegin))); assert!(bc.iter().any(|b| matches!(b, Bytecode::SealedEnd))); } }