feat: engram-lang — new programming language, quantum-sealed prod target, spreading activation types

This commit is contained in:
Will Anderson
2026-04-27 18:46:51 -05:00
commit 9ced941590
5569 changed files with 8153 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
//! 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>),
}
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(", "))
}
}
}
}
/// 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,
// ── 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,
}
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::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"),
}
}
}
/// 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)
}
}
+384
View File
@@ -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)));
}
}
+249
View File
@@ -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());
}
}
+29
View File
@@ -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>;
+33
View File
@@ -0,0 +1,33 @@
//! 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 error;
mod source_map;
pub use bytecode::{Bytecode, Value};
pub use compiler::{CompileOutput, Compiler, CompilerOptions, Target};
pub use error::{CompileError, CompileResult};
pub use source_map::SourceMap;
+57
View File
@@ -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())
}
}