rename crates/ to engrams/; add el-compiler el package with bootstrap artifact
- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
- src/{compiler,lexer,parser,codegen}.el
- bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
//! Bytecode instruction set for the Engram virtual machine.
|
||||
//!
|
||||
//! The VM is a simple stack machine. Every instruction pops its operands
|
||||
//! from the stack and pushes its result. Control flow uses relative signed
|
||||
//! offsets from the instruction *after* the jump.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A runtime value on the VM stack.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Str(String),
|
||||
Bool(bool),
|
||||
Nil,
|
||||
/// A list of values (used for `activate` results and array literals).
|
||||
List(Vec<Value>),
|
||||
/// A key-value map — used for Map<K,V> literals.
|
||||
/// Stored as a Vec of pairs to keep ordering and remain Serialize-friendly.
|
||||
Map(Vec<(String, Value)>),
|
||||
/// A Result<T,E> value — Ok variant.
|
||||
ResultOk(Box<Value>),
|
||||
/// A Result<T,E> value — Err variant.
|
||||
ResultErr(Box<Value>),
|
||||
/// A struct instance: type name + ordered field name-value pairs.
|
||||
Struct { type_name: String, fields: Vec<(String, Value)> },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Int(n) => write!(f, "{n}"),
|
||||
Value::Float(n) => write!(f, "{n}"),
|
||||
Value::Str(s) => write!(f, "{s}"),
|
||||
Value::Bool(b) => write!(f, "{b}"),
|
||||
Value::Nil => write!(f, "nil"),
|
||||
Value::List(vs) => {
|
||||
let items: Vec<_> = vs.iter().map(|v| v.to_string()).collect();
|
||||
write!(f, "[{}]", items.join(", "))
|
||||
}
|
||||
Value::Map(pairs) => {
|
||||
let items: Vec<_> = pairs.iter().map(|(k, v)| format!("{k}: {v}")).collect();
|
||||
write!(f, "{{{}}}", items.join(", "))
|
||||
}
|
||||
Value::ResultOk(v) => write!(f, "Ok({v})"),
|
||||
Value::ResultErr(e) => write!(f, "Err({e})"),
|
||||
Value::Struct { type_name, fields } => {
|
||||
let fs: Vec<_> = fields.iter().map(|(k, v)| format!("{k}: {v}")).collect();
|
||||
write!(f, "{type_name} {{ {} }}", fs.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single VM instruction.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Bytecode {
|
||||
// ── Stack ─────────────────────────────────────────────────────────────────
|
||||
/// Push a constant value onto the stack.
|
||||
Push(Value),
|
||||
/// Discard the top of stack.
|
||||
Pop,
|
||||
/// Duplicate the top of stack.
|
||||
Dup,
|
||||
|
||||
// ── Arithmetic ────────────────────────────────────────────────────────────
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
|
||||
// ── Comparison ────────────────────────────────────────────────────────────
|
||||
Eq,
|
||||
NotEq,
|
||||
Lt,
|
||||
Gt,
|
||||
LtEq,
|
||||
GtEq,
|
||||
|
||||
// ── Logical ───────────────────────────────────────────────────────────────
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
|
||||
// ── Locals ───────────────────────────────────────────────────────────────
|
||||
/// Load a local variable by name.
|
||||
LoadLocal(String),
|
||||
/// Store the top of stack into a local variable.
|
||||
StoreLocal(String),
|
||||
|
||||
// ── Functions ─────────────────────────────────────────────────────────────
|
||||
/// Call a function by name with `arity` arguments.
|
||||
Call { name: String, arity: u32 },
|
||||
/// Return from the current function (leaves return value on stack).
|
||||
Return,
|
||||
|
||||
// ── Control flow ──────────────────────────────────────────────────────────
|
||||
/// Unconditional jump: `ip += offset` (offset is from the *next* instruction).
|
||||
Jump(i32),
|
||||
/// Jump if the top of stack is truthy; pops the value.
|
||||
JumpIf(i32),
|
||||
/// Jump if the top of stack is falsy; pops the value.
|
||||
JumpIfNot(i32),
|
||||
|
||||
// ── Fields & Indexing ─────────────────────────────────────────────────────
|
||||
/// Load a named field from the struct on top of stack.
|
||||
GetField(String),
|
||||
/// Index into an array: pops index then array.
|
||||
GetIndex,
|
||||
/// Build a Map from the top N key-value pairs on the stack
|
||||
/// (keys are strings pushed as Str, values follow each key).
|
||||
BuildMap(u32),
|
||||
/// Build a list from the top N items on the stack.
|
||||
BuildList(u32),
|
||||
/// Build a struct instance: pop N field values (named by fields in order), push Map.
|
||||
BuildStruct { type_name: String, fields: Vec<String> },
|
||||
/// Set a field on the Map on top of stack.
|
||||
SetField(String),
|
||||
|
||||
// ── Special ───────────────────────────────────────────────────────────────
|
||||
/// `activate TypeName "query"` — emit a semantic query stub.
|
||||
/// In a full implementation this would call into the Engram runtime.
|
||||
Activate { type_name: String, query: String },
|
||||
/// Mark the start of a sealed section (the runtime enforces protection).
|
||||
SealedBegin,
|
||||
/// Mark the end of a sealed section.
|
||||
SealedEnd,
|
||||
/// No-op — used as a placeholder for forward jumps.
|
||||
Nop,
|
||||
/// Halt the VM.
|
||||
Halt,
|
||||
/// `reason "query"` — call soma AI inference endpoint.
|
||||
Reason { query: String },
|
||||
/// `parallel { name: expr, ... }` — spawn entries concurrently.
|
||||
/// Each entry is a (name, entry_ip) pair where entry_ip is the bytecode offset.
|
||||
Parallel { entries: Vec<(String, usize)> },
|
||||
/// Begin a trace region (debug mode: record start time).
|
||||
TraceBegin { label: String },
|
||||
/// End a trace region (debug mode: print elapsed).
|
||||
TraceEnd { label: String },
|
||||
/// Contract check: if top of stack is falsy, panic with message.
|
||||
ContractCheck { message: String },
|
||||
/// Deploy: POST to soma deployment API.
|
||||
DeployFn { fn_name: String, route: String, target: String },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Bytecode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Bytecode::Push(v) => write!(f, "PUSH {v}"),
|
||||
Bytecode::Pop => write!(f, "POP"),
|
||||
Bytecode::Dup => write!(f, "DUP"),
|
||||
Bytecode::Add => write!(f, "ADD"),
|
||||
Bytecode::Sub => write!(f, "SUB"),
|
||||
Bytecode::Mul => write!(f, "MUL"),
|
||||
Bytecode::Div => write!(f, "DIV"),
|
||||
Bytecode::Eq => write!(f, "EQ"),
|
||||
Bytecode::NotEq => write!(f, "NEQ"),
|
||||
Bytecode::Lt => write!(f, "LT"),
|
||||
Bytecode::Gt => write!(f, "GT"),
|
||||
Bytecode::LtEq => write!(f, "LTE"),
|
||||
Bytecode::GtEq => write!(f, "GTE"),
|
||||
Bytecode::And => write!(f, "AND"),
|
||||
Bytecode::Or => write!(f, "OR"),
|
||||
Bytecode::Not => write!(f, "NOT"),
|
||||
Bytecode::LoadLocal(n) => write!(f, "LOAD {n}"),
|
||||
Bytecode::StoreLocal(n) => write!(f, "STORE {n}"),
|
||||
Bytecode::Call { name, arity } => write!(f, "CALL {name}/{arity}"),
|
||||
Bytecode::Return => write!(f, "RETURN"),
|
||||
Bytecode::Jump(off) => write!(f, "JUMP {off:+}"),
|
||||
Bytecode::JumpIf(off) => write!(f, "JUMPIF {off:+}"),
|
||||
Bytecode::JumpIfNot(off) => write!(f, "JUMPIFNOT {off:+}"),
|
||||
Bytecode::GetField(n) => write!(f, "GETFIELD {n}"),
|
||||
Bytecode::GetIndex => write!(f, "GETINDEX"),
|
||||
Bytecode::BuildMap(n) => write!(f, "BUILDMAP {n}"),
|
||||
Bytecode::BuildList(n) => write!(f, "BUILDLIST {n}"),
|
||||
Bytecode::BuildStruct { type_name, fields } => {
|
||||
write!(f, "BUILDSTRUCT {type_name} [{}]", fields.join(", "))
|
||||
}
|
||||
Bytecode::SetField(n) => write!(f, "SETFIELD {n}"),
|
||||
Bytecode::Activate { type_name, query } => {
|
||||
write!(f, "ACTIVATE {type_name} \"{query}\"")
|
||||
}
|
||||
Bytecode::SealedBegin => write!(f, "SEALED_BEGIN"),
|
||||
Bytecode::SealedEnd => write!(f, "SEALED_END"),
|
||||
Bytecode::Nop => write!(f, "NOP"),
|
||||
Bytecode::Halt => write!(f, "HALT"),
|
||||
Bytecode::Reason { query } => write!(f, "REASON \"{query}\""),
|
||||
Bytecode::Parallel { entries } => {
|
||||
let names: Vec<_> = entries.iter().map(|(n, ip)| format!("{n}@{ip}")).collect();
|
||||
write!(f, "PARALLEL [{}]", names.join(", "))
|
||||
}
|
||||
Bytecode::TraceBegin { label } => write!(f, "TRACE_BEGIN \"{label}\""),
|
||||
Bytecode::TraceEnd { label } => write!(f, "TRACE_END \"{label}\""),
|
||||
Bytecode::ContractCheck { message } => write!(f, "CONTRACT_CHECK \"{message}\""),
|
||||
Bytecode::DeployFn { fn_name, route, target } => {
|
||||
write!(f, "DEPLOY {fn_name} -> {route} via {target}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize bytecode instructions to bytes for storage/sealing.
|
||||
pub fn serialize_bytecode(instructions: &[Bytecode]) -> Result<Vec<u8>, String> {
|
||||
serde_json::to_vec(instructions).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Deserialize bytecode instructions from bytes.
|
||||
pub fn deserialize_bytecode(bytes: &[u8]) -> Result<Vec<Bytecode>, String> {
|
||||
serde_json::from_slice(bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
impl Bytecode {
|
||||
/// Deserialize a bytecode slice from JSON bytes (convenience wrapper).
|
||||
pub fn deserialize_all(bytes: &[u8]) -> Result<Vec<Bytecode>, String> {
|
||||
deserialize_bytecode(bytes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
//! 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 ─────────────────────────────────────────────
|
||||
|
||||
/// 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)?;
|
||||
}
|
||||
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)?;
|
||||
// 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, 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"),
|
||||
});
|
||||
}
|
||||
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::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(..) => {}
|
||||
// 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 } => {
|
||||
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) => {
|
||||
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 {
|
||||
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 {
|
||||
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::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));
|
||||
}
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! Top-level compiler struct — orchestrates the full pipeline.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use el_lexer::tokenize;
|
||||
use el_parser::parse;
|
||||
use el_seal::{seal, SealConfig, SealedArtifact};
|
||||
use el_types::TypeChecker;
|
||||
|
||||
use crate::bytecode::{deserialize_bytecode, serialize_bytecode};
|
||||
use crate::codegen::Codegen;
|
||||
use crate::error::{CompileError, CompileResult};
|
||||
|
||||
/// Which compilation target to produce.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Target {
|
||||
/// Full debug info: source maps, stack traces, no optimization.
|
||||
Debug,
|
||||
/// Optimized, stripped, no debug info.
|
||||
Release,
|
||||
/// Quantum-sealed: encrypted bytecode, cannot be decompiled.
|
||||
Prod,
|
||||
}
|
||||
|
||||
/// Compiler configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompilerOptions {
|
||||
pub target: Target,
|
||||
pub output_path: PathBuf,
|
||||
pub source_path: PathBuf,
|
||||
/// Path to an Engram database for `activate` type resolution.
|
||||
/// `None` disables semantic type compatibility (falls back to structural).
|
||||
pub engram_db_path: Option<PathBuf>,
|
||||
/// Seal configuration for the `prod` target.
|
||||
pub seal_config: SealConfig,
|
||||
}
|
||||
|
||||
impl Default for CompilerOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: Target::Debug,
|
||||
output_path: PathBuf::from("out.elc"),
|
||||
source_path: PathBuf::from("main.el"),
|
||||
engram_db_path: None,
|
||||
seal_config: SealConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The output of a compilation.
|
||||
#[derive(Debug)]
|
||||
pub struct CompileOutput {
|
||||
/// The compiled artifact bytes. Format depends on target:
|
||||
/// - Debug/Release: JSON-serialized `Vec<Bytecode>`
|
||||
/// - Prod: `SealedArtifact` wire format (`ENGRAM01` + JSON body)
|
||||
pub artifact: Vec<u8>,
|
||||
pub target: Target,
|
||||
/// Whether the artifact is quantum-sealed.
|
||||
pub sealed: bool,
|
||||
/// JSON source map (debug target only).
|
||||
pub source_map: Option<String>,
|
||||
/// Type-check and compilation diagnostics.
|
||||
pub diagnostics: Vec<String>,
|
||||
}
|
||||
|
||||
/// The Engram language compiler.
|
||||
pub struct Compiler;
|
||||
|
||||
impl Compiler {
|
||||
/// Compile `source` with the given options.
|
||||
pub fn compile(source: &str, opts: CompilerOptions) -> CompileResult<CompileOutput> {
|
||||
// ── Step 1: Lex ───────────────────────────────────────────────────────
|
||||
let tokens = tokenize(source)?;
|
||||
|
||||
// ── Step 2: Parse ─────────────────────────────────────────────────────
|
||||
let program = parse(tokens, source.to_string())?;
|
||||
|
||||
// ── Step 3: Type-check ────────────────────────────────────────────────
|
||||
let mut checker = TypeChecker::with_builtins();
|
||||
let diags = checker.check(&program);
|
||||
let diagnostics: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();
|
||||
|
||||
// We continue compiling even with type errors in debug/release mode.
|
||||
// In prod mode, type errors are fatal.
|
||||
if opts.target == Target::Prod && !checker.ok() {
|
||||
return Err(CompileError::Type(
|
||||
diagnostics.join("; ")
|
||||
));
|
||||
}
|
||||
|
||||
// ── Step 4: Code generation ───────────────────────────────────────────
|
||||
let emit_sm = matches!(opts.target, Target::Debug);
|
||||
let cg = Codegen::new(emit_sm);
|
||||
let (bytecode, source_map) = cg.generate(&program)
|
||||
.map_err(|e| CompileError::Codegen(e.to_string()))?;
|
||||
|
||||
let bytecode_bytes = serialize_bytecode(&bytecode)
|
||||
.map_err(|e| CompileError::Codegen(e.to_string()))?;
|
||||
|
||||
// ── Step 5: Target-specific post-processing ───────────────────────────
|
||||
match opts.target {
|
||||
Target::Debug => {
|
||||
let sm_json = source_map.to_json()
|
||||
.map_err(|e| CompileError::Serialization(e.to_string()))?;
|
||||
Ok(CompileOutput {
|
||||
artifact: bytecode_bytes,
|
||||
target: Target::Debug,
|
||||
sealed: false,
|
||||
source_map: Some(sm_json),
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
Target::Release => {
|
||||
Ok(CompileOutput {
|
||||
artifact: bytecode_bytes,
|
||||
target: Target::Release,
|
||||
sealed: false,
|
||||
source_map: None,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
Target::Prod => {
|
||||
let artifact = seal(&bytecode_bytes, &opts.seal_config)?;
|
||||
let artifact_bytes = artifact.to_bytes()
|
||||
.map_err(|e| CompileError::Serialization(e.to_string()))?;
|
||||
Ok(CompileOutput {
|
||||
artifact: artifact_bytes,
|
||||
target: Target::Prod,
|
||||
sealed: true,
|
||||
source_map: None,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: compile and unseal, returning the bytecode instructions.
|
||||
pub fn compile_and_unseal(
|
||||
source: &str,
|
||||
opts: CompilerOptions,
|
||||
binding_key: &[u8],
|
||||
) -> CompileResult<Vec<crate::bytecode::Bytecode>> {
|
||||
let output = Self::compile(source, opts)?;
|
||||
let sealed_artifact = SealedArtifact::from_bytes(&output.artifact)
|
||||
.map_err(CompileError::Seal)?;
|
||||
let bytecode_bytes = el_seal::unseal(&sealed_artifact, binding_key)
|
||||
.map_err(CompileError::Seal)?;
|
||||
let instructions = deserialize_bytecode(&bytecode_bytes)
|
||||
.map_err(|e| CompileError::Codegen(e.to_string()))?;
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use el_seal::{DeploymentBinding, SealAlgorithm};
|
||||
use super::*;
|
||||
|
||||
fn debug_opts() -> CompilerOptions {
|
||||
CompilerOptions {
|
||||
target: Target::Debug,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn release_opts() -> CompilerOptions {
|
||||
CompilerOptions {
|
||||
target: Target::Release,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn prod_opts() -> CompilerOptions {
|
||||
CompilerOptions {
|
||||
target: Target::Prod,
|
||||
seal_config: SealConfig {
|
||||
algorithm: SealAlgorithm::Aes256Gcm,
|
||||
deployment_binding: DeploymentBinding::None,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_hello_world_debug() {
|
||||
let src = r#"let msg: String = "Hello, World!""#;
|
||||
let out = Compiler::compile(src, debug_opts()).unwrap();
|
||||
assert!(!out.artifact.is_empty());
|
||||
assert!(!out.sealed);
|
||||
assert!(out.source_map.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_release_no_source_map() {
|
||||
let src = "let x: Int = 42";
|
||||
let out = Compiler::compile(src, release_opts()).unwrap();
|
||||
assert!(out.source_map.is_none());
|
||||
assert!(!out.sealed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_prod_is_sealed() {
|
||||
let src = "let x: Int = 1";
|
||||
let out = Compiler::compile(src, prod_opts()).unwrap();
|
||||
assert!(out.sealed);
|
||||
// Artifact must start with ENGRAM01 magic
|
||||
assert_eq!(&out.artifact[..8], b"ENGRAM01");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prod_roundtrip() {
|
||||
let src = "let answer: Int = 42";
|
||||
let opts = prod_opts();
|
||||
let out = Compiler::compile(src, opts).unwrap();
|
||||
let sealed = SealedArtifact::from_bytes(&out.artifact).unwrap();
|
||||
let bytecode_bytes = el_seal::unseal(&sealed, &[]).unwrap();
|
||||
let instructions = deserialize_bytecode(&bytecode_bytes).unwrap();
|
||||
// Should have a PUSH 42, STORE answer, and HALT at minimum
|
||||
assert!(instructions.iter().any(|b| matches!(b, crate::bytecode::Bytecode::Push(crate::bytecode::Value::Int(42)))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_fn_def() {
|
||||
let src = r#"
|
||||
fn add(a: Int, b: Int) -> Int {
|
||||
return a + b
|
||||
}
|
||||
"#;
|
||||
let out = Compiler::compile(src, debug_opts()).unwrap();
|
||||
assert!(!out.artifact.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_type_mismatch_warning_debug() {
|
||||
// In debug mode, type errors are warnings (not fatal)
|
||||
let src = r#"let x: Int = "not an int""#;
|
||||
let out = Compiler::compile(src, debug_opts()).unwrap();
|
||||
assert!(!out.diagnostics.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_map_has_entries() {
|
||||
let src = "let x = 1\nlet y = 2";
|
||||
let out = Compiler::compile(src, debug_opts()).unwrap();
|
||||
let sm_json = out.source_map.unwrap();
|
||||
let sm: crate::source_map::SourceMap = serde_json::from_str(&sm_json).unwrap();
|
||||
assert!(!sm.entries.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! Step-debugger infrastructure for the Engram VM.
|
||||
//!
|
||||
//! The [`Debugger`] is attached to the bytecode interpreter and emits
|
||||
//! [`DebugEvent`]s as execution proceeds. IDEs and `el debug` consume these
|
||||
//! events to show variable state, call stack, and current source line.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::bytecode::Value;
|
||||
|
||||
/// A single frame on the call stack.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StackFrame {
|
||||
pub function_name: String,
|
||||
pub source_file: String,
|
||||
pub line: u32,
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
/// Controls how the debugger advances through bytecode.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StepMode {
|
||||
/// Run freely until the next breakpoint.
|
||||
Run,
|
||||
/// Execute exactly one statement, then pause.
|
||||
StepOver,
|
||||
/// Step into function calls (pause on first instruction of callee).
|
||||
StepInto,
|
||||
/// Run until the current frame returns, then pause.
|
||||
StepOut,
|
||||
}
|
||||
|
||||
/// An event emitted by the VM when in debug mode.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DebugEvent {
|
||||
/// Execution paused at a breakpoint.
|
||||
Breakpoint {
|
||||
offset: usize,
|
||||
frame: StackFrame,
|
||||
},
|
||||
/// Execution paused after a single step.
|
||||
Step {
|
||||
frame: StackFrame,
|
||||
locals: HashMap<String, Value>,
|
||||
},
|
||||
/// A function returned a value.
|
||||
Return {
|
||||
value: Value,
|
||||
},
|
||||
/// The VM encountered a runtime error.
|
||||
Error {
|
||||
message: String,
|
||||
frame: StackFrame,
|
||||
},
|
||||
}
|
||||
|
||||
/// The debugger attached to a running VM instance.
|
||||
///
|
||||
/// In debug mode the interpreter queries [`should_pause`] before each
|
||||
/// instruction. If it returns `true`, execution stops and a [`DebugEvent`]
|
||||
/// is emitted to the registered handler.
|
||||
pub struct Debugger {
|
||||
/// Bytecode offsets at which to pause execution.
|
||||
pub breakpoints: HashSet<usize>,
|
||||
/// Current stepping mode.
|
||||
pub step_mode: StepMode,
|
||||
/// Simulated call stack (maintained by the interpreter).
|
||||
pub call_stack: Vec<StackFrame>,
|
||||
/// Snapshot of local variables at the last pause.
|
||||
pub locals: HashMap<String, Value>,
|
||||
/// Events emitted since the last [`drain_events`] call.
|
||||
events: Vec<DebugEvent>,
|
||||
}
|
||||
|
||||
impl Debugger {
|
||||
/// Create a new debugger that will break on the very first instruction.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
breakpoints: HashSet::new(),
|
||||
step_mode: StepMode::StepOver,
|
||||
call_stack: vec![StackFrame {
|
||||
function_name: "<top>".into(),
|
||||
source_file: "<unknown>".into(),
|
||||
line: 1,
|
||||
col: 1,
|
||||
}],
|
||||
locals: HashMap::new(),
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a breakpoint at a bytecode offset.
|
||||
pub fn add_breakpoint(&mut self, offset: usize) {
|
||||
self.breakpoints.insert(offset);
|
||||
}
|
||||
|
||||
/// Remove a breakpoint.
|
||||
pub fn remove_breakpoint(&mut self, offset: usize) {
|
||||
self.breakpoints.remove(&offset);
|
||||
}
|
||||
|
||||
/// Returns `true` if the debugger should pause at `offset`.
|
||||
pub fn should_pause(&self, offset: usize) -> bool {
|
||||
if self.breakpoints.contains(&offset) {
|
||||
return true;
|
||||
}
|
||||
matches!(self.step_mode, StepMode::StepOver | StepMode::StepInto)
|
||||
}
|
||||
|
||||
/// Called by the interpreter when it pauses at `offset`.
|
||||
pub fn on_pause(&mut self, offset: usize, locals: HashMap<String, Value>) {
|
||||
self.locals = locals.clone();
|
||||
let frame = self.current_frame_cloned();
|
||||
if self.breakpoints.contains(&offset) {
|
||||
self.events.push(DebugEvent::Breakpoint { offset, frame });
|
||||
} else {
|
||||
self.events.push(DebugEvent::Step { frame, locals });
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a function is entered.
|
||||
pub fn push_frame(&mut self, function_name: String, source_file: String) {
|
||||
self.call_stack.push(StackFrame {
|
||||
function_name,
|
||||
source_file,
|
||||
line: 1,
|
||||
col: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when a function returns.
|
||||
pub fn pop_frame(&mut self, value: Value) {
|
||||
self.call_stack.pop();
|
||||
self.events.push(DebugEvent::Return { value });
|
||||
}
|
||||
|
||||
/// Record a runtime error.
|
||||
pub fn on_error(&mut self, message: String) {
|
||||
let frame = self.current_frame_cloned();
|
||||
self.events.push(DebugEvent::Error { message, frame });
|
||||
}
|
||||
|
||||
/// Update the source position of the top frame.
|
||||
pub fn update_position(&mut self, line: u32, col: u32) {
|
||||
if let Some(frame) = self.call_stack.last_mut() {
|
||||
frame.line = line;
|
||||
frame.col = col;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain and return all queued events.
|
||||
pub fn drain_events(&mut self) -> Vec<DebugEvent> {
|
||||
std::mem::take(&mut self.events)
|
||||
}
|
||||
|
||||
/// Current frame clone, or a sentinel if the stack is empty.
|
||||
fn current_frame_cloned(&self) -> StackFrame {
|
||||
self.call_stack.last().cloned().unwrap_or_else(|| StackFrame {
|
||||
function_name: String::new(),
|
||||
source_file: String::new(),
|
||||
line: 0,
|
||||
col: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Debugger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_breakpoint_triggers_pause() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::Run; // not stepping — only breakpoints
|
||||
dbg.add_breakpoint(5);
|
||||
assert!(!dbg.should_pause(0));
|
||||
assert!(!dbg.should_pause(4));
|
||||
assert!(dbg.should_pause(5));
|
||||
assert!(!dbg.should_pause(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_over_always_pauses() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::StepOver;
|
||||
assert!(dbg.should_pause(0));
|
||||
assert!(dbg.should_pause(99));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_into_always_pauses() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::StepInto;
|
||||
assert!(dbg.should_pause(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_mode_no_pause_without_breakpoint() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::Run;
|
||||
assert!(!dbg.should_pause(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_breakpoint() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::Run;
|
||||
dbg.add_breakpoint(10);
|
||||
assert!(dbg.should_pause(10));
|
||||
dbg.remove_breakpoint(10);
|
||||
assert!(!dbg.should_pause(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_pause_emits_step_event() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::StepOver;
|
||||
let mut locals = HashMap::new();
|
||||
locals.insert("x".into(), Value::Int(42));
|
||||
dbg.on_pause(0, locals.clone());
|
||||
let events = dbg.drain_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(events[0], DebugEvent::Step { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_pause_at_breakpoint_emits_breakpoint_event() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.step_mode = StepMode::Run;
|
||||
dbg.add_breakpoint(7);
|
||||
dbg.on_pause(7, HashMap::new());
|
||||
let events = dbg.drain_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(events[0], DebugEvent::Breakpoint { offset: 7, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_pop_frame() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.push_frame("my_fn".into(), "test.el".into());
|
||||
assert_eq!(dbg.call_stack.len(), 2);
|
||||
assert_eq!(dbg.call_stack[1].function_name, "my_fn");
|
||||
dbg.pop_frame(Value::Int(0));
|
||||
assert_eq!(dbg.call_stack.len(), 1);
|
||||
let events = dbg.drain_events();
|
||||
assert!(matches!(events[0], DebugEvent::Return { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_error_emits_error_event() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.on_error("division by zero".into());
|
||||
let events = dbg.drain_events();
|
||||
assert!(matches!(&events[0], DebugEvent::Error { message, .. } if message == "division by zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drain_clears_events() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.on_error("oops".into());
|
||||
let _ = dbg.drain_events();
|
||||
let events2 = dbg.drain_events();
|
||||
assert!(events2.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_position() {
|
||||
let mut dbg = Debugger::new();
|
||||
dbg.update_position(10, 5);
|
||||
assert_eq!(dbg.call_stack[0].line, 10);
|
||||
assert_eq!(dbg.call_stack[0].col, 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Compiler error type.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CompileError {
|
||||
#[error("lex error: {0}")]
|
||||
Lex(#[from] el_lexer::LexError),
|
||||
|
||||
#[error("parse error: {0}")]
|
||||
Parse(#[from] el_parser::ParseError),
|
||||
|
||||
#[error("type error: {0}")]
|
||||
Type(String),
|
||||
|
||||
#[error("codegen error: {0}")]
|
||||
Codegen(String),
|
||||
|
||||
#[error("seal error: {0}")]
|
||||
Seal(#[from] el_seal::SealError),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(String),
|
||||
}
|
||||
|
||||
pub type CompileResult<T> = Result<T, CompileError>;
|
||||
@@ -0,0 +1,35 @@
|
||||
//! el-compiler — Engram language compilation pipeline.
|
||||
//!
|
||||
//! Takes a source string and produces a compiled artifact for one of three
|
||||
//! targets: [`Target::Debug`], [`Target::Release`], or [`Target::Prod`].
|
||||
//!
|
||||
//! # Pipeline
|
||||
//!
|
||||
//! ```text
|
||||
//! Source ──lex──► Tokens ──parse──► AST ──typecheck──► Typed AST
|
||||
//! ──codegen──► Bytecode ──[seal]──► Artifact
|
||||
//! ```
|
||||
//!
|
||||
//! # Debug target
|
||||
//! Emits bytecode + a JSON source map (bytecode offset → source span).
|
||||
//!
|
||||
//! # Release target
|
||||
//! Emits bytecode only; no debug info; minor dead-code pruning.
|
||||
//!
|
||||
//! # Prod target
|
||||
//! Emits bytecode, then passes it through [`el_seal`] with the deployment
|
||||
//! key from `ENGRAM_SEAL_KEY`. The result is a [`SealedArtifact`] that
|
||||
//! cannot be decompiled without the key.
|
||||
|
||||
mod bytecode;
|
||||
mod codegen;
|
||||
mod compiler;
|
||||
mod debugger;
|
||||
mod error;
|
||||
mod source_map;
|
||||
|
||||
pub use bytecode::{Bytecode, Value, serialize_bytecode, deserialize_bytecode};
|
||||
pub use compiler::{CompileOutput, Compiler, CompilerOptions, Target};
|
||||
pub use debugger::{DebugEvent, Debugger, StackFrame, StepMode};
|
||||
pub use error::{CompileError, CompileResult};
|
||||
pub use source_map::SourceMap;
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Source map: maps bytecode instruction indices to source spans.
|
||||
//!
|
||||
//! Only emitted for the debug target. The JSON format is simple and can be
|
||||
//! consumed by any debugger or IDE extension.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use el_lexer::Span;
|
||||
|
||||
/// A single mapping entry: bytecode index → source span.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MapEntry {
|
||||
/// Index of the bytecode instruction (0-based).
|
||||
pub instruction: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub line: u32,
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
impl MapEntry {
|
||||
pub fn new(instruction: usize, span: Span) -> Self {
|
||||
Self {
|
||||
instruction,
|
||||
start: span.start,
|
||||
end: span.end,
|
||||
line: span.line,
|
||||
col: span.col,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The full source map for a compilation unit.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SourceMap {
|
||||
pub entries: Vec<MapEntry>,
|
||||
}
|
||||
|
||||
impl SourceMap {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Record that instruction at `index` was generated from `span`.
|
||||
pub fn record(&mut self, index: usize, span: Span) {
|
||||
self.entries.push(MapEntry::new(index, span));
|
||||
}
|
||||
|
||||
/// Look up the source span for a given instruction index.
|
||||
pub fn lookup(&self, index: usize) -> Option<&MapEntry> {
|
||||
self.entries.iter().rfind(|e| e.instruction <= index)
|
||||
}
|
||||
|
||||
/// Serialize to JSON string.
|
||||
pub fn to_json(&self) -> Result<String, String> {
|
||||
serde_json::to_string_pretty(self).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user