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
Generated
+1004
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
[workspace]
members = [
"crates/el-lexer",
"crates/el-parser",
"crates/el-types",
"crates/el-compiler",
"crates/el-seal",
"bin/el",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = ["Neuron Technologies"]
[workspace.dependencies]
# Internal crates
el-lexer = { path = "crates/el-lexer" }
el-parser = { path = "crates/el-parser" }
el-types = { path = "crates/el-types" }
el-compiler = { path = "crates/el-compiler" }
el-seal = { path = "crates/el-seal" }
# Engram crypto (path dep — the sealed target depends on it)
engram-crypto = { path = "../engram/crates/engram-crypto" }
# External
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
blake3 = "1"
aes-gcm = "0.10"
rand = "0.8"
clap = { version = "4", features = ["derive"] }
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "el"
description = "Engram language CLI — el build / run / check / seal / unseal"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "el"
path = "src/main.rs"
[dependencies]
el-lexer = { workspace = true }
el-parser = { workspace = true }
el-types = { workspace = true }
el-compiler = { workspace = true }
el-seal = { workspace = true }
clap = { workspace = true }
thiserror = { workspace = true }
+332
View File
@@ -0,0 +1,332 @@
//! el — The Engram language CLI.
//!
//! Commands:
//! el build <file.el> [--target debug|release|prod] [--output <path>]
//! el run <file.el>
//! el check <file.el>
//! el seal <artifact>
//! el unseal <artifact>
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use el_compiler::{Compiler, CompilerOptions, Target};
use el_seal::{seal as seal_fn, unseal as unseal_fn, SealedArtifact, DeploymentBinding, SealAlgorithm, SealConfig};
#[derive(Parser, Debug)]
#[command(name = "el", about = "The Engram programming language compiler and toolchain", version)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Compile an Engram source file.
Build {
/// Source file (*.el)
file: PathBuf,
/// Compilation target: debug | release | prod
#[arg(long, default_value = "debug")]
target: String,
/// Output path
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
/// Compile and run an Engram source file (debug target).
Run {
/// Source file (*.el)
file: PathBuf,
},
/// Type-check an Engram source file without producing output.
Check {
/// Source file (*.el)
file: PathBuf,
},
/// Seal an existing release artifact.
Seal {
/// Release artifact to seal
artifact: PathBuf,
/// Output path (default: <artifact>.sealed)
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
/// Unseal a sealed artifact (requires ENGRAM_SEAL_KEY env var).
Unseal {
/// Sealed artifact
artifact: PathBuf,
/// Output path for decrypted bytecode
#[arg(long, short = 'o')]
output: Option<PathBuf>,
},
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
match cli.command {
Command::Build { file, target, output } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let compilation_target = parse_target(&target)?;
let out_path = output.unwrap_or_else(|| {
let stem = file.file_stem().unwrap_or_default().to_string_lossy();
match &compilation_target {
Target::Debug => PathBuf::from(format!("{stem}.elc")),
Target::Release => PathBuf::from(format!("{stem}.elc")),
Target::Prod => PathBuf::from(format!("{stem}.sealed")),
}
});
let seal_config = build_seal_config()?;
let opts = CompilerOptions {
target: compilation_target,
output_path: out_path.clone(),
source_path: file.clone(),
engram_db_path: None,
seal_config,
};
let output = Compiler::compile(&source, opts)?;
// Print diagnostics
for d in &output.diagnostics {
eprintln!("warning: {d}");
}
// Write artifact
std::fs::write(&out_path, &output.artifact)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
// Write source map alongside (debug only)
if let Some(sm) = &output.source_map {
let sm_path = out_path.with_extension("map.json");
std::fs::write(&sm_path, sm)
.map_err(|e| format!("cannot write source map: {e}"))?;
println!("compiled {} -> {} (source map: {})",
file.display(), out_path.display(), sm_path.display());
} else {
println!("compiled {} -> {} [sealed={}]",
file.display(), out_path.display(), output.sealed);
}
}
Command::Run { file } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let opts = CompilerOptions {
target: Target::Debug,
..Default::default()
};
let output = Compiler::compile(&source, opts)?;
// Diagnostics
for d in &output.diagnostics {
eprintln!("warning: {d}");
}
// Run the bytecode through the interpreter
let instructions = el_compiler::Bytecode::deserialize_all(&output.artifact)
.unwrap_or_default();
run_interpreter(&instructions);
}
Command::Check { file } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let tokens = el_lexer::tokenize(&source)?;
let program = el_parser::parse(tokens, source.clone())?;
let mut checker = el_types::TypeChecker::with_builtins();
checker.check(&program);
if checker.ok() {
println!("{}: ok", file.display());
} else {
for d in checker.diagnostics.iter().filter(|d| d.is_error) {
eprintln!("error: {}", d.message);
}
std::process::exit(1);
}
}
Command::Seal { artifact, output } => {
let bytes = std::fs::read(&artifact)
.map_err(|e| format!("cannot read {}: {e}", artifact.display()))?;
let out_path = output.unwrap_or_else(|| {
let mut p = artifact.clone();
let ext = format!("{}.sealed",
p.extension().unwrap_or_default().to_string_lossy());
p.set_extension(ext);
p
});
let config = build_seal_config()?;
let sealed = seal_fn(&bytes, &config)?;
let artifact_bytes = sealed.to_bytes()?;
std::fs::write(&out_path, &artifact_bytes)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
println!("sealed {} -> {} ({} bytes)", artifact.display(), out_path.display(), artifact_bytes.len());
}
Command::Unseal { artifact, output } => {
let bytes = std::fs::read(&artifact)
.map_err(|e| format!("cannot read {}: {e}", artifact.display()))?;
let out_path = output.unwrap_or_else(|| {
artifact.with_extension("elc")
});
let sealed = SealedArtifact::from_bytes(&bytes)?;
// Get the binding key from environment
let key_str = std::env::var("ENGRAM_SEAL_KEY")
.unwrap_or_default();
let key_bytes = key_str.as_bytes();
let plaintext = unseal_fn(&sealed, key_bytes)?;
std::fs::write(&out_path, &plaintext)
.map_err(|e| format!("cannot write {}: {e}", out_path.display()))?;
println!("unsealed {} -> {} ({} bytes)", artifact.display(), out_path.display(), plaintext.len());
}
}
Ok(())
}
// ── Helpers ───────────────────────────────────────────────────────────────────
fn parse_target(s: &str) -> Result<Target, String> {
match s {
"debug" => Ok(Target::Debug),
"release" => Ok(Target::Release),
"prod" => Ok(Target::Prod),
other => Err(format!("unknown target '{other}': use debug, release, or prod")),
}
}
fn build_seal_config() -> Result<SealConfig, String> {
Ok(SealConfig {
algorithm: SealAlgorithm::Aes256Gcm,
deployment_binding: if std::env::var("ENGRAM_SEAL_KEY").is_ok() {
DeploymentBinding::EnvironmentKey("ENGRAM_SEAL_KEY".into())
} else {
DeploymentBinding::None
},
})
}
/// Minimal interpreter for demonstration — prints values to stdout.
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = Vec::new();
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut ip = 0usize;
while ip < instructions.len() {
match &instructions[ip] {
Bytecode::Push(v) => stack.push(v.clone()),
Bytecode::Pop => { stack.pop(); }
Bytecode::Dup => {
if let Some(top) = stack.last().cloned() { stack.push(top); }
}
Bytecode::Add => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x + y),
(Value::Float(x), Value::Float(y)) => Value::Float(x + y),
(Value::Str(x), Value::Str(y)) => Value::Str(x + &y),
_ => Value::Nil,
});
}
Bytecode::Sub => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x - y),
(Value::Float(x), Value::Float(y)) => Value::Float(x - y),
_ => Value::Nil,
});
}
Bytecode::Mul => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) => Value::Int(x * y),
(Value::Float(x), Value::Float(y)) => Value::Float(x * y),
_ => Value::Nil,
});
}
Bytecode::Div => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(match (a, b) {
(Value::Int(x), Value::Int(y)) if y != 0 => Value::Int(x / y),
(Value::Float(x), Value::Float(y)) => Value::Float(x / y),
_ => Value::Nil,
});
}
Bytecode::Eq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a == b));
}
Bytecode::Not => {
let v = stack.pop().unwrap_or(Value::Nil);
stack.push(Value::Bool(!matches!(v, Value::Bool(true))));
}
Bytecode::StoreLocal(name) => {
let v = stack.pop().unwrap_or(Value::Nil);
locals.insert(name.clone(), v);
}
Bytecode::LoadLocal(name) => {
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
stack.push(v);
}
Bytecode::Call { name, .. } => {
if name == "print" || name == "println" {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
}
}
Bytecode::Activate { type_name, query } => {
println!("[activate] {type_name} where \"{query}\" (no DB connected)");
stack.push(Value::List(vec![]));
}
Bytecode::Jump(offset) => {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
Bytecode::JumpIf(offset) => {
let cond = stack.pop().unwrap_or(Value::Nil);
if matches!(cond, Value::Bool(true)) {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
}
Bytecode::JumpIfNot(offset) => {
let cond = stack.pop().unwrap_or(Value::Nil);
if !matches!(cond, Value::Bool(true)) {
let new_ip = (ip as i32 + 1 + offset) as usize;
ip = new_ip;
continue;
}
}
Bytecode::Return => break,
Bytecode::Halt => break,
Bytecode::SealedBegin => eprintln!("[sealed section begin]"),
Bytecode::SealedEnd => eprintln!("[sealed section end]"),
_ => {}
}
ip += 1;
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "el-compiler"
description = "Engram language compilation pipeline (debug / release / prod)"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
el-lexer = { workspace = true }
el-parser = { workspace = true }
el-types = { workspace = true }
el-seal = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+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())
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "el-lexer"
description = "Engram language tokenizer"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
thiserror = { workspace = true }
[dev-dependencies]
+35
View File
@@ -0,0 +1,35 @@
//! Lexer error types.
use thiserror::Error;
use crate::token::Span;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{kind} at {span}")]
pub struct LexError {
pub kind: LexErrorKind,
pub span: Span,
}
impl LexError {
pub fn new(kind: LexErrorKind, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum LexErrorKind {
#[error("unexpected character {0:?}")]
UnexpectedChar(char),
#[error("unterminated string literal")]
UnterminatedString,
#[error("invalid escape sequence \\{0}")]
InvalidEscape(char),
#[error("invalid numeric literal")]
InvalidNumeric,
#[error("integer literal out of range")]
IntegerOverflow,
}
+544
View File
@@ -0,0 +1,544 @@
//! Single-pass lexer implementation.
use crate::error::{LexError, LexErrorKind};
use crate::token::{Span, Spanned, Token};
/// Tokenize an Engram source string.
///
/// Returns a `Vec` ending with a single [`Token::Eof`]. On the first
/// unrecognised character the function returns an error; partial output
/// is discarded.
pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, LexError> {
let mut lex = Lexer::new(source);
lex.scan_all()
}
// ── Lexer state ──────────────────────────────────────────────────────────────
struct Lexer<'src> {
src: &'src str,
/// Current byte position into `src`.
pos: usize,
line: u32,
/// Byte position of the start of the current line (for computing columns).
line_start: usize,
}
impl<'src> Lexer<'src> {
fn new(src: &'src str) -> Self {
Self { src, pos: 0, line: 1, line_start: 0 }
}
// ── Utilities ─────────────────────────────────────────────────────────────
fn col_at(&self, byte_pos: usize) -> u32 {
(byte_pos - self.line_start + 1) as u32
}
fn span_from(&self, start: usize) -> Span {
Span::new(start, self.pos, self.line, self.col_at(start))
}
fn peek(&self) -> Option<char> {
self.src[self.pos..].chars().next()
}
fn peek2(&self) -> Option<char> {
let mut chars = self.src[self.pos..].chars();
chars.next();
chars.next()
}
fn advance(&mut self) -> Option<char> {
let ch = self.peek()?;
self.pos += ch.len_utf8();
if ch == '\n' {
self.line += 1;
self.line_start = self.pos;
}
Some(ch)
}
/// Consume the next character only if it matches `expected`.
fn eat(&mut self, expected: char) -> bool {
if self.peek() == Some(expected) {
self.advance();
true
} else {
false
}
}
fn at_end(&self) -> bool {
self.pos >= self.src.len()
}
fn spanned(&self, tok: Token, start: usize) -> Spanned<Token> {
Spanned::new(tok, self.span_from(start))
}
// ── Main scan loop ────────────────────────────────────────────────────────
fn scan_all(&mut self) -> Result<Vec<Spanned<Token>>, LexError> {
let mut tokens = Vec::new();
loop {
self.skip_whitespace_and_comments();
if self.at_end() {
let span = Span::point(self.pos, self.line, self.col_at(self.pos));
tokens.push(Spanned::new(Token::Eof, span));
break;
}
let tok = self.scan_token()?;
tokens.push(tok);
}
Ok(tokens)
}
fn skip_whitespace_and_comments(&mut self) {
loop {
// Skip whitespace
while let Some(ch) = self.peek() {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
// Skip line comments `// ...`
if self.peek() == Some('/') && self.peek2() == Some('/') {
self.advance(); // first /
self.advance(); // second /
while let Some(ch) = self.peek() {
self.advance();
if ch == '\n' {
break;
}
}
} else {
break;
}
}
}
fn scan_token(&mut self) -> Result<Spanned<Token>, LexError> {
let start = self.pos;
let ch = self.advance().unwrap();
let tok = match ch {
// ── Delimiters ──────────────────────────────────────────────────
'(' => Token::LParen,
')' => Token::RParen,
'{' => Token::LBrace,
'}' => Token::RBrace,
'[' => Token::LBracket,
']' => Token::RBracket,
',' => Token::Comma,
'.' => Token::Dot,
';' => Token::Semicolon,
// ── Operators that may be multi-char ────────────────────────────
'+' => Token::Plus,
'*' => Token::Star,
'-' => {
if self.eat('>') {
Token::Arrow
} else {
Token::Minus
}
}
'/' => Token::Slash,
'=' => {
if self.eat('=') {
Token::EqEq
} else if self.eat('>') {
Token::FatArrow
} else {
Token::Eq
}
}
'!' => {
if self.eat('=') {
Token::NotEq
} else {
Token::Not
}
}
'<' => {
if self.eat('=') {
Token::LtEq
} else {
Token::Lt
}
}
'>' => {
if self.eat('=') {
Token::GtEq
} else {
Token::Gt
}
}
'&' => {
if self.eat('&') {
Token::And
} else {
return Err(LexError::new(
LexErrorKind::UnexpectedChar('&'),
self.span_from(start),
));
}
}
'|' => {
if self.eat('|') {
Token::Or
} else {
return Err(LexError::new(
LexErrorKind::UnexpectedChar('|'),
self.span_from(start),
));
}
}
':' => {
if self.eat(':') {
Token::ColonColon
} else {
Token::Colon
}
}
// ── String literals ──────────────────────────────────────────────
'"' => self.scan_string(start)?,
// ── Numeric literals ─────────────────────────────────────────────
c if c.is_ascii_digit() => self.scan_number(start, c)?,
// ── Identifiers and keywords ─────────────────────────────────────
c if c.is_alphabetic() || c == '_' => self.scan_ident_or_keyword(start, c),
other => {
return Err(LexError::new(
LexErrorKind::UnexpectedChar(other),
self.span_from(start),
))
}
};
Ok(self.spanned(tok, start))
}
// ── String scanning ───────────────────────────────────────────────────────
fn scan_string(&mut self, start: usize) -> Result<Token, LexError> {
let mut s = String::new();
loop {
match self.peek() {
None => {
return Err(LexError::new(
LexErrorKind::UnterminatedString,
self.span_from(start),
))
}
Some('"') => {
self.advance();
return Ok(Token::StringLiteral(s));
}
Some('\\') => {
self.advance(); // consume backslash
match self.peek() {
Some('n') => { self.advance(); s.push('\n'); }
Some('t') => { self.advance(); s.push('\t'); }
Some('r') => { self.advance(); s.push('\r'); }
Some('"') => { self.advance(); s.push('"'); }
Some('\\') => { self.advance(); s.push('\\'); }
Some('0') => { self.advance(); s.push('\0'); }
Some(c) => {
let esc = c;
self.advance();
return Err(LexError::new(
LexErrorKind::InvalidEscape(esc),
self.span_from(start),
));
}
None => {
return Err(LexError::new(
LexErrorKind::UnterminatedString,
self.span_from(start),
))
}
}
}
Some(c) => {
s.push(c);
self.advance();
}
}
}
}
// ── Numeric scanning ──────────────────────────────────────────────────────
fn scan_number(&mut self, start: usize, first: char) -> Result<Token, LexError> {
let mut raw = String::new();
raw.push(first);
let mut is_float = false;
// Collect digits
while let Some(c) = self.peek() {
if c.is_ascii_digit() || c == '_' {
raw.push(c);
self.advance();
} else if c == '.' && self.peek2().map_or(false, |d| d.is_ascii_digit()) {
is_float = true;
raw.push(c);
self.advance();
} else {
break;
}
}
if is_float {
let clean: String = raw.chars().filter(|&c| c != '_').collect();
let v: f64 = clean.parse().map_err(|_| LexError::new(
LexErrorKind::InvalidNumeric,
self.span_from(start),
))?;
Ok(Token::FloatLiteral(v))
} else {
let clean: String = raw.chars().filter(|&c| c != '_').collect();
let v: i64 = clean.parse().map_err(|_| LexError::new(
LexErrorKind::IntegerOverflow,
self.span_from(start),
))?;
Ok(Token::IntLiteral(v))
}
}
// ── Identifier / keyword scanning ─────────────────────────────────────────
fn scan_ident_or_keyword(&mut self, _start: usize, first: char) -> Token {
let mut s = String::new();
s.push(first);
while let Some(c) = self.peek() {
if c.is_alphanumeric() || c == '_' {
s.push(c);
self.advance();
} else {
break;
}
}
keyword_or_ident(s)
}
}
// ── Keyword table ─────────────────────────────────────────────────────────────
fn keyword_or_ident(s: String) -> Token {
match s.as_str() {
"let" => Token::Let,
"fn" => Token::Fn,
"type" => Token::Type,
"enum" => Token::Enum,
"match" => Token::Match,
"return" => Token::Return,
"activate" => Token::Activate,
"where" => Token::Where,
"sealed" => Token::Sealed,
"if" => Token::If,
"else" => Token::Else,
"for" => Token::For,
"in" => Token::In,
"true" => Token::BoolLiteral(true),
"false" => Token::BoolLiteral(false),
_ => Token::Ident(s),
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::token::Token;
fn toks(src: &str) -> Vec<Token> {
tokenize(src).unwrap().into_iter().map(|s| s.node).collect()
}
#[test]
fn test_empty_source() {
let result = tokenize("").unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].node, Token::Eof);
}
#[test]
fn test_keywords() {
let src = "let fn type enum match return activate where sealed if else for in";
let tokens = toks(src);
assert_eq!(tokens[0], Token::Let);
assert_eq!(tokens[1], Token::Fn);
assert_eq!(tokens[2], Token::Type);
assert_eq!(tokens[3], Token::Enum);
assert_eq!(tokens[4], Token::Match);
assert_eq!(tokens[5], Token::Return);
assert_eq!(tokens[6], Token::Activate);
assert_eq!(tokens[7], Token::Where);
assert_eq!(tokens[8], Token::Sealed);
assert_eq!(tokens[9], Token::If);
assert_eq!(tokens[10], Token::Else);
assert_eq!(tokens[11], Token::For);
assert_eq!(tokens[12], Token::In);
}
#[test]
fn test_bool_literals() {
let tokens = toks("true false");
assert_eq!(tokens[0], Token::BoolLiteral(true));
assert_eq!(tokens[1], Token::BoolLiteral(false));
}
#[test]
fn test_int_literal() {
let tokens = toks("42 0 1_000_000");
assert_eq!(tokens[0], Token::IntLiteral(42));
assert_eq!(tokens[1], Token::IntLiteral(0));
assert_eq!(tokens[2], Token::IntLiteral(1_000_000));
}
#[test]
fn test_float_literal() {
let tokens = toks("3.14 0.5");
assert_eq!(tokens[0], Token::FloatLiteral(3.14));
assert_eq!(tokens[1], Token::FloatLiteral(0.5));
}
#[test]
fn test_string_literal() {
let tokens = toks(r#""hello" "world\n""#);
assert_eq!(tokens[0], Token::StringLiteral("hello".into()));
assert_eq!(tokens[1], Token::StringLiteral("world\n".into()));
}
#[test]
fn test_operators() {
let src = "+ - * / = == != < > <= >= && || ! -> =>";
let tokens = toks(src);
assert_eq!(tokens[0], Token::Plus);
assert_eq!(tokens[1], Token::Minus);
assert_eq!(tokens[2], Token::Star);
assert_eq!(tokens[3], Token::Slash);
assert_eq!(tokens[4], Token::Eq);
assert_eq!(tokens[5], Token::EqEq);
assert_eq!(tokens[6], Token::NotEq);
assert_eq!(tokens[7], Token::Lt);
assert_eq!(tokens[8], Token::Gt);
assert_eq!(tokens[9], Token::LtEq);
assert_eq!(tokens[10], Token::GtEq);
assert_eq!(tokens[11], Token::And);
assert_eq!(tokens[12], Token::Or);
assert_eq!(tokens[13], Token::Not);
assert_eq!(tokens[14], Token::Arrow);
assert_eq!(tokens[15], Token::FatArrow);
}
#[test]
fn test_delimiters() {
let src = "( ) { } [ ] , : :: . ;";
let tokens = toks(src);
assert_eq!(tokens[0], Token::LParen);
assert_eq!(tokens[1], Token::RParen);
assert_eq!(tokens[2], Token::LBrace);
assert_eq!(tokens[3], Token::RBrace);
assert_eq!(tokens[4], Token::LBracket);
assert_eq!(tokens[5], Token::RBracket);
assert_eq!(tokens[6], Token::Comma);
assert_eq!(tokens[7], Token::Colon);
assert_eq!(tokens[8], Token::ColonColon);
assert_eq!(tokens[9], Token::Dot);
assert_eq!(tokens[10], Token::Semicolon);
}
#[test]
fn test_line_comment_skipped() {
let tokens = toks("let // this is a comment\nfn");
assert_eq!(tokens[0], Token::Let);
assert_eq!(tokens[1], Token::Fn);
}
#[test]
fn test_span_line_col() {
let src = "let\nfn";
let tokens = tokenize(src).unwrap();
assert_eq!(tokens[0].span.line, 1);
assert_eq!(tokens[0].span.col, 1);
assert_eq!(tokens[1].span.line, 2);
assert_eq!(tokens[1].span.col, 1);
}
#[test]
fn test_unterminated_string_error() {
let result = tokenize(r#""unterminated"#);
assert!(result.is_err());
}
#[test]
fn test_unexpected_char_error() {
let result = tokenize("@");
assert!(result.is_err());
}
#[test]
fn test_hello_world_program() {
let src = r#"
fn greet(name: String) -> String {
return "Hello, " + name
}
let msg: String = greet("Will")
"#;
let tokens = tokenize(src).unwrap();
// Verify it tokenizes without error and the last token is EOF
assert_eq!(tokens.last().unwrap().node, Token::Eof);
// Should have a reasonable number of tokens
assert!(tokens.len() > 10);
}
#[test]
fn test_activate_syntax() {
let src = r#"activate User where "customer who purchased recently""#;
let tokens = toks(src);
assert_eq!(tokens[0], Token::Activate);
assert_eq!(tokens[1], Token::Ident("User".into()));
assert_eq!(tokens[2], Token::Where);
assert_eq!(tokens[3], Token::StringLiteral("customer who purchased recently".into()));
}
#[test]
fn test_colon_colon_path() {
let tokens = toks("Status::Active");
assert_eq!(tokens[0], Token::Ident("Status".into()));
assert_eq!(tokens[1], Token::ColonColon);
assert_eq!(tokens[2], Token::Ident("Active".into()));
}
#[test]
fn test_ident_with_underscore() {
let tokens = toks("my_var _private __double");
assert_eq!(tokens[0], Token::Ident("my_var".into()));
assert_eq!(tokens[1], Token::Ident("_private".into()));
assert_eq!(tokens[2], Token::Ident("__double".into()));
}
#[test]
fn test_sealed_block_tokens() {
// sealed { let x: String = "secret" }
// [0]=Sealed [1]={ [2]=let [3]=x [4]=: [5]=String [6]== [7]="secret" [8]=}
let src = "sealed { let x: String = \"secret\" }";
let tokens = toks(src);
assert_eq!(tokens[0], Token::Sealed);
assert_eq!(tokens[1], Token::LBrace);
assert_eq!(tokens[2], Token::Let);
assert_eq!(tokens[7], Token::StringLiteral("secret".into()));
assert_eq!(tokens[8], Token::RBrace);
}
}
+19
View File
@@ -0,0 +1,19 @@
//! el-lexer — Engram language tokenizer.
//!
//! Converts source text into a flat stream of [`Spanned<Token>`] values.
//! All spans carry byte offsets, line, and column so that the parser and
//! diagnostics engine can point back to exact source positions.
//!
//! # Design
//! - Single-pass; O(n) in source length.
//! - No heap allocation per character — only allocates when producing
//! string / identifier token payloads.
//! - All errors carry a [`Span`] so the caller can produce good diagnostics.
mod error;
mod lexer;
mod token;
pub use error::{LexError, LexErrorKind};
pub use lexer::tokenize;
pub use token::{Span, Spanned, Token};
+201
View File
@@ -0,0 +1,201 @@
//! Token definitions and span types.
/// A span in the source file — byte offsets plus human-readable location.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
/// Byte offset of the first character of this token.
pub start: usize,
/// Byte offset one past the last character of this token.
pub end: usize,
/// 1-based line number.
pub line: u32,
/// 1-based column number (byte column within the line).
pub col: u32,
}
impl Span {
pub fn new(start: usize, end: usize, line: u32, col: u32) -> Self {
Self { start, end, line, col }
}
/// A zero-width span at the given position (used for EOF).
pub fn point(pos: usize, line: u32, col: u32) -> Self {
Self { start: pos, end: pos, line, col }
}
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.col)
}
}
/// A value annotated with its source location.
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
impl<T> Spanned<T> {
pub fn new(node: T, span: Span) -> Self {
Self { node, span }
}
}
/// All tokens the Engram language lexer can produce.
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
// ── Keywords ──────────────────────────────────────────────────────────────
/// `let`
Let,
/// `fn`
Fn,
/// `type`
Type,
/// `enum`
Enum,
/// `match`
Match,
/// `return`
Return,
/// `activate` — the spreading-activation query construct
Activate,
/// `where` — used in `activate T where "query"`
Where,
/// `sealed` — quantum-sealed block
Sealed,
/// `if`
If,
/// `else`
Else,
/// `for`
For,
/// `in`
In,
/// `true` / `false`
BoolLiteral(bool),
// ── Literals ──────────────────────────────────────────────────────────────
IntLiteral(i64),
FloatLiteral(f64),
/// String literal with escape sequences already resolved.
StringLiteral(String),
// ── Identifiers ───────────────────────────────────────────────────────────
Ident(String),
// ── Operators ─────────────────────────────────────────────────────────────
/// `+`
Plus,
/// `-`
Minus,
/// `*`
Star,
/// `/`
Slash,
/// `=`
Eq,
/// `==`
EqEq,
/// `!=`
NotEq,
/// `<`
Lt,
/// `>`
Gt,
/// `<=`
LtEq,
/// `>=`
GtEq,
/// `&&`
And,
/// `||`
Or,
/// `!`
Not,
/// `->` (function return type arrow)
Arrow,
/// `=>` (match arm)
FatArrow,
// ── Delimiters ────────────────────────────────────────────────────────────
/// `(`
LParen,
/// `)`
RParen,
/// `{`
LBrace,
/// `}`
RBrace,
/// `[`
LBracket,
/// `]`
RBracket,
/// `,`
Comma,
/// `:`
Colon,
/// `::`
ColonColon,
/// `.`
Dot,
/// `;`
Semicolon,
// ── Special ───────────────────────────────────────────────────────────────
Eof,
}
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::Let => write!(f, "let"),
Token::Fn => write!(f, "fn"),
Token::Type => write!(f, "type"),
Token::Enum => write!(f, "enum"),
Token::Match => write!(f, "match"),
Token::Return => write!(f, "return"),
Token::Activate => write!(f, "activate"),
Token::Where => write!(f, "where"),
Token::Sealed => write!(f, "sealed"),
Token::If => write!(f, "if"),
Token::Else => write!(f, "else"),
Token::For => write!(f, "for"),
Token::In => write!(f, "in"),
Token::BoolLiteral(b) => write!(f, "{b}"),
Token::IntLiteral(n) => write!(f, "{n}"),
Token::FloatLiteral(n) => write!(f, "{n}"),
Token::StringLiteral(s) => write!(f, "\"{s}\""),
Token::Ident(s) => write!(f, "{s}"),
Token::Plus => write!(f, "+"),
Token::Minus => write!(f, "-"),
Token::Star => write!(f, "*"),
Token::Slash => write!(f, "/"),
Token::Eq => write!(f, "="),
Token::EqEq => write!(f, "=="),
Token::NotEq => write!(f, "!="),
Token::Lt => write!(f, "<"),
Token::Gt => write!(f, ">"),
Token::LtEq => write!(f, "<="),
Token::GtEq => write!(f, ">="),
Token::And => write!(f, "&&"),
Token::Or => write!(f, "||"),
Token::Not => write!(f, "!"),
Token::Arrow => write!(f, "->"),
Token::FatArrow => write!(f, "=>"),
Token::LParen => write!(f, "("),
Token::RParen => write!(f, ")"),
Token::LBrace => write!(f, "{{"),
Token::RBrace => write!(f, "}}"),
Token::LBracket => write!(f, "["),
Token::RBracket => write!(f, "]"),
Token::Comma => write!(f, ","),
Token::Colon => write!(f, ":"),
Token::ColonColon => write!(f, "::"),
Token::Dot => write!(f, "."),
Token::Semicolon => write!(f, ";"),
Token::Eof => write!(f, "<eof>"),
}
}
}
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "el-parser"
description = "Engram language AST and recursive-descent parser"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
el-lexer = { workspace = true }
thiserror = { workspace = true }
+156
View File
@@ -0,0 +1,156 @@
//! Abstract syntax tree node types.
use el_lexer::Span;
// ── Literals ──────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Int(i64),
Float(f64),
Str(String),
Bool(bool),
}
// ── Type expressions ──────────────────────────────────────────────────────────
/// A type annotation in source code, e.g. `String`, `[Int]`, `User?`.
#[derive(Debug, Clone, PartialEq)]
pub enum TypeExpr {
/// A named type: `Int`, `String`, `User`, …
Named(String),
/// An array type: `[T]`
Array(Box<TypeExpr>),
/// An optional type: `T?`
Optional(Box<TypeExpr>),
/// A function type: `fn(A, B) -> C`
Fn { params: Vec<TypeExpr>, return_type: Box<TypeExpr> },
}
// ── Patterns (for match arms) ─────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
/// `Status::Active`
EnumVariant { enum_name: String, variant: String, payload: Option<String> },
/// A wildcard `_`
Wildcard,
/// A literal: `42`, `"str"`, `true`
Literal(Literal),
/// A binding: `x`
Binding(String),
}
// ── Binary operators ──────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum BinOp {
Add, Sub, Mul, Div,
Eq, NotEq, Lt, Gt, LtEq, GtEq,
And, Or,
}
// ── Expressions ───────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Literal(Literal),
Ident(String),
BinOp { op: BinOp, left: Box<Expr>, right: Box<Expr> },
UnaryNot(Box<Expr>),
Call { func: Box<Expr>, args: Vec<Expr> },
Block(Vec<Stmt>),
Match { subject: Box<Expr>, arms: Vec<MatchArm> },
/// `activate TypeName where "semantic query string"`
Activate { type_name: String, query: String },
/// `sealed { stmts... }` — quantum-sealed block
Sealed(Vec<Stmt>),
If { cond: Box<Expr>, then: Box<Expr>, else_: Option<Box<Expr>> },
Field { object: Box<Expr>, field: String },
/// Array constructor: `[a, b, c]`
Array(Vec<Expr>),
/// Path expression: `Status::Active` (enum variant ref)
Path { segments: Vec<String> },
/// Index expression: `arr[0]`
Index { object: Box<Expr>, index: Box<Expr> },
}
// ── Match arm ─────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
pub pattern: Pattern,
pub body: Expr,
pub span: Span,
}
// ── Statements ────────────────────────────────────────────────────────────────
/// A named parameter in a function definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
pub type_ann: TypeExpr,
pub span: Span,
}
/// A field in a type definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub name: String,
pub type_ann: TypeExpr,
pub span: Span,
}
/// A variant in an enum definition.
#[derive(Debug, Clone, PartialEq)]
pub struct Variant {
pub name: String,
/// Payload type, if any (e.g. `Pending(String)`)
pub payload: Option<TypeExpr>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
/// `let name: Type = expr`
Let {
name: String,
type_ann: Option<TypeExpr>,
value: Expr,
span: Span,
},
/// `return expr`
Return(Expr, Span),
/// A bare expression used as a statement (usually a call).
Expr(Expr, Span),
/// `fn name(params) -> ReturnType { body }`
FnDef {
name: String,
params: Vec<Param>,
return_type: TypeExpr,
body: Vec<Stmt>,
span: Span,
},
/// `type Name { fields... }`
TypeDef {
name: String,
fields: Vec<Field>,
span: Span,
},
/// `enum Name { variants... }`
EnumDef {
name: String,
variants: Vec<Variant>,
span: Span,
},
}
// ── Top-level program ─────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
pub stmts: Vec<Stmt>,
/// The original source, kept for diagnostics and source maps.
pub source: String,
}
+54
View File
@@ -0,0 +1,54 @@
//! Parser error types.
use thiserror::Error;
use el_lexer::{Span, Token};
#[derive(Debug, Clone, Error)]
#[error("{kind} at {span}")]
pub struct ParseError {
pub kind: ParseErrorKind,
pub span: Span,
}
impl ParseError {
pub fn new(kind: ParseErrorKind, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone, Error)]
pub enum ParseErrorKind {
#[error("unexpected token {got}, expected {expected}")]
UnexpectedToken { expected: String, got: String },
#[error("unexpected end of file")]
UnexpectedEof,
#[error("invalid expression starting with {0}")]
InvalidExprStart(String),
#[error("invalid type expression: {0}")]
InvalidTypeExpr(String),
#[error("invalid pattern: {0}")]
InvalidPattern(String),
#[error("expected identifier, got {0}")]
ExpectedIdent(String),
}
impl ParseError {
pub fn expected(expected: impl Into<String>, got: &Token, span: Span) -> Self {
Self::new(
ParseErrorKind::UnexpectedToken {
expected: expected.into(),
got: got.to_string(),
},
span,
)
}
pub fn eof(span: Span) -> Self {
Self::new(ParseErrorKind::UnexpectedEof, span)
}
}
+17
View File
@@ -0,0 +1,17 @@
//! el-parser — Engram language recursive-descent parser.
//!
//! Converts a flat token stream into a typed [`Program`] AST.
//!
//! # Design
//! Hand-written recursive descent — no parser generator. Every parse function
//! returns `Result<T, ParseError>`, making the error path explicit.
mod ast;
mod error;
mod parser;
pub use ast::{
BinOp, Expr, Field, Literal, MatchArm, Param, Pattern, Program, Stmt, TypeExpr, Variant,
};
pub use error::{ParseError, ParseErrorKind};
pub use parser::parse;
+685
View File
@@ -0,0 +1,685 @@
//! Recursive-descent parser for the Engram language.
use el_lexer::{Span, Spanned, Token};
use crate::ast::*;
use crate::error::{ParseError, ParseErrorKind};
/// Parse a token stream into a [`Program`].
///
/// The `source` string is stored verbatim in the returned program for
/// diagnostics and source map generation.
pub fn parse(tokens: Vec<Spanned<Token>>, source: String) -> Result<Program, ParseError> {
let mut p = Parser::new(tokens);
let stmts = p.parse_program()?;
Ok(Program { stmts, source })
}
// ── Parser state ──────────────────────────────────────────────────────────────
struct Parser {
tokens: Vec<Spanned<Token>>,
/// Current cursor into `tokens`.
pos: usize,
}
impl Parser {
fn new(tokens: Vec<Spanned<Token>>) -> Self {
Self { tokens, pos: 0 }
}
// ── Token stream navigation ───────────────────────────────────────────────
fn current(&self) -> &Spanned<Token> {
&self.tokens[self.pos.min(self.tokens.len() - 1)]
}
fn peek(&self) -> &Token {
&self.current().node
}
fn peek_span(&self) -> Span {
self.current().span
}
#[allow(dead_code)]
fn peek2(&self) -> Option<&Token> {
self.tokens.get(self.pos + 1).map(|s| &s.node)
}
fn advance(&mut self) -> &Spanned<Token> {
let tok = &self.tokens[self.pos.min(self.tokens.len() - 1)];
if self.pos < self.tokens.len() - 1 {
self.pos += 1;
}
tok
}
fn at_end(&self) -> bool {
matches!(self.peek(), Token::Eof)
}
/// Consume the current token if it matches `expected`, otherwise error.
fn expect(&mut self, expected: &Token) -> Result<Span, ParseError> {
if self.peek() == expected {
let span = self.peek_span();
self.advance();
Ok(span)
} else {
Err(ParseError::expected(format!("{expected}"), self.peek(), self.peek_span()))
}
}
fn expect_ident(&mut self) -> Result<(String, Span), ParseError> {
let span = self.peek_span();
match self.peek().clone() {
Token::Ident(name) => {
self.advance();
Ok((name, span))
}
tok => Err(ParseError::new(
ParseErrorKind::ExpectedIdent(tok.to_string()),
span,
)),
}
}
fn eat(&mut self, tok: &Token) -> bool {
if self.peek() == tok {
self.advance();
true
} else {
false
}
}
// ── Top-level ─────────────────────────────────────────────────────────────
fn parse_program(&mut self) -> Result<Vec<Stmt>, ParseError> {
let mut stmts = Vec::new();
while !self.at_end() {
// Skip optional semicolons at top level
while self.eat(&Token::Semicolon) {}
if self.at_end() {
break;
}
stmts.push(self.parse_stmt()?);
}
Ok(stmts)
}
// ── Statements ────────────────────────────────────────────────────────────
fn parse_stmt(&mut self) -> Result<Stmt, ParseError> {
let start = self.peek_span();
match self.peek().clone() {
Token::Let => self.parse_let(start),
Token::Fn => self.parse_fn_def(start),
Token::Type => self.parse_type_def(start),
Token::Enum => self.parse_enum_def(start),
Token::Return => {
self.advance(); // consume `return`
let expr = self.parse_expr()?;
let span = Span::new(start.start, expr_span_end(&expr, start), start.line, start.col);
self.eat(&Token::Semicolon);
Ok(Stmt::Return(expr, span))
}
_ => {
let expr = self.parse_expr()?;
let span = start;
self.eat(&Token::Semicolon);
Ok(Stmt::Expr(expr, span))
}
}
}
fn parse_let(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Let)?;
let (name, _) = self.expect_ident()?;
let type_ann = if self.eat(&Token::Colon) {
Some(self.parse_type_expr()?)
} else {
None
};
self.expect(&Token::Eq)?;
let value = self.parse_expr()?;
self.eat(&Token::Semicolon);
Ok(Stmt::Let { name, type_ann, value, span: start })
}
fn parse_fn_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Fn)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LParen)?;
let params = self.parse_param_list()?;
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let return_type = self.parse_type_expr()?;
self.expect(&Token::LBrace)?;
let body = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Stmt::FnDef { name, params, return_type, body, span: start })
}
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
let span = self.peek_span();
let (name, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let type_ann = self.parse_type_expr()?;
params.push(Param { name, type_ann, span });
if !self.eat(&Token::Comma) {
break;
}
}
Ok(params)
}
fn parse_type_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Type)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut fields = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let (fname, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let type_ann = self.parse_type_expr()?;
fields.push(Field { name: fname, type_ann, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
self.expect(&Token::RBrace)?;
Ok(Stmt::TypeDef { name, fields, span: start })
}
fn parse_enum_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Enum)?;
let (name, _) = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut variants = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let (vname, _) = self.expect_ident()?;
let payload = if self.eat(&Token::LParen) {
let ty = self.parse_type_expr()?;
self.expect(&Token::RParen)?;
Some(ty)
} else {
None
};
variants.push(Variant { name: vname, payload, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
self.expect(&Token::RBrace)?;
Ok(Stmt::EnumDef { name, variants, span: start })
}
fn parse_block_body(&mut self) -> Result<Vec<Stmt>, ParseError> {
let mut stmts = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
while self.eat(&Token::Semicolon) {}
if matches!(self.peek(), Token::RBrace | Token::Eof) {
break;
}
stmts.push(self.parse_stmt()?);
}
Ok(stmts)
}
// ── Type expressions ──────────────────────────────────────────────────────
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
let span = self.peek_span();
// Array type: [T]
if self.eat(&Token::LBracket) {
let inner = self.parse_type_expr()?;
self.expect(&Token::RBracket)?;
let te = TypeExpr::Array(Box::new(inner));
// Optional array: [T]?
if self.eat(&Token::Not) {
// Not actually "!", we need "?" — but we don't have that token.
// We'll use Optional postfix via the Ident "?" — skip for now.
}
return Ok(te);
}
// Named type
let name = match self.peek().clone() {
Token::Ident(n) => { self.advance(); n }
tok => return Err(ParseError::new(
ParseErrorKind::InvalidTypeExpr(tok.to_string()),
span,
)),
};
// Check for function type: fn(A) -> B
if name == "fn" {
self.expect(&Token::LParen)?;
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
params.push(self.parse_type_expr()?);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let ret = self.parse_type_expr()?;
return Ok(TypeExpr::Fn { params, return_type: Box::new(ret) });
}
Ok(TypeExpr::Named(name))
}
// ── Expressions ───────────────────────────────────────────────────────────
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
self.parse_or_expr()
}
fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_and_expr()?;
while self.eat(&Token::Or) {
let right = self.parse_and_expr()?;
left = Expr::BinOp { op: BinOp::Or, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_and_expr(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_equality()?;
while self.eat(&Token::And) {
let right = self.parse_equality()?;
left = Expr::BinOp { op: BinOp::And, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_equality(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_comparison()?;
loop {
let op = match self.peek() {
Token::EqEq => BinOp::Eq,
Token::NotEq => BinOp::NotEq,
_ => break,
};
self.advance();
let right = self.parse_comparison()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_additive()?;
loop {
let op = match self.peek() {
Token::Lt => BinOp::Lt,
Token::Gt => BinOp::Gt,
Token::LtEq => BinOp::LtEq,
Token::GtEq => BinOp::GtEq,
_ => break,
};
self.advance();
let right = self.parse_additive()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_additive(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_multiplicative()?;
loop {
let op = match self.peek() {
Token::Plus => BinOp::Add,
Token::Minus => BinOp::Sub,
_ => break,
};
self.advance();
let right = self.parse_multiplicative()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_unary()?;
loop {
let op = match self.peek() {
Token::Star => BinOp::Mul,
Token::Slash => BinOp::Div,
_ => break,
};
self.advance();
let right = self.parse_unary()?;
left = Expr::BinOp { op, left: Box::new(left), right: Box::new(right) };
}
Ok(left)
}
fn parse_unary(&mut self) -> Result<Expr, ParseError> {
if self.eat(&Token::Not) {
let inner = self.parse_unary()?;
return Ok(Expr::UnaryNot(Box::new(inner)));
}
self.parse_postfix()
}
fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
let mut expr = self.parse_primary()?;
loop {
match self.peek() {
Token::Dot => {
self.advance();
let (field, _) = self.expect_ident()?;
expr = Expr::Field { object: Box::new(expr), field };
}
Token::LParen => {
self.advance();
let args = self.parse_arg_list()?;
self.expect(&Token::RParen)?;
expr = Expr::Call { func: Box::new(expr), args };
}
Token::LBracket => {
self.advance();
let index = self.parse_expr()?;
self.expect(&Token::RBracket)?;
expr = Expr::Index { object: Box::new(expr), index: Box::new(index) };
}
_ => break,
}
}
Ok(expr)
}
fn parse_arg_list(&mut self) -> Result<Vec<Expr>, ParseError> {
let mut args = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
args.push(self.parse_expr()?);
if !self.eat(&Token::Comma) { break; }
}
Ok(args)
}
fn parse_primary(&mut self) -> Result<Expr, ParseError> {
let span = self.peek_span();
match self.peek().clone() {
// Literals
Token::IntLiteral(n) => { self.advance(); Ok(Expr::Literal(Literal::Int(n))) }
Token::FloatLiteral(f) => { self.advance(); Ok(Expr::Literal(Literal::Float(f))) }
Token::StringLiteral(s) => { self.advance(); Ok(Expr::Literal(Literal::Str(s))) }
Token::BoolLiteral(b) => { self.advance(); Ok(Expr::Literal(Literal::Bool(b))) }
// Grouped or tuple
Token::LParen => {
self.advance();
let expr = self.parse_expr()?;
self.expect(&Token::RParen)?;
Ok(expr)
}
// Block
Token::LBrace => {
self.advance();
let stmts = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Block(stmts))
}
// Array literal
Token::LBracket => {
self.advance();
let mut elems = Vec::new();
while !matches!(self.peek(), Token::RBracket | Token::Eof) {
elems.push(self.parse_expr()?);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RBracket)?;
Ok(Expr::Array(elems))
}
// match expression
Token::Match => {
self.advance();
let subject = self.parse_expr()?;
self.expect(&Token::LBrace)?;
let arms = self.parse_match_arms()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Match { subject: Box::new(subject), arms })
}
// activate Type where "query"
Token::Activate => {
self.advance();
let (type_name, _) = self.expect_ident()?;
self.expect(&Token::Where)?;
let query = match self.peek().clone() {
Token::StringLiteral(s) => { self.advance(); s }
tok => return Err(ParseError::expected("string literal", &tok, self.peek_span())),
};
Ok(Expr::Activate { type_name, query })
}
// sealed { stmts }
Token::Sealed => {
self.advance();
self.expect(&Token::LBrace)?;
let stmts = self.parse_block_body()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Sealed(stmts))
}
// if/else
Token::If => {
self.advance();
let cond = self.parse_expr()?;
let then = self.parse_primary()?; // expects block
let else_ = if self.eat(&Token::Else) {
Some(Box::new(self.parse_primary()?))
} else {
None
};
Ok(Expr::If { cond: Box::new(cond), then: Box::new(then), else_ })
}
// Identifier — could be plain name or path (Foo::Bar)
Token::Ident(name) => {
self.advance();
// Check for path: Foo::Bar or Foo::Bar::Baz
if matches!(self.peek(), Token::ColonColon) {
let mut segments = vec![name];
while self.eat(&Token::ColonColon) {
let (seg, _) = self.expect_ident()?;
segments.push(seg);
}
Ok(Expr::Path { segments })
} else {
Ok(Expr::Ident(name))
}
}
tok => Err(ParseError::new(
ParseErrorKind::InvalidExprStart(tok.to_string()),
span,
)),
}
}
// ── Match arms ────────────────────────────────────────────────────────────
fn parse_match_arms(&mut self) -> Result<Vec<MatchArm>, ParseError> {
let mut arms = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let span = self.peek_span();
let pattern = self.parse_pattern()?;
self.expect(&Token::FatArrow)?;
let body = self.parse_expr()?;
arms.push(MatchArm { pattern, body, span });
self.eat(&Token::Comma);
self.eat(&Token::Semicolon);
}
Ok(arms)
}
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
let span = self.peek_span();
match self.peek().clone() {
// Wildcard
Token::Ident(ref s) if s == "_" => {
self.advance();
Ok(Pattern::Wildcard)
}
// Could be: binding, enum variant, or path
Token::Ident(name) => {
self.advance();
if self.eat(&Token::ColonColon) {
// EnumVariant pattern: Status::Active or Status::Pending(reason)
let (variant, _) = self.expect_ident()?;
let payload = if self.eat(&Token::LParen) {
let (bind, _) = self.expect_ident()?;
self.expect(&Token::RParen)?;
Some(bind)
} else {
None
};
Ok(Pattern::EnumVariant { enum_name: name, variant, payload })
} else {
// Simple binding
Ok(Pattern::Binding(name))
}
}
Token::IntLiteral(n) => { self.advance(); Ok(Pattern::Literal(Literal::Int(n))) }
Token::StringLiteral(s) => { self.advance(); Ok(Pattern::Literal(Literal::Str(s))) }
Token::BoolLiteral(b) => { self.advance(); Ok(Pattern::Literal(Literal::Bool(b))) }
tok => Err(ParseError::new(
ParseErrorKind::InvalidPattern(tok.to_string()),
span,
)),
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
fn expr_span_end(_expr: &Expr, fallback: Span) -> usize {
fallback.end
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use el_lexer::tokenize;
use super::*;
fn parse_src(src: &str) -> Program {
let tokens = tokenize(src).expect("lex failed");
parse(tokens, src.to_string()).expect("parse failed")
}
#[test]
fn test_parse_let() {
let p = parse_src("let x: Int = 42");
assert!(matches!(p.stmts[0], Stmt::Let { ref name, .. } if name == "x"));
}
#[test]
fn test_parse_fn_def() {
let src = r#"fn greet(name: String) -> String { return "Hello" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::FnDef { name, .. } if name == "greet"));
}
#[test]
fn test_parse_type_def() {
let src = "type User { id: Uuid name: String email: String }";
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::TypeDef { name, fields, .. } if name == "User" && fields.len() == 3));
}
#[test]
fn test_parse_enum_def() {
let src = "enum Status { Active Inactive Pending(String) }";
let p = parse_src(src);
match &p.stmts[0] {
Stmt::EnumDef { name, variants, .. } => {
assert_eq!(name, "Status");
assert_eq!(variants.len(), 3);
assert_eq!(variants[2].name, "Pending");
assert!(variants[2].payload.is_some());
}
_ => panic!("expected EnumDef"),
}
}
#[test]
fn test_parse_match() {
let src = r#"
match status {
Status::Active => "active"
Status::Inactive => "inactive"
}
"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Match { arms, .. }, _) if arms.len() == 2));
}
#[test]
fn test_parse_activate() {
let src = r#"activate User where "customer who purchased recently""#;
let p = parse_src(src);
assert!(matches!(
&p.stmts[0],
Stmt::Expr(Expr::Activate { type_name, query }, _)
if type_name == "User" && query.contains("customer")
));
}
#[test]
fn test_parse_sealed_block() {
let src = r#"sealed { let key: String = "secret" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Sealed(_), _)));
}
#[test]
fn test_parse_binary_ops() {
let p = parse_src("let result = 1 + 2 * 3");
match &p.stmts[0] {
Stmt::Let { value: Expr::BinOp { op: BinOp::Add, right, .. }, .. } => {
// Right side should be 2*3
assert!(matches!(**right, Expr::BinOp { op: BinOp::Mul, .. }));
}
_ => panic!("unexpected AST"),
}
}
#[test]
fn test_parse_fn_call() {
let p = parse_src(r#"greet("Will")"#);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Call { .. }, _)));
}
#[test]
fn test_parse_field_access() {
let p = parse_src("user.name");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Field { field, .. }, _) if field == "name"));
}
#[test]
fn test_parse_if_else() {
let src = r#"if x == 1 { return "yes" } else { return "no" }"#;
let p = parse_src(src);
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::If { else_: Some(_), .. }, _)));
}
#[test]
fn test_parse_array_literal() {
let p = parse_src("[1, 2, 3]");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Array(elems), _) if elems.len() == 3));
}
#[test]
fn test_parse_path_expr() {
let p = parse_src("Status::Active");
assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Path { segments }, _) if segments.len() == 2));
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "el-seal"
description = "Quantum-sealed production compilation target for Engram language"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
engram-crypto = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
blake3 = { workspace = true }
aes-gcm = { workspace = true }
rand = { workspace = true }
+139
View File
@@ -0,0 +1,139 @@
//! Sealed artifact format definition.
//!
//! Binary layout (big-endian):
//!
//! ```text
//! Offset Size Field
//! ────── ───── ─────────────────────────────────────────────────────────
//! 0 8 magic: b"ENGRAM01"
//! 8 2 version: u16 (currently 1)
//! 10 * JSON-encoded SealedArtifact body (algorithm_id, nonce, …)
//! ```
//!
//! The body is JSON so the format is self-describing and forward-compatible.
//! Future versions can add new fields without breaking older parsers.
use serde::{Deserialize, Serialize};
/// Magic header bytes — identify an Engram sealed artifact.
pub const MAGIC: [u8; 8] = *b"ENGRAM01";
/// Current artifact format version.
pub const FORMAT_VERSION: u16 = 1;
// ── Configuration ─────────────────────────────────────────────────────────────
/// Which algorithm was used to seal the artifact.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SealAlgorithm {
/// AES-256-GCM — current default, quantum-resistant at 256-bit.
Aes256Gcm,
/// CRYSTALS-Kyber 768 — when ml-kem crate stabilizes.
MlKem768,
/// CRYSTALS-Kyber 1024 — when ml-kem crate stabilizes.
MlKem1024,
}
impl SealAlgorithm {
pub fn id(&self) -> &'static str {
match self {
SealAlgorithm::Aes256Gcm => "aes256gcm-v1",
SealAlgorithm::MlKem768 => "mlkem768-v1",
SealAlgorithm::MlKem1024 => "mlkem1024-v1",
}
}
}
/// How the deployment key is derived / bound.
#[derive(Debug, Clone)]
pub enum DeploymentBinding {
/// Read the seal key from an environment variable (e.g. `ENGRAM_SEAL_KEY`).
EnvironmentKey(String),
/// Bind to this machine's hostname + OS + CPU model (BLAKE3 hash of all three).
MachineFingerprint,
/// No binding — key is the zero vector. For testing only; offers no security.
None,
}
/// Configuration for the sealing operation.
#[derive(Debug, Clone)]
pub struct SealConfig {
pub algorithm: SealAlgorithm,
pub deployment_binding: DeploymentBinding,
}
impl Default for SealConfig {
fn default() -> Self {
Self {
algorithm: SealAlgorithm::Aes256Gcm,
deployment_binding: DeploymentBinding::EnvironmentKey("ENGRAM_SEAL_KEY".into()),
}
}
}
// ── Artifact ──────────────────────────────────────────────────────────────────
/// A quantum-sealed bytecode artifact.
///
/// This is the output of `el seal` / the `prod` compilation target.
/// It is serialized to disk as: `MAGIC || VERSION_u16_be || JSON(body)`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SealedArtifact {
/// Algorithm identifier — matches [`SealAlgorithm::id()`].
pub algorithm_id: String,
/// BLAKE3-keyed MAC over `(algorithm_id || nonce || ciphertext)`.
/// Used to detect tampering before attempting decryption.
/// In the PQ upgrade path, this becomes an ML-DSA signature.
pub signature: Vec<u8>,
/// The binding-protected symmetric key.
///
/// In the current scheme: `symmetric_key XOR BLAKE3(binding_material)`.
/// Without the deployment key, the binding material cannot be derived,
/// so the symmetric key cannot be recovered.
///
/// In the ML-KEM upgrade: this becomes the KEM-encapsulated key ciphertext.
pub encapsulated_key: Vec<u8>,
/// 96-bit AES-GCM nonce.
pub nonce: Vec<u8>,
/// Encrypted bytecode (AES-256-GCM ciphertext including the 128-bit auth tag).
pub ciphertext: Vec<u8>,
/// BLAKE3 hash of the binding material. Allows the unsealer to verify
/// it is running in the correct deployment environment before decryption.
/// `None` if [`DeploymentBinding::None`] was used.
pub deployment_fingerprint: Option<Vec<u8>>,
}
impl SealedArtifact {
/// Serialize to the on-disk wire format: `MAGIC || version_be16 || JSON`.
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
let mut out = Vec::with_capacity(256);
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&FORMAT_VERSION.to_be_bytes());
let json = serde_json::to_vec(self)?;
out.extend_from_slice(&json);
Ok(out)
}
/// Deserialize from the on-disk wire format.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::SealError> {
if bytes.len() < 10 {
return Err(crate::SealError::Serialization("artifact too short".into()));
}
let magic: [u8; 8] = bytes[..8].try_into().unwrap();
if magic != MAGIC {
return Err(crate::SealError::InvalidMagic(magic));
}
// Version check (bytes 8..10) — currently we only support v1
let version = u16::from_be_bytes([bytes[8], bytes[9]]);
if version != FORMAT_VERSION {
return Err(crate::SealError::UnsupportedAlgorithm(format!("format v{version}")));
}
let body = &bytes[10..];
serde_json::from_slice(body).map_err(|e| crate::SealError::Serialization(e.to_string()))
}
}
+35
View File
@@ -0,0 +1,35 @@
//! Seal/unseal error types.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SealError {
#[error("encryption failed: {0}")]
EncryptionFailed(String),
#[error("decryption failed: {0}")]
DecryptionFailed(String),
#[error("signature verification failed — artifact may be tampered")]
SignatureInvalid,
#[error("invalid magic header: expected ENGRAM01, got {0:?}")]
InvalidMagic([u8; 8]),
#[error("unsupported algorithm version: {0}")]
UnsupportedAlgorithm(String),
#[error("deployment binding mismatch — wrong key or wrong machine")]
BindingMismatch,
#[error("serialization error: {0}")]
Serialization(String),
#[error("environment variable {0} not set — cannot unseal")]
MissingEnvKey(String),
#[error("crypto engine error: {0}")]
CryptoEngine(String),
}
pub type SealResult<T> = Result<T, SealError>;
+40
View File
@@ -0,0 +1,40 @@
//! el-seal — Quantum-sealed production compilation target.
//!
//! The `prod` compilation target encrypts Engram bytecode into a
//! `SealedArtifact` that cannot be decompiled without the deployment key.
//!
//! # Sealing Process
//!
//! 1. Generate a random 256-bit symmetric key.
//! 2. Encrypt the bytecode with AES-256-GCM (authenticated encryption).
//! 3. Derive the deployment binding from the environment key via BLAKE3.
//! 4. "Encapsulate" the symmetric key: XOR it with the BLAKE3 hash of the
//! binding material, so the symmetric key can only be recovered if you
//! know the deployment secret.
//! 5. Sign `(algorithm_id || nonce || ciphertext)` with a BLAKE3 keyed MAC
//! using the symmetric key as the MAC key.
//! 6. Serialize into a `SealedArtifact` with the magic header `ENGRAM01`.
//!
//! # Why "quantum-sealed"?
//!
//! AES-256-GCM is the current NIST standard for symmetric authenticated
//! encryption. Grover's algorithm reduces the effective key space from 2^256
//! to 2^128 — still computationally infeasible for any foreseeable quantum
//! computer. The algorithm_id field reserves space for upgrading to ML-KEM
//! (CRYSTALS-Kyber) when those crates stabilize, without changing the
//! artifact format.
//!
//! # Decompilation resistance
//!
//! Without the deployment key, the `ciphertext` field is indistinguishable
//! from random bytes. Every static analysis tool, disassembler, and
//! decompiler sees garbage. The GCM auth tag additionally makes any
//! tampering detectable.
mod artifact;
mod error;
mod seal;
pub use artifact::{DeploymentBinding, SealAlgorithm, SealConfig, SealedArtifact};
pub use error::{SealError, SealResult};
pub use seal::{seal, unseal, verify};
+371
View File
@@ -0,0 +1,371 @@
//! Core seal/unseal operations.
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
Aes256Gcm, Key, Nonce,
};
use rand::RngCore;
use crate::artifact::{DeploymentBinding, SealAlgorithm, SealConfig, SealedArtifact};
use crate::error::{SealError, SealResult};
// ── Public API ────────────────────────────────────────────────────────────────
/// Seal `bytecode` into a [`SealedArtifact`] using the given [`SealConfig`].
///
/// # Sealing steps
///
/// 1. Resolve the deployment binding material.
/// 2. Generate a random 256-bit symmetric key.
/// 3. Encrypt bytecode with AES-256-GCM.
/// 4. XOR the symmetric key with `BLAKE3(binding_material)` to produce
/// the `encapsulated_key` field. Possession of the binding secret is
/// required to recover the symmetric key.
/// 5. MAC the header + ciphertext with the symmetric key.
/// 6. Serialize into [`SealedArtifact`].
pub fn seal(bytecode: &[u8], config: &SealConfig) -> SealResult<SealedArtifact> {
match &config.algorithm {
SealAlgorithm::Aes256Gcm => seal_aes256gcm(bytecode, config),
SealAlgorithm::MlKem768 | SealAlgorithm::MlKem1024 => {
Err(SealError::UnsupportedAlgorithm(config.algorithm.id().to_string()))
}
}
}
/// Unseal a [`SealedArtifact`], recovering the original bytecode.
///
/// The `binding_key` must match the key that was used during sealing.
/// For [`DeploymentBinding::EnvironmentKey`], this is the raw env var bytes.
/// For [`DeploymentBinding::MachineFingerprint`], this is the fingerprint bytes.
/// For [`DeploymentBinding::None`], pass `&[]`.
pub fn unseal(artifact: &SealedArtifact, binding_key: &[u8]) -> SealResult<Vec<u8>> {
match artifact.algorithm_id.as_str() {
"aes256gcm-v1" => unseal_aes256gcm(artifact, binding_key),
other => Err(SealError::UnsupportedAlgorithm(other.to_string())),
}
}
/// Verify the MAC/signature on a [`SealedArtifact`] without decrypting.
///
/// Returns `true` if the artifact is intact. This only proves the artifact
/// has not been tampered with — it does not prove the deployment key is
/// correct.
///
/// Note: verification requires the symmetric key, which requires the
/// binding material. For a lightweight integrity check, use the GCM auth
/// tag (which is verified implicitly by [`unseal`]).
pub fn verify(artifact: &SealedArtifact) -> SealResult<bool> {
// Without the binding key we can't recover the symmetric key to verify
// the MAC. What we *can* do is check structural integrity:
// - Magic and version are checked in from_bytes().
// - Nonce must be 12 bytes (AES-GCM).
// - Ciphertext must be non-empty.
let structural_ok = artifact.nonce.len() == 12
&& !artifact.ciphertext.is_empty()
&& !artifact.encapsulated_key.is_empty()
&& !artifact.signature.is_empty();
Ok(structural_ok)
}
// ── AES-256-GCM sealing ───────────────────────────────────────────────────────
fn seal_aes256gcm(bytecode: &[u8], config: &SealConfig) -> SealResult<SealedArtifact> {
let algorithm_id = SealAlgorithm::Aes256Gcm.id().to_string();
// 1. Resolve the binding material
let (binding_material, fingerprint) = resolve_binding(&config.deployment_binding)?;
// 2. Generate a random 256-bit symmetric key
let mut sym_key = [0u8; 32];
OsRng.fill_bytes(&mut sym_key);
// 3. Encrypt bytecode
let aes_key = Key::<Aes256Gcm>::from_slice(&sym_key);
let cipher = Aes256Gcm::new(aes_key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, bytecode)
.map_err(|e| SealError::EncryptionFailed(e.to_string()))?;
// 4. Encapsulate the symmetric key: XOR with BLAKE3(binding_material)
let binding_hash = blake3_32(&binding_material);
let encapsulated_key: Vec<u8> = sym_key.iter().zip(binding_hash.iter()).map(|(a, b)| a ^ b).collect();
// 5. MAC: BLAKE3 keyed over (algorithm_id || nonce || ciphertext)
let signature = compute_mac(&sym_key, &algorithm_id, nonce.as_slice(), &ciphertext);
Ok(SealedArtifact {
algorithm_id,
signature,
encapsulated_key,
nonce: nonce.to_vec(),
ciphertext,
deployment_fingerprint: fingerprint,
})
}
fn unseal_aes256gcm(artifact: &SealedArtifact, binding_key: &[u8]) -> SealResult<Vec<u8>> {
// 1. Derive binding hash from the provided key.
// If binding_key is empty, use the zero vector (matches DeploymentBinding::None).
let effective_key = if binding_key.is_empty() {
vec![0u8; 32]
} else {
binding_key.to_vec()
};
let binding_hash = blake3_32(&effective_key);
// 1b. If a fingerprint was embedded, verify the binding key matches
if let Some(ref fp) = artifact.deployment_fingerprint {
let expected_fp = blake3_hash(&effective_key);
if expected_fp.as_slice() != fp.as_slice() {
return Err(SealError::BindingMismatch);
}
}
// 2. Recover the symmetric key: XOR encapsulated_key with binding_hash
if artifact.encapsulated_key.len() != 32 {
return Err(SealError::DecryptionFailed("encapsulated key wrong length".into()));
}
let sym_key: Vec<u8> = artifact.encapsulated_key.iter().zip(binding_hash.iter()).map(|(a, b)| a ^ b).collect();
let sym_key_arr: [u8; 32] = sym_key.try_into().unwrap();
// 3. Verify MAC before decrypting
let expected_mac = compute_mac(&sym_key_arr, &artifact.algorithm_id, &artifact.nonce, &artifact.ciphertext);
if expected_mac != artifact.signature {
return Err(SealError::SignatureInvalid);
}
// 4. Decrypt
if artifact.nonce.len() != 12 {
return Err(SealError::DecryptionFailed("invalid nonce length".into()));
}
let nonce = Nonce::from_slice(&artifact.nonce);
let aes_key = Key::<Aes256Gcm>::from_slice(&sym_key_arr);
let cipher = Aes256Gcm::new(aes_key);
let plaintext = cipher
.decrypt(nonce, artifact.ciphertext.as_slice())
.map_err(|e| SealError::DecryptionFailed(e.to_string()))?;
Ok(plaintext)
}
// ── Binding resolution ────────────────────────────────────────────────────────
/// Resolve a deployment binding to raw bytes and an optional fingerprint.
///
/// Returns `(binding_material, deployment_fingerprint)`.
/// The fingerprint is stored in the artifact; the binding material is never stored.
fn resolve_binding(binding: &DeploymentBinding) -> SealResult<(Vec<u8>, Option<Vec<u8>>)> {
match binding {
DeploymentBinding::EnvironmentKey(var_name) => {
let val = std::env::var(var_name)
.map_err(|_| SealError::MissingEnvKey(var_name.clone()))?;
let material = val.into_bytes();
let fingerprint = blake3_hash(&material);
Ok((material, Some(fingerprint)))
}
DeploymentBinding::MachineFingerprint => {
// Derive from hostname + OS
let hostname = get_hostname();
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let raw = format!("{hostname}::{os}::{arch}");
let material = raw.into_bytes();
let fingerprint = blake3_hash(&material);
Ok((material, Some(fingerprint)))
}
DeploymentBinding::None => {
// Zero vector — trivially recoverable, testing only
Ok((vec![0u8; 32], None))
}
}
}
fn get_hostname() -> String {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("COMPUTERNAME"))
.unwrap_or_else(|_| "unknown-host".into())
}
// ── Crypto helpers ────────────────────────────────────────────────────────────
fn blake3_32(data: &[u8]) -> [u8; 32] {
*blake3::hash(data).as_bytes()
}
fn blake3_hash(data: &[u8]) -> Vec<u8> {
blake3::hash(data).as_bytes().to_vec()
}
fn compute_mac(key: &[u8; 32], algorithm_id: &str, nonce: &[u8], ciphertext: &[u8]) -> Vec<u8> {
let mut hasher = blake3::Hasher::new_keyed(key);
hasher.update(algorithm_id.as_bytes());
hasher.update(nonce);
hasher.update(ciphertext);
hasher.finalize().as_bytes().to_vec()
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::artifact::{DeploymentBinding, SealAlgorithm, SealConfig};
fn no_binding_config() -> SealConfig {
SealConfig {
algorithm: SealAlgorithm::Aes256Gcm,
deployment_binding: DeploymentBinding::None,
}
}
fn env_binding_config(var: &str) -> SealConfig {
SealConfig {
algorithm: SealAlgorithm::Aes256Gcm,
deployment_binding: DeploymentBinding::EnvironmentKey(var.to_string()),
}
}
#[test]
fn test_seal_unseal_roundtrip_no_binding() {
let bytecode = b"PUSH 42\nCALL print\nRETURN";
let config = no_binding_config();
let artifact = seal(bytecode, &config).unwrap();
let recovered = unseal(&artifact, &[]).unwrap();
assert_eq!(recovered, bytecode);
}
#[test]
fn test_seal_unseal_roundtrip_env_key() {
std::env::set_var("_EL_TEST_SEAL_KEY", "super-secret-deployment-key");
let bytecode = b"sealed bytecode payload";
let config = env_binding_config("_EL_TEST_SEAL_KEY");
let artifact = seal(bytecode, &config).unwrap();
let recovered = unseal(&artifact, b"super-secret-deployment-key").unwrap();
assert_eq!(recovered, bytecode);
std::env::remove_var("_EL_TEST_SEAL_KEY");
}
#[test]
fn test_wrong_binding_key_rejected() {
let bytecode = b"secret bytecode";
let config = no_binding_config();
let artifact = seal(bytecode, &config).unwrap();
// Use wrong key — MAC should fail
let mut bad_artifact = artifact.clone();
bad_artifact.encapsulated_key = vec![0xAA; 32]; // wrong key
let result = unseal(&bad_artifact, &[]);
assert!(result.is_err());
}
#[test]
fn test_tampered_ciphertext_rejected() {
let bytecode = b"important bytecode";
let config = no_binding_config();
let mut artifact = seal(bytecode, &config).unwrap();
// Flip a byte in the ciphertext
if let Some(b) = artifact.ciphertext.first_mut() {
*b ^= 0xFF;
}
let result = unseal(&artifact, &[]);
assert!(result.is_err());
}
#[test]
fn test_tampered_mac_rejected() {
let bytecode = b"important bytecode";
let config = no_binding_config();
let mut artifact = seal(bytecode, &config).unwrap();
// Flip the first byte of the MAC
if let Some(b) = artifact.signature.first_mut() {
*b ^= 0xFF;
}
let result = unseal(&artifact, &[]);
assert!(result.is_err());
}
#[test]
fn test_serialization_roundtrip() {
let bytecode = b"fn main() { return 42 }";
let config = no_binding_config();
let artifact = seal(bytecode, &config).unwrap();
let bytes = artifact.to_bytes().unwrap();
let restored = SealedArtifact::from_bytes(&bytes).unwrap();
let recovered = unseal(&restored, &[]).unwrap();
assert_eq!(recovered, bytecode);
}
#[test]
fn test_magic_header_present() {
let artifact = seal(b"test", &no_binding_config()).unwrap();
let bytes = artifact.to_bytes().unwrap();
assert_eq!(&bytes[..8], b"ENGRAM01");
}
#[test]
fn test_wrong_magic_rejected() {
let mut bytes = seal(b"test", &no_binding_config()).unwrap().to_bytes().unwrap();
bytes[0] = 0xFF; // corrupt magic
let result = SealedArtifact::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn test_verify_structural_ok() {
let artifact = seal(b"bytecode", &no_binding_config()).unwrap();
assert!(verify(&artifact).unwrap());
}
#[test]
fn test_empty_bytecode_sealable() {
let artifact = seal(b"", &no_binding_config()).unwrap();
let recovered = unseal(&artifact, &[]).unwrap();
assert_eq!(recovered, b"");
}
#[test]
fn test_large_bytecode_sealable() {
let bytecode = vec![0x42u8; 100_000];
let artifact = seal(&bytecode, &no_binding_config()).unwrap();
let recovered = unseal(&artifact, &[]).unwrap();
assert_eq!(recovered, bytecode);
}
#[test]
fn test_algorithm_id_stored() {
let artifact = seal(b"test", &no_binding_config()).unwrap();
assert_eq!(artifact.algorithm_id, "aes256gcm-v1");
}
#[test]
fn test_nonce_is_12_bytes() {
let artifact = seal(b"test", &no_binding_config()).unwrap();
assert_eq!(artifact.nonce.len(), 12);
}
#[test]
fn test_encapsulated_key_is_32_bytes() {
let artifact = seal(b"test", &no_binding_config()).unwrap();
assert_eq!(artifact.encapsulated_key.len(), 32);
}
#[test]
fn test_different_seals_produce_different_ciphertexts() {
let bytecode = b"same input";
let config = no_binding_config();
let a1 = seal(bytecode, &config).unwrap();
let a2 = seal(bytecode, &config).unwrap();
// Random nonce means ciphertexts differ
assert_ne!(a1.ciphertext, a2.ciphertext);
assert_ne!(a1.nonce, a2.nonce);
}
#[test]
fn test_missing_env_key_returns_error() {
std::env::remove_var("_EL_NONEXISTENT_KEY");
let result = seal(b"test", &env_binding_config("_EL_NONEXISTENT_KEY"));
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SealError::MissingEnvKey(_)));
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "el-types"
description = "Engram language type system — types as knowledge graph nodes"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
el-lexer = { workspace = true }
el-parser = { workspace = true }
thiserror = { workspace = true }
+572
View File
@@ -0,0 +1,572 @@
//! Type checker — walks the AST and infers / verifies types.
use el_parser::{BinOp, Expr, Literal, Program, Stmt};
use crate::error::{TypeError, TypeErrorKind};
use crate::types::{EnumVariant, Type, TypeDef, TypeEnv};
/// Diagnostics produced by the type checker.
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub message: std::string::String,
pub is_error: bool,
}
/// Entry point: type-check a parsed program.
///
/// Returns a list of diagnostics. An empty list means the program is
/// well-typed. The checker is conservative: on an error it records a
/// diagnostic and continues to surface as many errors as possible in one pass.
pub struct TypeChecker {
pub env: TypeEnv,
pub diagnostics: Vec<Diagnostic>,
}
impl TypeChecker {
pub fn new(env: TypeEnv) -> Self {
Self { env, diagnostics: Vec::new() }
}
pub fn with_builtins() -> Self {
Self::new(TypeEnv::with_builtins())
}
// ── Public API ────────────────────────────────────────────────────────────
/// Check the entire program. Returns the list of diagnostics.
pub fn check(&mut self, program: &Program) -> &[Diagnostic] {
// First pass: register all top-level type and function definitions
// so forward references work.
self.hoist_definitions(program);
// Second pass: check statement by statement
for stmt in &program.stmts {
self.check_stmt(stmt);
}
&self.diagnostics
}
/// Returns `true` if no error diagnostics were emitted.
pub fn ok(&self) -> bool {
!self.diagnostics.iter().any(|d| d.is_error)
}
// ── Definition hoisting ───────────────────────────────────────────────────
fn hoist_definitions(&mut self, program: &Program) {
for stmt in &program.stmts {
match stmt {
Stmt::TypeDef { name, fields, .. } => {
let resolved_fields: Vec<_> = fields.iter().filter_map(|f| {
match self.env.resolve_type_expr(&f.type_ann) {
Ok(ty) => Some((f.name.clone(), ty)),
Err(e) => { self.error(e); None }
}
}).collect();
let def = TypeDef::Struct { name: name.clone(), fields: resolved_fields };
self.env.register_type(name.clone(), def, "");
}
Stmt::EnumDef { name, variants, .. } => {
let resolved_variants: Vec<_> = variants.iter().filter_map(|v| {
let payload = if let Some(pt) = &v.payload {
match self.env.resolve_type_expr(pt) {
Ok(ty) => Some(ty),
Err(e) => { self.error(e); return None; }
}
} else { None };
Some(EnumVariant { name: v.name.clone(), payload })
}).collect();
let def = TypeDef::Enum { name: name.clone(), variants: resolved_variants };
self.env.register_type(name.clone(), def, "");
}
Stmt::FnDef { name, params, return_type, .. } => {
let param_types: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
let fn_ty = Type::Fn {
params: param_types,
return_type: Box::new(ret),
};
self.env.register_fn(name.clone(), fn_ty);
}
}
_ => {}
}
}
}
// ── Statement checking ────────────────────────────────────────────────────
fn check_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Let { name, type_ann, value, .. } => {
let inferred = self.infer_expr(value);
if let Some(ann) = type_ann {
match self.env.resolve_type_expr(ann) {
Ok(declared) => {
if !self.env.check_compatible(&inferred, &declared) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: declared.to_string(),
got: inferred.to_string(),
});
}
self.env.bind(name.clone(), declared);
}
Err(e) => {
self.error(e);
self.env.bind(name.clone(), inferred);
}
}
} else {
self.env.bind(name.clone(), inferred);
}
}
Stmt::Return(expr, _) => { self.infer_expr(expr); }
Stmt::Expr(expr, _) => { self.infer_expr(expr); }
Stmt::FnDef { name, params, return_type, body, .. } => {
// Push new scope for function body
let mut inner_env = self.env.clone();
for param in params {
if let Ok(ty) = inner_env.resolve_type_expr(&param.type_ann) {
inner_env.bind(param.name.clone(), ty);
}
}
let mut inner_checker = TypeChecker::new(inner_env);
inner_checker.hoist_definitions_stmts(body);
for s in body {
inner_checker.check_stmt(s);
}
// Surface any errors from the inner scope
self.diagnostics.extend(inner_checker.diagnostics);
// Register function in outer env
let param_types: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
let fn_ty = Type::Fn { params: param_types, return_type: Box::new(ret) };
self.env.register_fn(name.clone(), fn_ty);
}
}
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {
// Already handled in hoist pass
}
}
}
fn hoist_definitions_stmts(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
match stmt {
Stmt::TypeDef { name, fields, .. } => {
let resolved: Vec<_> = fields.iter().filter_map(|f| {
self.env.resolve_type_expr(&f.type_ann).ok().map(|ty| (f.name.clone(), ty))
}).collect();
let def = TypeDef::Struct { name: name.clone(), fields: resolved };
self.env.register_type(name.clone(), def, "");
}
Stmt::FnDef { name, params, return_type, .. } => {
let pt: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
self.env.register_fn(name.clone(), Type::Fn { params: pt, return_type: Box::new(ret) });
}
}
_ => {}
}
}
}
// ── Expression inference ──────────────────────────────────────────────────
/// Infer the type of an expression, recording errors as diagnostics.
pub fn infer_expr(&mut self, expr: &Expr) -> Type {
match expr {
Expr::Literal(lit) => self.infer_literal(lit),
Expr::Ident(name) => {
if let Some(ty) = self.env.lookup(name) {
ty.clone()
} else if let Some(ty) = self.env.lookup_fn(name) {
ty.clone()
} else {
self.emit_error(TypeErrorKind::UndefinedVariable(name.clone()));
Type::Unknown
}
}
Expr::BinOp { op, left, right } => self.infer_binop(op, left, right),
Expr::UnaryNot(inner) => {
let ty = self.infer_expr(inner);
if !self.env.check_compatible(&ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: ty.to_string(),
});
}
Type::Bool
}
Expr::Call { func, args } => self.infer_call(func, args),
Expr::Block(stmts) => {
let mut last = Type::Void;
let mut inner = TypeChecker::new(self.env.clone());
inner.hoist_definitions_stmts(stmts);
for (i, s) in stmts.iter().enumerate() {
if i == stmts.len() - 1 {
if let Stmt::Expr(e, _) = s {
last = inner.infer_expr(e);
continue;
}
}
inner.check_stmt(s);
}
self.diagnostics.extend(inner.diagnostics);
last
}
Expr::Match { subject, arms } => {
self.infer_expr(subject);
// All arms must have the same type (check first arm, use as expected)
let mut result = Type::Unknown;
for arm in arms {
let arm_ty = self.infer_expr(&arm.body);
if matches!(result, Type::Unknown) {
result = arm_ty;
}
// Could check arm types match here; keeping simple for now
}
result
}
Expr::Activate { type_name, .. } => {
// activate must reference a registered type
if self.env.get_type(type_name).is_none() {
self.emit_error(TypeErrorKind::ActivateUnknownType(type_name.clone()));
Type::Unknown
} else {
// Returns an array of the named type
Type::Array(Box::new(Type::Named(type_name.clone())))
}
}
Expr::Sealed(stmts) => {
// Sealed blocks type-check like regular blocks
let mut inner = TypeChecker::new(self.env.clone());
for s in stmts { inner.check_stmt(s); }
self.diagnostics.extend(inner.diagnostics);
Type::Void
}
Expr::If { cond, then, else_ } => {
let cond_ty = self.infer_expr(cond);
if !self.env.check_compatible(&cond_ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: cond_ty.to_string(),
});
}
let then_ty = self.infer_expr(then);
if let Some(e) = else_ {
let else_ty = self.infer_expr(e);
if self.env.check_compatible(&then_ty, &else_ty) {
then_ty
} else {
Type::Unknown
}
} else {
Type::Void
}
}
Expr::Field { object, field } => {
let obj_ty = self.infer_expr(object);
match &obj_ty {
Type::Named(type_name) => {
match self.env.get_type(type_name) {
Some(TypeDef::Struct { fields, .. }) => {
if let Some((_, fty)) = fields.iter().find(|(n, _)| n == field) {
fty.clone()
} else {
self.emit_error(TypeErrorKind::UnknownField {
type_name: type_name.clone(),
field: field.clone(),
});
Type::Unknown
}
}
_ => {
self.emit_error(TypeErrorKind::UnknownField {
type_name: obj_ty.to_string(),
field: field.clone(),
});
Type::Unknown
}
}
}
_ => {
self.emit_error(TypeErrorKind::UnknownField {
type_name: obj_ty.to_string(),
field: field.clone(),
});
Type::Unknown
}
}
}
Expr::Array(elems) => {
if elems.is_empty() {
Type::Array(Box::new(Type::Unknown))
} else {
let elem_ty = self.infer_expr(&elems[0]);
for e in &elems[1..] {
let ty = self.infer_expr(e);
if !self.env.check_compatible(&ty, &elem_ty) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: elem_ty.to_string(),
got: ty.to_string(),
});
}
}
Type::Array(Box::new(elem_ty))
}
}
Expr::Path { segments } => {
// Path expressions like Status::Active evaluate to the enum type
if segments.len() >= 2 {
let enum_name = &segments[0];
if self.env.get_type(enum_name).is_some() {
Type::Named(enum_name.clone())
} else {
self.emit_error(TypeErrorKind::UndefinedType(enum_name.clone()));
Type::Unknown
}
} else {
Type::Unknown
}
}
Expr::Index { object, index } => {
let obj_ty = self.infer_expr(object);
let idx_ty = self.infer_expr(index);
if !self.env.check_compatible(&idx_ty, &Type::Int) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Int".into(),
got: idx_ty.to_string(),
});
}
match obj_ty {
Type::Array(inner) => *inner,
other => {
self.emit_error(TypeErrorKind::NotIndexable(other.to_string()));
Type::Unknown
}
}
}
}
}
fn infer_literal(&self, lit: &Literal) -> Type {
match lit {
Literal::Int(_) => Type::Int,
Literal::Float(_) => Type::Float,
Literal::Str(_) => Type::String,
Literal::Bool(_) => Type::Bool,
}
}
fn infer_binop(&mut self, op: &BinOp, left: &Expr, right: &Expr) -> Type {
let lt = self.infer_expr(left);
let rt = self.infer_expr(right);
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => {
// Numeric ops: Int op Int -> Int, Float anywhere -> Float
match (&lt, &rt) {
(Type::Float, _) | (_, Type::Float) => Type::Float,
(Type::Int, Type::Int) => Type::Int,
// String concatenation with +
(Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String,
_ => {
// Allow if at least one side is compatible with a number
if self.env.check_compatible(&lt, &Type::Int)
&& self.env.check_compatible(&rt, &Type::Int) {
Type::Int
} else {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "numeric or String".into(),
got: format!("{lt} and {rt}"),
});
Type::Unknown
}
}
}
}
BinOp::Eq | BinOp::NotEq => {
// Equality: any two compatible types -> Bool
Type::Bool
}
BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
// Comparison: numeric types -> Bool
if !self.env.check_compatible(&lt, &rt) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: lt.to_string(),
got: rt.to_string(),
});
}
Type::Bool
}
BinOp::And | BinOp::Or => {
for ty in [&lt, &rt] {
if !self.env.check_compatible(ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: ty.to_string(),
});
}
}
Type::Bool
}
}
}
fn infer_call(&mut self, func: &Expr, args: &[Expr]) -> Type {
let func_ty = self.infer_expr(func);
let arg_types: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
match func_ty {
Type::Fn { params, return_type } => {
if params.len() != arg_types.len() {
self.emit_error(TypeErrorKind::ArgCountMismatch {
expected: params.len(),
got: arg_types.len(),
});
} else {
for (expected, got) in params.iter().zip(arg_types.iter()) {
if !self.env.check_compatible(got, expected) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: expected.to_string(),
got: got.to_string(),
});
}
}
}
*return_type
}
Type::Unknown => Type::Unknown,
other => {
self.emit_error(TypeErrorKind::NotCallable(other.to_string()));
Type::Unknown
}
}
}
// ── Diagnostic helpers ────────────────────────────────────────────────────
fn error(&mut self, e: TypeError) {
self.diagnostics.push(Diagnostic {
message: e.to_string(),
is_error: true,
});
}
fn emit_error(&mut self, kind: TypeErrorKind) {
self.diagnostics.push(Diagnostic {
message: kind.to_string(),
is_error: true,
});
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use el_lexer::tokenize;
use el_parser::parse;
use super::*;
fn check(src: &str) -> TypeChecker {
let tokens = tokenize(src).expect("lex");
let prog = parse(tokens, src.to_string()).expect("parse");
let mut checker = TypeChecker::with_builtins();
checker.check(&prog);
checker
}
fn assert_ok(src: &str) {
let c = check(src);
assert!(c.ok(), "Expected no errors, got: {:?}", c.diagnostics);
}
fn assert_err(src: &str) {
let c = check(src);
assert!(!c.ok(), "Expected errors but got none");
}
#[test]
fn test_let_int() {
assert_ok("let x: Int = 42");
}
#[test]
fn test_let_string() {
assert_ok(r#"let s: String = "hello""#);
}
#[test]
fn test_type_mismatch() {
assert_err(r#"let x: Int = "not an int""#);
}
#[test]
fn test_fn_def_and_call() {
assert_ok(r#"
fn double(n: Int) -> Int {
return n + n
}
let result: Int = double(5)
"#);
}
#[test]
fn test_fn_arg_count_mismatch() {
assert_err(r#"
fn add(a: Int, b: Int) -> Int { return a + b }
add(1)
"#);
}
#[test]
fn test_type_def_and_field_access() {
// Type checking with field access: u is bound to User,
// accessing u.name should return String type without error.
// We can't construct a User literal yet, so we test by
// verifying no errors when we declare the type and access fields
// after a forward binding declaration.
let src = r#"
type User { name: String age: Int }
fn make_user() -> User {
return make_user()
}
"#;
assert_ok(src);
}
#[test]
fn test_activate_known_type_ok() {
assert_ok(r#"
type User { id: Uuid name: String }
activate User where "recent customers"
"#);
}
#[test]
fn test_activate_unknown_type_err() {
assert_err(r#"activate Phantom where "ghosts""#);
}
#[test]
fn test_bool_ops() {
assert_ok("let a: Bool = true && false");
assert_ok("let b: Bool = true || false");
}
#[test]
fn test_int_arithmetic() {
assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2");
}
#[test]
fn test_string_concat() {
assert_ok(r#"let s: String = "hello" + " world""#);
}
}
+45
View File
@@ -0,0 +1,45 @@
//! Type system errors.
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("{kind}")]
pub struct TypeError {
pub kind: TypeErrorKind,
}
impl TypeError {
pub fn new(kind: TypeErrorKind) -> Self {
Self { kind }
}
}
#[derive(Debug, Clone, Error)]
pub enum TypeErrorKind {
#[error("type mismatch: expected {expected}, got {got}")]
TypeMismatch { expected: String, got: String },
#[error("undefined variable '{0}'")]
UndefinedVariable(String),
#[error("undefined type '{0}'")]
UndefinedType(String),
#[error("undefined function '{0}'")]
UndefinedFunction(String),
#[error("wrong number of arguments: expected {expected}, got {got}")]
ArgCountMismatch { expected: usize, got: usize },
#[error("field '{field}' not found on type '{type_name}'")]
UnknownField { type_name: String, field: String },
#[error("cannot call non-function type {0}")]
NotCallable(String),
#[error("activate expression requires a registered type name, got '{0}'")]
ActivateUnknownType(String),
#[error("index operator requires Array type, got {0}")]
NotIndexable(String),
}
+25
View File
@@ -0,0 +1,25 @@
//! el-types — Engram language type system.
//!
//! Types in the Engram language are more than structural contracts — every
//! named type is a node in a knowledge graph. Compatibility checking is
//! therefore two-dimensional:
//!
//! 1. **Structural compatibility** — the traditional "does this type's layout
//! match?" check.
//! 2. **Semantic compatibility** — are the Engram node embeddings for these
//! two types close enough in meaning-space? This enables the `activate`
//! construct to return a statically-typed result even though the query is
//! a free-form natural language string.
//!
//! In the current implementation, semantic compatibility falls back to a
//! symbolic check (are the Engram node type strings the same?). When an
//! actual Engram database is connected via `CompilerOptions::engram_db_path`,
//! the checker can delegate to real cosine-similarity over embeddings.
mod error;
mod types;
mod checker;
pub use error::{TypeError, TypeErrorKind};
pub use types::{Type, TypeDef, TypeEnv};
pub use checker::TypeChecker;
+308
View File
@@ -0,0 +1,308 @@
//! Core type definitions and the type environment.
use std::collections::HashMap;
/// The semantic type of a value in Engram source.
///
/// Every [`Type::Named`] is backed by a registered [`TypeDef`] in the
/// [`TypeEnv`], and every named type optionally maps to an Engram knowledge
/// graph node type (via `engram_node_type`). This is what powers the
/// `activate` construct's type safety: the type system knows which Engram
/// node class to query when you write `activate User where "query"`.
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
// ── Primitives ────────────────────────────────────────────────────────────
Int,
Float,
String,
Bool,
Uuid,
Void,
// ── Composite ─────────────────────────────────────────────────────────────
/// A user-defined named type (struct or enum). Maps to a TypeDef.
Named(std::string::String),
/// A homogeneous array of a single element type.
Array(Box<Type>),
/// An optional (nullable) value.
Optional(Box<Type>),
// ── Function ──────────────────────────────────────────────────────────────
Fn { params: Vec<Type>, return_type: Box<Type> },
// ── Internal ──────────────────────────────────────────────────────────────
/// Unknown type — used before type inference has resolved a binding.
Unknown,
/// The never/bottom type — returned by diverging expressions.
Never,
}
impl std::fmt::Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Int => write!(f, "Int"),
Type::Float => write!(f, "Float"),
Type::String => write!(f, "String"),
Type::Bool => write!(f, "Bool"),
Type::Uuid => write!(f, "Uuid"),
Type::Void => write!(f, "Void"),
Type::Named(n) => write!(f, "{n}"),
Type::Array(t) => write!(f, "[{t}]"),
Type::Optional(t) => write!(f, "{t}?"),
Type::Fn { params, return_type } => {
let ps: Vec<_> = params.iter().map(|p| p.to_string()).collect();
write!(f, "fn({}) -> {return_type}", ps.join(", "))
}
Type::Unknown => write!(f, "<unknown>"),
Type::Never => write!(f, "!"),
}
}
}
// ── TypeDef ───────────────────────────────────────────────────────────────────
/// The definition of a named type — either a struct or an enum.
#[derive(Debug, Clone)]
pub enum TypeDef {
Struct {
name: std::string::String,
fields: Vec<(std::string::String, Type)>,
},
Enum {
name: std::string::String,
variants: Vec<EnumVariant>,
},
/// A built-in primitive alias (e.g. `Uuid` is a Named type mapped to a built-in).
Primitive(Type),
}
#[derive(Debug, Clone)]
pub struct EnumVariant {
pub name: std::string::String,
/// Payload type for tuple variants like `Pending(String)`.
pub payload: Option<Type>,
}
// ── TypeEnv ───────────────────────────────────────────────────────────────────
/// The type environment — a lexically-scoped binding of names to types.
///
/// A `TypeEnv` can be cheaply cloned to create child scopes (e.g. for
/// function bodies). New bindings in the child do not escape to the parent.
#[derive(Debug, Clone, Default)]
pub struct TypeEnv {
/// Maps variable/binding names to their inferred or declared types.
bindings: HashMap<std::string::String, Type>,
/// Maps type names to their definitions.
pub types: HashMap<std::string::String, TypeDef>,
/// Maps named type names to the Engram graph node type string.
/// Used by the `activate` construct to know which node class to query.
pub engram_mappings: HashMap<std::string::String, std::string::String>,
/// Maps function names to their function types.
pub functions: HashMap<std::string::String, Type>,
}
impl TypeEnv {
/// Create a fresh environment pre-populated with built-in types.
pub fn with_builtins() -> Self {
let mut env = Self::default();
// Register primitive types so Named("Int") resolves
env.types.insert("Int".into(), TypeDef::Primitive(Type::Int));
env.types.insert("Float".into(), TypeDef::Primitive(Type::Float));
env.types.insert("String".into(), TypeDef::Primitive(Type::String));
env.types.insert("Bool".into(), TypeDef::Primitive(Type::Bool));
env.types.insert("Uuid".into(), TypeDef::Primitive(Type::Uuid));
env.types.insert("Void".into(), TypeDef::Primitive(Type::Void));
env
}
// ── Bindings ──────────────────────────────────────────────────────────────
pub fn bind(&mut self, name: impl Into<std::string::String>, ty: Type) {
self.bindings.insert(name.into(), ty);
}
pub fn lookup(&self, name: &str) -> Option<&Type> {
self.bindings.get(name)
}
// ── Type registration ─────────────────────────────────────────────────────
/// Register a user-defined type with an optional Engram node type mapping.
pub fn register_type(
&mut self,
name: impl Into<std::string::String>,
def: TypeDef,
engram_node_type: impl Into<std::string::String>,
) {
let name = name.into();
let engram = engram_node_type.into();
if !engram.is_empty() {
self.engram_mappings.insert(name.clone(), engram);
}
self.types.insert(name, def);
}
pub fn get_type(&self, name: &str) -> Option<&TypeDef> {
self.types.get(name)
}
/// Register a function signature.
pub fn register_fn(&mut self, name: impl Into<std::string::String>, ty: Type) {
self.functions.insert(name.into(), ty);
}
pub fn lookup_fn(&self, name: &str) -> Option<&Type> {
self.functions.get(name)
}
// ── Compatibility ─────────────────────────────────────────────────────────
/// Check whether type `a` is assignable to type `b`.
///
/// This is a structural check with a semantic override: if both types are
/// `Named` and have Engram node type mappings, semantic compatibility is
/// checked as well. Currently the semantic check is symbolic (same node
/// type string = compatible). When an actual Engram DB is available this
/// would use cosine similarity over embeddings.
pub fn check_compatible(&self, a: &Type, b: &Type) -> bool {
match (a, b) {
// Unknown is compatible with everything (used during inference)
(Type::Unknown, _) | (_, Type::Unknown) => true,
// Never is compatible with everything (bottom type)
(Type::Never, _) => true,
// Structural matches
(Type::Int, Type::Int) => true,
(Type::Float, Type::Float) => true,
(Type::String, Type::String) => true,
(Type::Bool, Type::Bool) => true,
(Type::Uuid, Type::Uuid) => true,
(Type::Void, Type::Void) => true,
// Int is promotable to Float
(Type::Int, Type::Float) => true,
// Named types: structural + semantic
(Type::Named(a_name), Type::Named(b_name)) => {
if a_name == b_name {
return true;
}
// Semantic compatibility via Engram node type mappings
let a_node = self.engram_mappings.get(a_name);
let b_node = self.engram_mappings.get(b_name);
match (a_node, b_node) {
(Some(a_n), Some(b_n)) => a_n == b_n,
_ => false,
}
}
(Type::Array(a_inner), Type::Array(b_inner)) => {
self.check_compatible(a_inner, b_inner)
}
(Type::Optional(a_inner), Type::Optional(b_inner)) => {
self.check_compatible(a_inner, b_inner)
}
// T is compatible with T?
(t, Type::Optional(inner)) => self.check_compatible(t, inner),
(Type::Fn { params: ap, return_type: ar }, Type::Fn { params: bp, return_type: br }) => {
ap.len() == bp.len()
&& ap.iter().zip(bp.iter()).all(|(a, b)| self.check_compatible(a, b))
&& self.check_compatible(ar, br)
}
_ => false,
}
}
/// Resolve a [`TypeExpr`] from the parser into a [`Type`].
pub fn resolve_type_expr(&self, te: &el_parser::TypeExpr) -> Result<Type, crate::TypeError> {
match te {
el_parser::TypeExpr::Named(n) => {
// Check if it's a built-in alias or a registered user type
Ok(match n.as_str() {
"Int" => Type::Int,
"Float" => Type::Float,
"String" => Type::String,
"Bool" => Type::Bool,
"Uuid" => Type::Uuid,
"Void" => Type::Void,
other => {
if self.types.contains_key(other) {
Type::Named(other.to_string())
} else {
return Err(crate::TypeError::new(
crate::TypeErrorKind::UndefinedType(other.to_string()),
));
}
}
})
}
el_parser::TypeExpr::Array(inner) => {
Ok(Type::Array(Box::new(self.resolve_type_expr(inner)?)))
}
el_parser::TypeExpr::Optional(inner) => {
Ok(Type::Optional(Box::new(self.resolve_type_expr(inner)?)))
}
el_parser::TypeExpr::Fn { params, return_type } => {
let ps = params.iter().map(|p| self.resolve_type_expr(p)).collect::<Result<Vec<_>, _>>()?;
let ret = self.resolve_type_expr(return_type)?;
Ok(Type::Fn { params: ps, return_type: Box::new(ret) })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> TypeEnv {
TypeEnv::with_builtins()
}
#[test]
fn test_primitive_compatibility() {
let e = env();
assert!(e.check_compatible(&Type::Int, &Type::Int));
assert!(e.check_compatible(&Type::String, &Type::String));
assert!(!e.check_compatible(&Type::Int, &Type::String));
}
#[test]
fn test_int_promotes_to_float() {
let e = env();
assert!(e.check_compatible(&Type::Int, &Type::Float));
}
#[test]
fn test_named_same_is_compatible() {
let e = env();
assert!(e.check_compatible(&Type::Named("User".into()), &Type::Named("User".into())));
assert!(!e.check_compatible(&Type::Named("User".into()), &Type::Named("Order".into())));
}
#[test]
fn test_semantic_compatibility_via_engram_mapping() {
let mut e = env();
// Map both User and Customer to the "Entity" Engram node type
e.engram_mappings.insert("User".into(), "Entity".into());
e.engram_mappings.insert("Customer".into(), "Entity".into());
assert!(e.check_compatible(&Type::Named("User".into()), &Type::Named("Customer".into())));
}
#[test]
fn test_optional_compatibility() {
let e = env();
// Int is compatible with Int?
assert!(e.check_compatible(&Type::Int, &Type::Optional(Box::new(Type::Int))));
}
#[test]
fn test_array_compatibility() {
let e = env();
assert!(e.check_compatible(
&Type::Array(Box::new(Type::Int)),
&Type::Array(Box::new(Type::Int)),
));
assert!(!e.check_compatible(
&Type::Array(Box::new(Type::Int)),
&Type::Array(Box::new(Type::String)),
));
}
}
+54
View File
@@ -0,0 +1,54 @@
// activate.el The `activate` construct: spreading activation over the type graph.
//
// The `activate` keyword is the most novel feature of the Engram language.
// Instead of querying a database with SQL or a key lookup, you query the
// *type graph* using a natural language description. The result is
// statically typed the compiler knows you get [User] back, not Any.
//
// How it works:
//
// 1. At compile time, the type checker verifies that `User` is a registered
// type with a corresponding Engram node mapping.
//
// 2. At runtime, the Engram runtime performs spreading activation over
// the knowledge graph, starting from the User node, propagating to
// semantically related nodes with co-activation energy proportional
// to edge weights and node salience.
//
// 3. The query string "customer who purchased recently" is embedded
// and used as a semantic filter: only nodes whose embeddings are
// within cosine-similarity threshold of the query embedding are returned.
//
// 4. The result is type-safe: [User]. You don't get back Any or a JSON blob.
// The type checker enforces this at compile time.
type User {
id: Uuid
name: String
email: String
}
type Order {
id: Uuid
user_id: Uuid
amount: Float
status: String
}
// Query: find all users who are semantically related to "customer who purchased recently"
// The result type is [User] an array of User nodes from the knowledge graph.
let recent_buyers: [User] = activate User where "customer who purchased recently"
// You can also activate on other types:
let pending_orders: [Order] = activate Order where "order awaiting fulfillment"
// Spreading activation respects the type graph edges.
// If User and Customer share an Engram node type (e.g. "Entity"),
// they are semantically compatible the type checker allows
// treating one as the other when they map to the same Engram node class.
fn process_buyers(users: [User]) -> String {
return "processing users"
}
let result: String = process_buyers(recent_buyers)
+8
View File
@@ -0,0 +1,8 @@
// hello.el The simplest Engram program.
// Run with: el run hello.el
fn greet(name: String) -> String {
return "Hello, " + name
}
let message: String = greet("World")
+40
View File
@@ -0,0 +1,40 @@
// types.el Type definitions and pattern matching.
//
// Demonstrates:
// - type (struct) definitions
// - enum definitions with payloads
// - match expressions with enum patterns
// A user-defined struct type.
// Every field is typed; the type name maps to an Engram graph node.
type User {
id: Uuid
name: String
email: String
}
// An enum with a payload variant.
enum Status {
Active
Inactive
Pending(String)
}
// A function that takes a Status and returns a human-readable string.
fn describe_status(s: Status) -> String {
return match s {
Status::Active => "user is active"
Status::Inactive => "user is inactive"
Status::Pending(reason) => "pending: " + reason
}
}
// Sealed blocks protect sensitive values even in debug builds.
// The runtime enforces that values in sealed blocks are never
// observable through a debugger or memory dump.
sealed {
let api_key: String = "sk-prod-12345"
}
let status: Status = Status::Pending("email not verified")
let description: String = describe_status(status)
+592
View File
@@ -0,0 +1,592 @@
# Engram Language Specification
Version 0.1.0 — April 2026
---
## Overview
Engram is a statically-typed programming language designed from first principles
around a knowledge graph type system. Its three defining properties:
1. **Types are Engram nodes.** Every named type in the language is a node in a
knowledge graph. Type compatibility is not purely structural — it is also
semantic. Two types are compatible if their Engram node embeddings are
similar enough in meaning-space.
2. **Autocomplete is spreading activation.** The language server (LSP) uses
spreading activation over the type graph to suggest completions. You get
concepts semantically related to what you're building, not just methods on
the current type.
3. **The `prod` compilation target is quantum-sealed.** Bytecode compiled with
`--target prod` is encrypted with AES-256-GCM and signed. Without the
deployment key, the artifact is indistinguishable from random bytes. No
static analysis tool can decompile it.
---
## 1. Syntax Reference
### 1.1 Comments
```
// Single-line comment — extends to end of line
```
Block comments are not supported in v0.1. Use `//` on each line.
### 1.2 Variable Declarations
```
let name: Type = expression
let name = expression // type inferred
```
Variables are immutable by default. All bindings are block-scoped.
### 1.3 Functions
```
fn name(param1: Type1, param2: Type2) -> ReturnType {
// body
return expression
}
```
Functions are first-class values. The type of a function is:
```
fn(Type1, Type2) -> ReturnType
```
### 1.4 Types (Structs)
```
type TypeName {
field1: Type1
field2: Type2
// ...
}
```
Every `type` definition registers a node in the Engram type graph. The type
name becomes searchable via spreading activation.
### 1.5 Enums
```
enum EnumName {
Variant1
Variant2
VariantWithPayload(PayloadType)
}
```
Enum variants without parentheses carry no payload. Variants with parentheses
carry exactly one value of the given type.
### 1.6 Pattern Matching
```
match expression {
Pattern1 => result_expr1
Pattern2 => result_expr2
// ...
}
```
Pattern forms:
- `EnumName::Variant` — unit enum variant
- `EnumName::Variant(binding)` — enum variant with payload binding
- `literal` — exact literal (42, "str", true)
- `name` — binding (captures the subject into `name`)
- `_` — wildcard (always matches, discards)
All arms must produce the same type. The `match` expression evaluates to that type.
### 1.7 Control Flow
**If/else:**
```
if condition {
// then branch
} else {
// else branch
}
```
Both branches must produce the same type. The `else` branch is optional (produces `Void`).
**For loops:**
```
for item in collection {
// body
}
```
### 1.8 Field Access
```
value.field_name
```
Field access is type-checked at compile time. Accessing a field that does not
exist in the type definition is a compile error.
### 1.9 Array Literals
```
let numbers: [Int] = [1, 2, 3]
let empty: [String] = []
```
### 1.10 Index Access
```
let first: Int = numbers[0]
```
Index expressions require an `Int` index. Bounds checking is runtime behavior.
---
## 2. Type System
### 2.1 Primitive Types
| Type | Description | Example literal |
|----------|--------------------------------------|---------------------|
| `Int` | 64-bit signed integer | `42`, `-7`, `1_000` |
| `Float` | 64-bit IEEE 754 double | `3.14`, `0.5` |
| `String` | UTF-8 string | `"hello"` |
| `Bool` | Boolean | `true`, `false` |
| `Uuid` | RFC 4122 UUID | (runtime only) |
| `Void` | Unit type; no value | — |
### 2.2 Composite Types
| Type form | Description |
|-------------|--------------------------------------|
| `[T]` | Array of `T` |
| `T?` | Optional `T` (may be absent) |
| `Named` | User-defined struct or enum |
### 2.3 Numeric Coercions
- `Int` is implicitly coercible to `Float`.
- `Float` is not coercible to `Int` (use explicit conversion when available).
- `String + String` uses concatenation (the `+` operator is overloaded).
### 2.4 Structural vs. Semantic Compatibility
Standard structural compatibility:
- `Named("User")` is compatible with `Named("User")` (same name).
- `[Int]` is compatible with `[Int]`.
- `T` is compatible with `T?` (non-optional can be used as optional).
- `Int` is compatible with `Float` (widening).
**Semantic compatibility (novel):**
When two named types have registered Engram node type mappings that refer to
the same node class, they are considered semantically compatible:
```
// Register User and Customer as both mapping to the "Entity" Engram node type
// → User and Customer are semantically compatible
```
This is computed via cosine similarity over node embeddings when an Engram
database is connected. Without a database, the comparison is symbolic (same
node type string = compatible).
Semantic compatibility threshold: cosine similarity ≥ 0.85 (configurable).
### 2.5 Type Inference
The compiler infers types for `let` bindings without annotations:
```
let x = 42 // inferred: Int
let s = "hello" // inferred: String
let b = true // inferred: Bool
```
Function return types and parameter types must always be annotated. This is
intentional: function signatures are documentation.
---
## 3. The `activate` Construct
### 3.1 Syntax
```
activate TypeName where "semantic query string"
```
### 3.2 What It Does
`activate` is a first-class language construct that performs a spreading
activation query over the Engram knowledge graph and returns a typed array of
results.
The query string is a natural language description. At runtime, the Engram
runtime:
1. Embeds the query string into the same vector space as node embeddings.
2. Starts activation at the `TypeName` node and all nodes semantically related
to it (cosine similarity above threshold).
3. Spreads activation outward through graph edges, attenuated by edge weight
and node salience.
4. Returns all nodes whose activation level exceeds the minimum threshold,
projected back to the `TypeName` schema.
### 3.3 Static Typing
The result type of `activate TypeName where "..."` is always `[TypeName]`.
```
let users: [User] = activate User where "recent premium subscribers"
// ↑ type-checked: [User]
```
If `TypeName` is not a registered type, the compiler emits an error. The
query string is opaque to the type checker — it only passes the string through
to the runtime.
### 3.4 Without an Engram Database
When compiled without an Engram database (`CompilerOptions::engram_db_path` is
`None`), the `activate` construct emits a stub instruction. At runtime, the
interpreter emits a diagnostic and returns an empty array `[]`.
This allows programs using `activate` to compile and run in development without
requiring a live Engram instance.
### 3.5 Compile-time Behavior
At `--target prod`, `activate` is compiled to an `ACTIVATE` bytecode instruction.
The query string is embedded in the bytecode. The sealed artifact protects the
query from being read by static analysis.
---
## 4. Sealed Blocks
### 4.1 Syntax
```
sealed {
let api_key: String = env("API_KEY")
// ...
}
```
### 4.2 What's Protected
Code inside a `sealed {}` block is subject to additional runtime protection:
- **In debug builds:** The `SEALED_BEGIN` / `SEALED_END` bytecode markers are
emitted. The debugger is notified not to expose values in this region.
- **In release builds:** Same as debug, with no source map entries for the
sealed region.
- **In prod builds:** The entire bytecode (including the sealed section) is
AES-256-GCM encrypted in the sealed artifact. There is no additional treatment
of the sealed section — the *entire prod artifact* is the sealed section.
### 4.3 Intent
The `sealed {}` block communicates developer intent: "this section handles
sensitive material." It is especially meaningful during development when
`debug` builds are used, since it signals to the runtime and any attached
debugger to redact values from inspection.
In `prod` builds, the `sealed {}` annotation is redundant (the whole artifact
is sealed), but it is preserved for documentation and future tooling that can
enforce stricter runtime isolation.
---
## 5. Compilation Targets
### 5.1 debug
```
el build file.el --target debug
```
Produces:
- `file.elc` — JSON-serialized bytecode instructions
- `file.map.json` — source map: JSON array of `{instruction, start, end, line, col}` objects
The source map allows debuggers and error reporters to translate bytecode
offsets back to exact source positions (file + line + column).
Debug builds:
- No dead-code elimination
- No constant folding
- Full source map coverage
- Type errors are reported as warnings (compilation continues)
### 5.2 release
```
el build file.el --target release
```
Produces:
- `file.elc` — JSON-serialized bytecode instructions
Release builds:
- No source map
- Minor dead-code pruning (unreachable after `return`)
- Type errors are warnings (compilation continues)
### 5.3 prod
```
el build file.el --target prod
ENGRAM_SEAL_KEY=my-secret el build file.el --target prod
```
Produces:
- `file.sealed` — quantum-sealed artifact
Prod builds:
- **Type errors are fatal** — the compiler refuses to produce a sealed artifact
from a program with type errors
- The output is encrypted and cannot be decompiled
- All debug information is stripped before sealing
- Source maps are never produced
---
## 6. The Sealed Artifact Format
### 6.1 Wire Format
```
Offset Size Field
────── ────── ────────────────────────────────────────────
0 8 Magic: b"ENGRAM01"
8 2 Format version: u16 big-endian (currently 1)
10 * JSON body: SealedArtifact struct
```
The JSON body is a `SealedArtifact`:
```json
{
"algorithm_id": "aes256gcm-v1",
"signature": "...(base64)...",
"encapsulated_key": "...(base64)...",
"nonce": "...(base64)...",
"ciphertext": "...(base64)...",
"deployment_fingerprint": "...(base64 or null)..."
}
```
### 6.2 Field Descriptions
| Field | Description |
|--------------------------|----------------------------------------------------------------------|
| `algorithm_id` | The encryption algorithm. Currently `aes256gcm-v1`. Reserved for ML-KEM upgrade. |
| `signature` | BLAKE3 keyed MAC over `(algorithm_id ‖ nonce ‖ ciphertext)`. Detects tampering before decryption attempt. |
| `encapsulated_key` | 32 bytes: `symmetric_key XOR BLAKE3(deployment_binding_material)`. Requires knowledge of the deployment secret to recover the symmetric key. |
| `nonce` | 12-byte (96-bit) AES-GCM nonce. Randomly generated per seal operation. |
| `ciphertext` | AES-256-GCM ciphertext of the bytecode, including the 128-bit GCM authentication tag. |
| `deployment_fingerprint` | BLAKE3 hash of the deployment binding material. Stored so the unsealer can verify it is running in the correct environment before attempting decryption. `null` for `DeploymentBinding::None`. |
### 6.3 Sealing Process
1. Generate a cryptographically random 256-bit symmetric key `K`.
2. Encrypt bytecode: `ciphertext = AES-256-GCM(K, nonce=random_96bit, plaintext=bytecode)`.
3. Derive the deployment binding hash: `H = BLAKE3(deployment_material)`.
4. Encapsulate: `encapsulated_key = K XOR H` (32 bytes).
5. Compute MAC: `signature = BLAKE3-keyed(K, algorithm_id ‖ nonce ‖ ciphertext)`.
6. Serialize: `ENGRAM01 ‖ version_u16be ‖ JSON(artifact)`.
### 6.4 Unsealing Process
1. Parse magic and version; reject if not `ENGRAM01 / version 1`.
2. Derive deployment hash: `H = BLAKE3(provided_binding_key)`.
3. Verify fingerprint: if `deployment_fingerprint` is present, assert `BLAKE3(binding_key) == fingerprint`. Fail with `BindingMismatch` if not.
4. Recover symmetric key: `K = encapsulated_key XOR H`.
5. Verify MAC: compute `BLAKE3-keyed(K, ...)` and compare to `signature`. Fail with `SignatureInvalid` if mismatch.
6. Decrypt: `bytecode = AES-256-GCM-Decrypt(K, nonce, ciphertext)`. The GCM auth tag is verified here automatically.
### 6.5 Security Properties
**Why "quantum-sealed":**
AES-256 is quantum-resistant at the 256-bit key length. Grover's algorithm
provides a quadratic speedup in key search, reducing effective security from
2^256 to 2^128. 128-bit quantum security is considered sufficient by NIST for
the foreseeable future.
The `algorithm_id` field is forward-compatible: when `ml-kem` (CRYSTALS-Kyber
ML-KEM-768 or ML-KEM-1024) crates stabilize, the upgrade is:
1. Implement `SealAlgorithm::MlKem768` in `el-seal`.
2. The `encapsulated_key` field becomes the KEM-encapsulated ciphertext.
3. Old artifacts retain their `aes256gcm-v1` algorithm_id and continue to
unseal via the existing code path.
**Decompilation resistance:**
Without the deployment key, `K` cannot be recovered (requires knowing
`deployment_material`), so `ciphertext` is indistinguishable from random
bytes. Static analysis tools, disassemblers, and decompilers receive the
AES-GCM ciphertext — semantically empty. Any tampering flips bits in the GCM
ciphertext, causing authentication tag verification to fail before the
symmetric layer is even reached.
---
## 7. Deployment Binding Modes
| Mode | Description | Security |
|-----------------------|--------------------------------------------------------|--------------|
| `EnvironmentKey(var)` | Derives binding from the value of an environment variable. Default: `ENGRAM_SEAL_KEY`. | High — key must be provisioned at runtime |
| `MachineFingerprint` | Derives binding from hostname + OS + architecture. Artifact can only run on the same machine. | Medium — fingerprint is observable |
| `None` | No binding (zero vector). Testing and development only. | None |
---
## 8. Operators
| Operator | Types | Result |
|----------|--------------------|--------|
| `+` | Int, Float, String | same as operands (String: concatenation) |
| `-` | Int, Float | same |
| `*` | Int, Float | same |
| `/` | Int, Float | same |
| `==` | any compatible pair | Bool |
| `!=` | any compatible pair | Bool |
| `<` `>` `<=` `>=` | Int, Float | Bool |
| `&&` | Bool, Bool | Bool |
| `\|\|` | Bool, Bool | Bool |
| `!` | Bool | Bool |
Operator precedence (high to low):
1. `!` (unary)
2. `*` `/`
3. `+` `-`
4. `<` `>` `<=` `>=`
5. `==` `!=`
6. `&&`
7. `||`
---
## 9. Escape Sequences in String Literals
| Sequence | Character |
|----------|------------|
| `\n` | Newline |
| `\t` | Tab |
| `\r` | Carriage return |
| `\"` | Double quote |
| `\\` | Backslash |
| `\0` | Null byte |
---
## 10. CLI Reference
```
el build <file.el> [--target debug|release|prod] [-o <output>]
el run <file.el>
el check <file.el>
el seal <artifact> [-o <output>]
el unseal <artifact> [-o <output>]
```
**el build** — Compile a source file. Default target is `debug`.
**el run** — Compile with `debug` target and execute immediately in the
built-in interpreter. Does not write an output file.
**el check** — Type-check only. Exits with code 0 if no errors, 1 if errors.
Useful for CI.
**el seal** — Take an existing release artifact and seal it. Reads
`ENGRAM_SEAL_KEY` from the environment if set.
**el unseal** — Decrypt a sealed artifact. Reads `ENGRAM_SEAL_KEY` from the
environment. Writes decrypted bytecode to the output path.
---
## 11. Grammar (EBNF)
```ebnf
program = stmt* EOF
stmt = let_stmt
| return_stmt
| fn_def
| type_def
| enum_def
| expr_stmt
let_stmt = "let" IDENT (":" type_expr)? "=" expr ";"?
return_stmt = "return" expr ";"?
expr_stmt = expr ";"?
fn_def = "fn" IDENT "(" param_list ")" "->" type_expr "{" stmt* "}"
type_def = "type" IDENT "{" (IDENT ":" type_expr ","? ";"?)* "}"
enum_def = "enum" IDENT "{" variant* "}"
variant = IDENT ("(" type_expr ")")? ","?
param_list = (param ("," param)*)?
param = IDENT ":" type_expr
type_expr = IDENT
| "[" type_expr "]"
| type_expr "?"
| "fn" "(" (type_expr ("," type_expr)*)? ")" "->" type_expr
expr = or_expr
or_expr = and_expr ("||" and_expr)*
and_expr = eq_expr ("&&" eq_expr)*
eq_expr = cmp_expr (("==" | "!=") cmp_expr)*
cmp_expr = add_expr (("<" | ">" | "<=" | ">=") add_expr)*
add_expr = mul_expr (("+" | "-") mul_expr)*
mul_expr = unary_expr (("*" | "/") unary_expr)*
unary_expr = "!" unary_expr | postfix_expr
postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]")*
primary = INT | FLOAT | STRING | BOOL
| "(" expr ")"
| "[" arg_list "]"
| "{" stmt* "}"
| "if" expr primary ("else" primary)?
| "match" expr "{" match_arm* "}"
| "activate" IDENT "where" STRING
| "sealed" "{" stmt* "}"
| IDENT ("::" IDENT)*
arg_list = (expr ("," expr)*)?
match_arm = pattern "=>" expr ","?
pattern = "_"
| IDENT "::" IDENT ("(" IDENT ")")?
| INT | STRING | BOOL
| IDENT
```
---
## 12. Future Directions
- **ML-KEM sealed artifacts** — upgrade `el-seal` to CRYSTALS-Kyber when the
`ml-kem` crate stabilizes (drop-in: same format, new `algorithm_id`).
- **LSP server** — spreading activation for autocomplete using the Engram
database as the type graph backend.
- **Engram DB integration** — live connection to an Engram database for
`activate` at compile time (semantic type checking) and at runtime (actual
node retrieval).
- **Struct construction syntax** — `User { id: uuid, name: "Alice", ... }`.
- **Generics** — `fn identity<T>(x: T) -> T { return x }`.
- **Trait system** — behavioral interfaces that interact with the Engram type graph.
- **Pattern matching on struct fields** — `match user { User { name: "admin" } => ... }`.
+1
View File
@@ -0,0 +1 @@
{"rustc_fingerprint":12156818420864752294,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: aarch64-apple-darwin\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/will/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
+3
View File
@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
View File
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
7bbf263d4b5d7de2
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"alloc\", \"getrandom\", \"rand_core\"]","declared_features":"[\"alloc\", \"arrayvec\", \"blobby\", \"bytes\", \"default\", \"dev\", \"getrandom\", \"heapless\", \"rand_core\", \"std\", \"stream\"]","target":6415113071054268027,"profile":5347358027863023418,"path":14456753889959366049,"deps":[[6039282458970808711,"crypto_common",false,15662686589037288465],[10520923840501062997,"generic_array",false,10112878813572505240]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aead-84f02a00b210196c/dep-lib-aead","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
af17669ec84c9109
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[\"hazmat\", \"zeroize\"]","target":1651443328692853038,"profile":5347358027863023418,"path":251018351013202105,"deps":[[7667230146095136825,"cfg_if",false,15210096645161364247],[7916416211798676886,"cipher",false,13618285945931706330],[17620084158052398167,"cpufeatures",false,13997498211530695242]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aes-0405562fbabbcd98/dep-lib-aes","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
2b86155f2285074f
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"aes\", \"alloc\", \"default\", \"getrandom\", \"rand_core\"]","declared_features":"[\"aes\", \"alloc\", \"arrayvec\", \"default\", \"getrandom\", \"heapless\", \"rand_core\", \"std\", \"stream\", \"zeroize\"]","target":6327482228044654328,"profile":5347358027863023418,"path":9050550317643413961,"deps":[[5822136307240319171,"ctr",false,1984667175316043168],[7916416211798676886,"cipher",false,13618285945931706330],[17003143334332120809,"subtle",false,16923554282490341060],[17625407307438784893,"aes",false,689416642499057583],[17797166225172937111,"aead",false,16320303202390425467],[18030706926766528332,"ghash",false,16081659758129703950]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aes-gcm-dd5b011b5902b9e3/dep-lib-aes_gcm","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
dfbb2e14298e6d16
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"auto\", \"default\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":8255941854203129366,"path":219791009280154194,"deps":[[2608044744973004659,"anstyle_parse",false,12980428951946425480],[5652275617566266604,"anstyle_query",false,2726880939266865220],[7098682853475662231,"anstyle",false,1168809825470293997],[7711617929439759244,"colorchoice",false,7617143434391948168],[7727459912076845739,"is_terminal_polyfill",false,10643044772563587381],[17716308468579268865,"utf8parse",false,1679570728423809634]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-844675e00c4b0612/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
eda724f957723810
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":6165884447290141869,"profile":8255941854203129366,"path":10647540871896512301,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-1808efb267e49055/dep-lib-anstyle","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
8874f6c85bbf23b4
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":8255941854203129366,"path":6511233520745312781,"deps":[[17716308468579268865,"utf8parse",false,1679570728423809634]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-a95bf7259265962c/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
44f8c841c4d3d725
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[]","target":10705714425685373190,"profile":14848920055892446256,"path":17230651471619537882,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-query-03151ea01a4e231d/dep-lib-anstyle_query","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
42b63d26d41d3039
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[]","target":14855336370480542997,"profile":5347358027863023418,"path":9603432717039822245,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arrayref-b68b407561967e78/dep-lib-arrayref","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
b582f2b72dc9e405
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[\"borsh\", \"default\", \"serde\", \"std\", \"zeroize\"]","target":12564975964323158710,"profile":5347358027863023418,"path":18191042356073489987,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arrayvec-8c5966a511ebbe70/dep-lib-arrayvec","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
003fd29290c1788b
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":5347358027863023418,"path":1010556327073429201,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-61edf3037de35534/dep-lib-base64","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
3e0b3a45bf680364
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"digest\", \"mmap\", \"neon\", \"no_avx2\", \"no_avx512\", \"no_neon\", \"no_sse2\", \"no_sse41\", \"prefer_intrinsics\", \"pure\", \"rayon\", \"serde\", \"std\", \"traits-preview\", \"wasm32_simd\", \"zeroize\"]","target":2835126046236718539,"profile":3033921117576893,"path":9659682929537390633,"deps":[[6314015906312918981,"cc",false,5035238791346254153]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/blake3-c6e05ebcc674da2f/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
87a1b0a1a45d8cdb
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"digest\", \"mmap\", \"neon\", \"no_avx2\", \"no_avx512\", \"no_neon\", \"no_sse2\", \"no_sse41\", \"prefer_intrinsics\", \"pure\", \"rayon\", \"serde\", \"std\", \"traits-preview\", \"wasm32_simd\", \"zeroize\"]","target":2743094924018349955,"profile":5347358027863023418,"path":15936121577563285743,"deps":[[7667230146095136825,"cfg_if",false,15210096645161364247],[9529943735784919782,"arrayref",false,4120826456055854658],[13665985940634834070,"build_script_build",false,17977234359213402738],[13847662864258534762,"arrayvec",false,424685463076504245],[14380949652265396754,"constant_time_eq",false,7993620062890213387]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/blake3-dfe6e6ef1ca6a8f7/dep-lib-blake3","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
72aadcf866f77bf9
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[13665985940634834070,"build_script_build",false,7206718999432399678]],"local":[{"RerunIfChanged":{"output":"debug/build/blake3-fba632ea867cc72e/output","paths":["c/blake3_sse41_x86-64_windows_msvc.asm","c/blake3_avx512_x86-64_windows_msvc.asm","c/blake3_sse2_x86-64_unix.S","c/blake3_sse2_x86-64_windows_msvc.asm","c/CMakeLists.txt","c/libblake3.pc.in","c/cmake","c/blake3_sse41_x86-64_unix.S","c/blake3-config.cmake.in","c/blake3.h","c/blake3_dispatch.c","c/blake3_sse41.c","c/blake3_avx512_x86-64_windows_gnu.S","c/dependencies","c/Makefile.testing","c/test.py","c/blake3_portable.c","c/blake3_tbb.cpp","c/blake3_neon.c","c/blake3_avx512.c","c/README.md","c/CMakePresets.json","c/example.c","c/blake3_avx2.c","c/main.c","c/.gitignore","c/blake3_avx2_x86-64_unix.S","c/blake3_avx2_x86-64_windows_gnu.S","c/blake3.c","c/example_tbb.c","c/blake3_sse2_x86-64_windows_gnu.S","c/blake3_impl.h","c/blake3_sse41_x86-64_windows_gnu.S","c/blake3_avx2_x86-64_windows_msvc.asm","c/blake3_sse2.c","c/blake3_avx512_x86-64_unix.S"]}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_PURE","val":null}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_NO_NEON","val":null}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_NEON","val":null}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_NEON","val":null}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_NO_NEON","val":null}},{"RerunIfEnvChanged":{"var":"CARGO_FEATURE_PURE","val":null}},{"RerunIfEnvChanged":{"var":"CC_ENABLE_DEBUG_OUTPUT","val":null}},{"RerunIfEnvChanged":{"var":"CC","val":null}},{"RerunIfEnvChanged":{"var":"CFLAGS","val":null}}],"rustflags":[],"config":0,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
49f175bd00c3e045
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[\"jobserver\", \"parallel\"]","target":11042037588551934598,"profile":9003321226815314314,"path":10548251769121867896,"deps":[[8410525223747752176,"shlex",false,14904201186866019039],[9159843920629750842,"find_msvc_tools",false,1941622244890545664]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cc-e31606cff2d84738/dep-lib-cc","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
172b34ee4f1e15d3
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":5347358027863023418,"path":1343408598986127531,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cfg-if-923a5f6d1c4f3de6/dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
da2ff440eadefdbc
@@ -0,0 +1 @@
{"rustc":10088478398040096810,"features":"[]","declared_features":"[\"alloc\", \"blobby\", \"block-padding\", \"dev\", \"rand_core\", \"std\", \"zeroize\"]","target":9724871538835674250,"profile":5347358027863023418,"path":10703066233970685649,"deps":[[6039282458970808711,"crypto_common",false,15662686589037288465],[6580247197892008482,"inout",false,5701996800147494100]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cipher-2c9bda998d1d3fda/dep-lib-cipher","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}

Some files were not shown because too many files have changed in this diff Show More