Archived
feat: engram-lang — new programming language, quantum-sealed prod target, spreading activation types
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
//! Code generator: walks the AST and emits bytecode instructions.
|
||||
|
||||
use el_parser::{BinOp, Expr, 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 ─────────────────────────────────────────────
|
||||
|
||||
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)?;
|
||||
// Discard the expression result unless it's a return-like
|
||||
if !matches!(expr, Expr::Block(_) | Expr::If { .. }) {
|
||||
self.emit(Bytecode::Pop);
|
||||
}
|
||||
}
|
||||
Stmt::FnDef { name, params, body, .. } => {
|
||||
// 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()));
|
||||
}
|
||||
for s in body {
|
||||
self.gen_stmt(s)?;
|
||||
}
|
||||
// Implicit void return
|
||||
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::TypeDef { .. } | Stmt::EnumDef { .. } => {
|
||||
// Type and enum definitions are compile-time only; no runtime code.
|
||||
}
|
||||
}
|
||||
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 } => {
|
||||
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,
|
||||
};
|
||||
self.emit(instr);
|
||||
}
|
||||
Expr::UnaryNot(inner) => {
|
||||
self.gen_expr(inner)?;
|
||||
self.emit(Bytecode::Not);
|
||||
}
|
||||
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) => n.clone(),
|
||||
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) => {
|
||||
for (i, s) in stmts.iter().enumerate() {
|
||||
self.gen_stmt(s)?;
|
||||
// The last expression statement is the block's value
|
||||
if i == stmts.len() - 1 {
|
||||
if let Stmt::Expr(_, _) = s {
|
||||
// Already on stack from gen_stmt (before the Pop)
|
||||
// We need to not pop it — handled by gen_stmt not
|
||||
// popping Block results; but we already did Pop.
|
||||
// Push nil as fallback for empty/void blocks.
|
||||
}
|
||||
}
|
||||
}
|
||||
if stmts.is_empty() {
|
||||
self.emit(Bytecode::Push(Value::Nil));
|
||||
}
|
||||
}
|
||||
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 {
|
||||
let after_then = self.current_idx();
|
||||
self.patch_jump(jump_false, after_then);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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 payload in a local (simplified: store subject as payload)
|
||||
self.emit(Bytecode::StoreLocal(bind.clone()));
|
||||
}
|
||||
}
|
||||
el_parser::Pattern::Binding(name) => {
|
||||
// Bind and always match — push duplicate and store
|
||||
self.emit(Bytecode::Dup);
|
||||
self.emit(Bytecode::StoreLocal(name.clone()));
|
||||
// Fall through — will compare to itself (always true)
|
||||
}
|
||||
el_parser::Pattern::Wildcard => {
|
||||
// Wildcard — push nil (always "matches")
|
||||
self.emit(Bytecode::Push(Value::Nil));
|
||||
}
|
||||
}
|
||||
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: 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) => {
|
||||
// Build a list by pushing all elements then collecting
|
||||
// In this simple VM we push a List value directly
|
||||
// For a stack-based VM we'd emit individual pushes + a BuildList instr;
|
||||
// here we inline the value since it's all literals at codegen time
|
||||
for e in elems {
|
||||
self.gen_expr(e)?;
|
||||
}
|
||||
// Emit a "build list of N" — we use a Call to a builtin
|
||||
self.emit(Bytecode::Call {
|
||||
name: "__build_list__".to_string(),
|
||||
arity: 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);
|
||||
}
|
||||
}
|
||||
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, .. } => *span,
|
||||
}
|
||||
}
|
||||
|
||||
#[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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user