Archived
737 lines
32 KiB
Rust
737 lines
32 KiB
Rust
//! 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<Bytecode>,
|
|
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<Bytecode>, 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 <condition> { <body> }`:
|
|
// 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_<Name>" 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 `<ComponentName />` 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<String> = 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<Bytecode> {
|
|
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)));
|
|
}
|
|
}
|