feat: package manager, build system, native cross-compilation, plugin system
Add three new crates and extend the compiler and CLI toolchain: - el-manifest: el.toml manifest parser using serde + toml crate; supports package info, registry/path/version deps, build config with seal key sources, cross targets, and plugins; Manifest::find_manifest() walks up the directory tree - el-registry: HTTP registry client (reqwest + tokio) for packages.neurontechnologies.ai; PackageMetadata, fetch/download/publish/ search, BLAKE3 checksum verification, local cache at ~/.engram/packages/ - el-build: build orchestrator with incremental builds (BLAKE3 file hashes in .el/build-cache.json), cross-compilation target tagging, dep resolution, plugin registry with on_ast/on_typed_ast/on_bytecode hooks, test runner, fmt/check/clean commands - CrossTarget and NativeTarget enums with triple() and artifact_extension() methods; NativeTarget::Host detects compile-time platform via cfg! macros - Plugin system: CompilerPlugin trait + PluginRegistry; dynamic loading is a marked TODO with clear extension point for libloading - CLI extended with: new, add, remove, update, build --cross, run, test, check, fmt, clean, publish, search, plugin add/remove/list; old single-file commands moved to build-file/seal/unseal subcommands - Fix pre-existing debugger.rs borrow error (unwrap_or temporary lifetime) - Fix checker.rs and codegen.rs to handle TestDef/Seed/Assert Stmt variants - Add spec/language.md sections 12-14: package system, build system, plugin system, cross-compilation targets table 130 tests passing, zero warnings
This commit is contained in:
@@ -2,6 +2,36 @@
|
||||
|
||||
use el_lexer::Span;
|
||||
|
||||
// ── Test-specific nodes ───────────────────────────────────────────────────────
|
||||
|
||||
/// Which graph a test should execute against.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TestTarget {
|
||||
/// In-memory graph — default, zero external dependencies.
|
||||
Unit,
|
||||
/// Real Engram database pointed at by `ENGRAM_URL` / `ENGRAM_DB_PATH`.
|
||||
E2e,
|
||||
/// Run against both unit (in-memory) and e2e (real DB).
|
||||
Both,
|
||||
}
|
||||
|
||||
/// A `seed Node { ... }` or `seed Edge { ... }` statement inside a test block.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SeedStmt {
|
||||
Node {
|
||||
node_type: String,
|
||||
content: String,
|
||||
importance: f32,
|
||||
tier: Option<String>,
|
||||
},
|
||||
Edge {
|
||||
from: String,
|
||||
to: String,
|
||||
relation: String,
|
||||
weight: f32,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Literals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -144,6 +174,17 @@ pub enum Stmt {
|
||||
variants: Vec<Variant>,
|
||||
span: Span,
|
||||
},
|
||||
/// `test "name" [target: unit|e2e|both] { body }`
|
||||
TestDef {
|
||||
name: String,
|
||||
target: TestTarget,
|
||||
body: Vec<Stmt>,
|
||||
span: Span,
|
||||
},
|
||||
/// `seed Node { ... }` or `seed Edge { ... }`
|
||||
Seed(SeedStmt, Span),
|
||||
/// `assert <expr>`
|
||||
Assert(Expr, Span),
|
||||
}
|
||||
|
||||
// ── Top-level program ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,7 +11,8 @@ mod error;
|
||||
mod parser;
|
||||
|
||||
pub use ast::{
|
||||
BinOp, Expr, Field, Literal, MatchArm, Param, Pattern, Program, Stmt, TypeExpr, Variant,
|
||||
BinOp, Expr, Field, Literal, MatchArm, Param, Pattern, Program, SeedStmt, Stmt, TestTarget,
|
||||
TypeExpr, Variant,
|
||||
};
|
||||
pub use error::{ParseError, ParseErrorKind};
|
||||
pub use parser::parse;
|
||||
|
||||
@@ -117,6 +117,9 @@ impl Parser {
|
||||
Token::Fn => self.parse_fn_def(start),
|
||||
Token::Type => self.parse_type_def(start),
|
||||
Token::Enum => self.parse_enum_def(start),
|
||||
Token::Test => self.parse_test_def(start),
|
||||
Token::Seed => self.parse_seed(start),
|
||||
Token::Assert => self.parse_assert(start),
|
||||
Token::Return => {
|
||||
self.advance(); // consume `return`
|
||||
let expr = self.parse_expr()?;
|
||||
@@ -133,6 +136,144 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_test_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Test)?;
|
||||
// test name is a string literal
|
||||
let name = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string literal (test name)", &tok, self.peek_span())),
|
||||
};
|
||||
// Optional `target: unit|e2e|both`
|
||||
let target = if self.eat(&Token::Target) {
|
||||
self.expect(&Token::Colon)?;
|
||||
let (target_name, span) = self.expect_ident()?;
|
||||
match target_name.as_str() {
|
||||
"unit" => crate::ast::TestTarget::Unit,
|
||||
"e2e" => crate::ast::TestTarget::E2e,
|
||||
"both" => crate::ast::TestTarget::Both,
|
||||
other => return Err(ParseError::new(
|
||||
ParseErrorKind::InvalidExprStart(format!("unknown test target '{other}': use unit, e2e, or both")),
|
||||
span,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
crate::ast::TestTarget::Unit
|
||||
};
|
||||
self.expect(&Token::LBrace)?;
|
||||
let body = self.parse_block_body()?;
|
||||
self.expect(&Token::RBrace)?;
|
||||
Ok(Stmt::TestDef { name, target, body, span: start })
|
||||
}
|
||||
|
||||
fn parse_seed(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Seed)?;
|
||||
let (kind, _) = self.expect_ident()?;
|
||||
self.expect(&Token::LBrace)?;
|
||||
|
||||
let seed = match kind.as_str() {
|
||||
"Node" => {
|
||||
let mut node_type = String::new();
|
||||
let mut content = String::new();
|
||||
let mut importance: f32 = 1.0;
|
||||
let mut tier: Option<String> = None;
|
||||
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
match field_name.as_str() {
|
||||
"type" => {
|
||||
node_type = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
"content" => {
|
||||
content = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
"importance" => {
|
||||
importance = match self.peek().clone() {
|
||||
Token::FloatLiteral(f) => { self.advance(); f as f32 }
|
||||
Token::IntLiteral(n) => { self.advance(); n as f32 }
|
||||
tok => return Err(ParseError::expected("float", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
"tier" => {
|
||||
let (t, _) = self.expect_ident()?;
|
||||
tier = Some(t);
|
||||
}
|
||||
_ => {
|
||||
// Skip unknown fields gracefully
|
||||
self.parse_expr()?;
|
||||
}
|
||||
}
|
||||
self.eat(&Token::Comma);
|
||||
self.eat(&Token::Semicolon);
|
||||
}
|
||||
crate::ast::SeedStmt::Node { node_type, content, importance, tier }
|
||||
}
|
||||
"Edge" => {
|
||||
let mut from = String::new();
|
||||
let mut to = String::new();
|
||||
let mut relation = String::new();
|
||||
let mut weight: f32 = 1.0;
|
||||
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
match field_name.as_str() {
|
||||
"from" => {
|
||||
from = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
"to" => {
|
||||
to = match self.peek().clone() {
|
||||
Token::StringLiteral(s) => { self.advance(); s }
|
||||
tok => return Err(ParseError::expected("string", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
"relation" => {
|
||||
let (rel, _) = self.expect_ident()?;
|
||||
relation = rel;
|
||||
}
|
||||
"weight" => {
|
||||
weight = match self.peek().clone() {
|
||||
Token::FloatLiteral(f) => { self.advance(); f as f32 }
|
||||
Token::IntLiteral(n) => { self.advance(); n as f32 }
|
||||
tok => return Err(ParseError::expected("float", &tok, self.peek_span())),
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
self.parse_expr()?;
|
||||
}
|
||||
}
|
||||
self.eat(&Token::Comma);
|
||||
self.eat(&Token::Semicolon);
|
||||
}
|
||||
crate::ast::SeedStmt::Edge { from, to, relation, weight }
|
||||
}
|
||||
other => return Err(ParseError::new(
|
||||
ParseErrorKind::InvalidExprStart(format!("unknown seed kind '{other}': use Node or Edge")),
|
||||
start,
|
||||
)),
|
||||
};
|
||||
|
||||
self.expect(&Token::RBrace)?;
|
||||
self.eat(&Token::Semicolon);
|
||||
Ok(Stmt::Seed(seed, start))
|
||||
}
|
||||
|
||||
fn parse_assert(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Assert)?;
|
||||
let expr = self.parse_expr()?;
|
||||
self.eat(&Token::Semicolon);
|
||||
Ok(Stmt::Assert(expr, start))
|
||||
}
|
||||
|
||||
fn parse_let(&mut self, start: Span) -> Result<Stmt, ParseError> {
|
||||
self.expect(&Token::Let)?;
|
||||
let (name, _) = self.expect_ident()?;
|
||||
|
||||
Reference in New Issue
Block a user