rename crates/ to engrams/; add el-compiler el package with bootstrap artifact

- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
  - src/{compiler,lexer,parser,codegen}.el
  - bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
This commit is contained in:
Will Anderson
2026-04-29 03:27:32 -05:00
parent 19ed2721ee
commit a42429012e
120 changed files with 3836 additions and 64 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "el-arch"
version = "0.1.0"
edition = "2021"
[dependencies]
el-parser = { path = "../el-parser" }
el-lexer = { path = "../el-lexer" }
[dev-dependencies]
el-lexer = { path = "../el-lexer" }
el-parser = { path = "../el-parser" }
+468
View File
@@ -0,0 +1,468 @@
//! Main architectural checker — walks the AST and applies all rules.
use std::collections::HashMap;
use el_parser::{Expr, Program, Stmt, TypeExpr};
use crate::error::{ArchDiagnostic, Severity};
use crate::rule::{ArchRule, CallInfo, FnContext};
use crate::rules::{
graph::{DuplicateActivateType, N1Detection},
security::{AuthnWithoutAuthz, PublicFnWithActivate, SealedInLoop},
swarm::{SwarmAgentIsolation, SwarmAgentNoSharedState, SwarmAgentNoSpawn},
vbd::{AccessorMustNotCallManager, ExperienceMustNotCallExperience, ExperienceShouldReturnResult},
};
/// The main architectural checker. Instantiate once, call `check` per program.
pub struct ArchChecker {
rules: Vec<Box<dyn ArchRule>>,
}
impl ArchChecker {
/// Create an `ArchChecker` with all built-in rules registered.
pub fn new() -> Self {
Self {
rules: vec![
Box::new(AccessorMustNotCallManager),
Box::new(ExperienceMustNotCallExperience),
Box::new(ExperienceShouldReturnResult),
Box::new(PublicFnWithActivate),
Box::new(SealedInLoop),
Box::new(AuthnWithoutAuthz),
Box::new(N1Detection),
Box::new(DuplicateActivateType),
Box::new(SwarmAgentIsolation),
Box::new(SwarmAgentNoSpawn),
Box::new(SwarmAgentNoSharedState),
],
}
}
/// Returns a reference to the registered rules (useful for introspection in tests).
pub fn rules(&self) -> &[Box<dyn ArchRule>] {
&self.rules
}
/// Run all rules against a parsed program and collect all diagnostics.
pub fn check(&self, program: &Program) -> Vec<ArchDiagnostic> {
// Step 1: build global fn_name → decorator names map (including impl methods).
let all_fn_annotations = collect_fn_annotations(&program.stmts);
// Step 2: gather all top-level + impl FnDef statements.
let fn_defs = collect_fn_defs(&program.stmts);
// Step 3: for each function, build FnContext and run every rule.
let mut diagnostics = Vec::new();
for (fn_name, decorators, return_type, body) in &fn_defs {
let annotations: Vec<String> =
decorators.iter().map(|d| d.name.clone()).collect();
let body_calls = extract_calls(body, false);
let activate_types = extract_activate_types(body, false);
let has_sealed_in_loop = has_sealed_in_loop_body(body, false);
let return_type_name = type_expr_name(return_type);
let ctx = FnContext {
fn_name: fn_name.as_str(),
annotations: &annotations,
body_calls: &body_calls,
all_fn_annotations: &all_fn_annotations,
activate_types: &activate_types,
has_sealed_in_loop,
return_type_name: &return_type_name,
};
for rule in &self.rules {
diagnostics.extend(rule.check(&ctx));
}
}
diagnostics
}
/// Returns true if any of the given diagnostics are errors.
pub fn has_errors(diagnostics: &[ArchDiagnostic]) -> bool {
diagnostics.iter().any(|d| d.severity == Severity::Error)
}
}
impl Default for ArchChecker {
fn default() -> Self {
Self::new()
}
}
// ── AST traversal helpers ─────────────────────────────────────────────────────
/// A collected function definition: (name, decorators, return_type, body).
type FnDef<'a> = (
&'a String,
&'a Vec<el_parser::Decorator>,
&'a TypeExpr,
&'a Vec<Stmt>,
);
/// Collect all FnDef statements from top-level and impl blocks.
fn collect_fn_defs<'a>(stmts: &'a [Stmt]) -> Vec<FnDef<'a>> {
let mut out = Vec::new();
for stmt in stmts {
match stmt {
Stmt::FnDef { name, decorators, return_type, body, .. } => {
out.push((name, decorators, return_type, body));
}
Stmt::ImplDef { methods, .. } => {
for m in methods {
if let Stmt::FnDef { name, decorators, return_type, body, .. } = m {
out.push((name, decorators, return_type, body));
}
}
}
_ => {}
}
}
out
}
/// Build a map from function name → list of decorator names, for all functions in the program.
fn collect_fn_annotations(stmts: &[Stmt]) -> HashMap<String, Vec<String>> {
let mut map = HashMap::new();
for stmt in stmts {
match stmt {
Stmt::FnDef { name, decorators, .. } => {
let anns: Vec<String> = decorators.iter().map(|d| d.name.clone()).collect();
map.insert(name.clone(), anns);
}
Stmt::ImplDef { methods, .. } => {
for m in methods {
if let Stmt::FnDef { name, decorators, .. } = m {
let anns: Vec<String> = decorators.iter().map(|d| d.name.clone()).collect();
map.insert(name.clone(), anns);
}
}
}
_ => {}
}
}
map
}
/// Extract all function calls from a statement list.
/// `in_loop` tracks whether we are currently inside a for/while loop body.
fn extract_calls(stmts: &[Stmt], in_loop: bool) -> Vec<CallInfo> {
let mut calls = Vec::new();
for stmt in stmts {
extract_calls_from_stmt(stmt, in_loop, &mut calls);
}
calls
}
fn extract_calls_from_stmt(stmt: &Stmt, in_loop: bool, out: &mut Vec<CallInfo>) {
match stmt {
Stmt::Let { value, .. } => extract_calls_from_expr(value, in_loop, out),
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
extract_calls_from_expr(expr, in_loop, out);
}
Stmt::FnDef { body, .. } => {
// Nested function defs: walk but don't count as callee of the outer fn.
for s in body {
extract_calls_from_stmt(s, false, out);
}
}
_ => {}
}
}
fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec<CallInfo>) {
match expr {
Expr::Call { func, args } => {
// Extract callee name
let callee = expr_as_call_name(func);
if let Some(name) = callee {
out.push(CallInfo { callee: name, is_in_loop: in_loop });
}
// Recurse into func expression and args
extract_calls_from_expr(func, in_loop, out);
for a in args {
extract_calls_from_expr(a, in_loop, out);
}
}
Expr::Activate { type_name, .. } => {
// Encode activate as a synthetic call so GRAPH-001 can detect in-loop activates.
out.push(CallInfo {
callee: format!("__activate__{type_name}"),
is_in_loop: in_loop,
});
}
Expr::BinOp { left, right, .. } => {
extract_calls_from_expr(left, in_loop, out);
extract_calls_from_expr(right, in_loop, out);
}
Expr::UnaryNot(inner) | Expr::Try(inner) => {
extract_calls_from_expr(inner, in_loop, out);
}
Expr::Block(stmts) => {
for s in stmts {
extract_calls_from_stmt(s, in_loop, out);
}
}
Expr::Sealed(stmts) => {
// Sealed blocks are scanned but tracked separately for sealed-in-loop.
for s in stmts {
extract_calls_from_stmt(s, in_loop, out);
}
}
Expr::If { cond, then, else_ } => {
extract_calls_from_expr(cond, in_loop, out);
extract_calls_from_expr(then, in_loop, out);
if let Some(e) = else_ {
extract_calls_from_expr(e, in_loop, out);
}
}
Expr::Match { subject, arms } => {
extract_calls_from_expr(subject, in_loop, out);
for arm in arms {
extract_calls_from_expr(&arm.body, in_loop, out);
}
}
Expr::Field { object, .. } => extract_calls_from_expr(object, in_loop, out),
Expr::Array(elems) => {
for e in elems {
extract_calls_from_expr(e, in_loop, out);
}
}
Expr::Index { object, index } => {
extract_calls_from_expr(object, in_loop, out);
extract_calls_from_expr(index, in_loop, out);
}
Expr::Closure { body, .. } => {
extract_calls_from_expr(body, in_loop, out);
}
Expr::MapLiteral(pairs) => {
for (k, v) in pairs {
extract_calls_from_expr(k, in_loop, out);
extract_calls_from_expr(v, in_loop, out);
}
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => {}
Expr::StructLit { fields, .. } => {
for (_, e) in fields {
extract_calls_from_expr(e, in_loop, out);
}
}
Expr::With { base, updates } => {
extract_calls_from_expr(base, in_loop, out);
for (_, e) in updates {
extract_calls_from_expr(e, in_loop, out);
}
}
Expr::Reason { .. } => {}
Expr::Parallel { entries } => {
for (_, e) in entries {
extract_calls_from_expr(e, in_loop, out);
}
}
Expr::Trace { body, .. } => {
for s in body {
extract_calls_from_stmt(s, in_loop, out);
}
}
}
}
/// Try to extract a simple callee name from a Call's `func` expression.
fn expr_as_call_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => Some(name.clone()),
Expr::Field { field, .. } => Some(field.clone()),
Expr::Path { segments } => segments.last().cloned(),
_ => None,
}
}
/// Collect all `activate TypeName` type names from a statement list.
/// `in_loop` indicates whether we're currently inside a loop.
fn extract_activate_types(stmts: &[Stmt], in_loop: bool) -> Vec<String> {
let mut types = Vec::new();
for stmt in stmts {
extract_activate_types_stmt(stmt, in_loop, &mut types);
}
types
}
fn extract_activate_types_stmt(stmt: &Stmt, in_loop: bool, out: &mut Vec<String>) {
match stmt {
Stmt::Let { value, .. } => extract_activate_types_expr(value, in_loop, out),
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
extract_activate_types_expr(expr, in_loop, out);
}
Stmt::FnDef { body, .. } => {
for s in body {
extract_activate_types_stmt(s, false, out);
}
}
_ => {}
}
}
fn extract_activate_types_expr(expr: &Expr, in_loop: bool, out: &mut Vec<String>) {
match expr {
Expr::Activate { type_name, .. } => {
out.push(type_name.clone());
}
Expr::Call { func, args } => {
extract_activate_types_expr(func, in_loop, out);
for a in args {
extract_activate_types_expr(a, in_loop, out);
}
}
Expr::BinOp { left, right, .. } => {
extract_activate_types_expr(left, in_loop, out);
extract_activate_types_expr(right, in_loop, out);
}
Expr::UnaryNot(inner) | Expr::Try(inner) => {
extract_activate_types_expr(inner, in_loop, out);
}
Expr::Block(stmts) => {
for s in stmts {
extract_activate_types_stmt(s, in_loop, out);
}
}
Expr::Sealed(stmts) => {
for s in stmts {
extract_activate_types_stmt(s, in_loop, out);
}
}
Expr::If { cond, then, else_ } => {
extract_activate_types_expr(cond, in_loop, out);
extract_activate_types_expr(then, in_loop, out);
if let Some(e) = else_ {
extract_activate_types_expr(e, in_loop, out);
}
}
Expr::Match { subject, arms } => {
extract_activate_types_expr(subject, in_loop, out);
for arm in arms {
extract_activate_types_expr(&arm.body, in_loop, out);
}
}
Expr::Field { object, .. } => extract_activate_types_expr(object, in_loop, out),
Expr::Array(elems) => {
for e in elems {
extract_activate_types_expr(e, in_loop, out);
}
}
Expr::Index { object, index } => {
extract_activate_types_expr(object, in_loop, out);
extract_activate_types_expr(index, in_loop, out);
}
Expr::Closure { body, .. } => {
extract_activate_types_expr(body, in_loop, out);
}
Expr::MapLiteral(pairs) => {
for (k, v) in pairs {
extract_activate_types_expr(k, in_loop, out);
extract_activate_types_expr(v, in_loop, out);
}
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => {}
Expr::StructLit { fields, .. } => {
for (_, e) in fields {
extract_activate_types_expr(e, in_loop, out);
}
}
Expr::With { base, updates } => {
extract_activate_types_expr(base, in_loop, out);
for (_, e) in updates {
extract_activate_types_expr(e, in_loop, out);
}
}
Expr::Reason { .. } => {}
Expr::Parallel { entries } => {
for (_, e) in entries {
extract_activate_types_expr(e, in_loop, out);
}
}
Expr::Trace { body, .. } => {
for s in body {
extract_activate_types_stmt(s, in_loop, out);
}
}
}
}
/// Returns true if any `sealed { }` block appears inside a loop in the given body.
fn has_sealed_in_loop_body(stmts: &[Stmt], in_loop: bool) -> bool {
stmts.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop))
}
fn has_sealed_in_loop_stmt(stmt: &Stmt, in_loop: bool) -> bool {
match stmt {
Stmt::Let { value, .. } => has_sealed_in_loop_expr(value, in_loop),
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
has_sealed_in_loop_expr(expr, in_loop)
}
Stmt::FnDef { body, .. } => {
// Inner function defs reset loop context.
body.iter().any(|s| has_sealed_in_loop_stmt(s, false))
}
_ => false,
}
}
fn has_sealed_in_loop_expr(expr: &Expr, in_loop: bool) -> bool {
match expr {
Expr::Sealed(_) => in_loop,
Expr::Call { func, args } => {
has_sealed_in_loop_expr(func, in_loop)
|| args.iter().any(|a| has_sealed_in_loop_expr(a, in_loop))
}
Expr::BinOp { left, right, .. } => {
has_sealed_in_loop_expr(left, in_loop) || has_sealed_in_loop_expr(right, in_loop)
}
Expr::UnaryNot(inner) | Expr::Try(inner) => has_sealed_in_loop_expr(inner, in_loop),
Expr::Block(stmts) => stmts.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)),
Expr::If { cond, then, else_ } => {
has_sealed_in_loop_expr(cond, in_loop)
|| has_sealed_in_loop_expr(then, in_loop)
|| else_.as_deref().is_some_and(|e| has_sealed_in_loop_expr(e, in_loop))
}
Expr::Match { subject, arms } => {
has_sealed_in_loop_expr(subject, in_loop)
|| arms.iter().any(|a| has_sealed_in_loop_expr(&a.body, in_loop))
}
Expr::Field { object, .. } => has_sealed_in_loop_expr(object, in_loop),
Expr::Array(elems) => elems.iter().any(|e| has_sealed_in_loop_expr(e, in_loop)),
Expr::Index { object, index } => {
has_sealed_in_loop_expr(object, in_loop) || has_sealed_in_loop_expr(index, in_loop)
}
Expr::Closure { body, .. } => has_sealed_in_loop_expr(body, in_loop),
Expr::MapLiteral(pairs) => pairs
.iter()
.any(|(k, v)| has_sealed_in_loop_expr(k, in_loop) || has_sealed_in_loop_expr(v, in_loop)),
Expr::Activate { .. } | Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => false,
Expr::StructLit { fields, .. } => fields
.iter()
.any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
Expr::With { base, updates } => {
has_sealed_in_loop_expr(base, in_loop)
|| updates.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop))
}
Expr::Reason { .. } => false,
Expr::Parallel { entries } => entries.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
Expr::Trace { body, .. } => body.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)),
}
}
/// Convert a `TypeExpr` to a display string for the return-type name check.
fn type_expr_name(te: &TypeExpr) -> String {
match te {
TypeExpr::Named(n) => n.clone(),
TypeExpr::Result { .. } => "Result".to_string(),
TypeExpr::Array(inner) => format!("[{}]", type_expr_name(inner)),
TypeExpr::Optional(inner) => format!("{}?", type_expr_name(inner)),
TypeExpr::Map { key, value } => {
format!("Map<{}, {}>", type_expr_name(key), type_expr_name(value))
}
TypeExpr::Fn { .. } => "fn".to_string(),
TypeExpr::TypeParam(n) => n.clone(),
}
}
+45
View File
@@ -0,0 +1,45 @@
//! Diagnostic types for the architectural checker.
/// Severity level of an architectural diagnostic.
#[derive(Debug, Clone, PartialEq)]
pub enum Severity {
Error,
Warning,
}
/// A single architectural diagnostic (error or warning).
#[derive(Debug, Clone)]
pub struct ArchDiagnostic {
pub severity: Severity,
/// Rule identifier, e.g. "VBD-001".
pub rule: String,
pub message: String,
/// Function name or other location hint.
pub location: Option<String>,
}
/// Type alias — an ArchError is an ArchDiagnostic with Severity::Error.
pub type ArchError = ArchDiagnostic;
/// Type alias — an ArchWarning is an ArchDiagnostic with Severity::Warning.
pub type ArchWarning = ArchDiagnostic;
impl ArchDiagnostic {
pub fn error(rule: impl Into<String>, message: impl Into<String>, location: Option<String>) -> Self {
Self {
severity: Severity::Error,
rule: rule.into(),
message: message.into(),
location,
}
}
pub fn warning(rule: impl Into<String>, message: impl Into<String>, location: Option<String>) -> Self {
Self {
severity: Severity::Warning,
rule: rule.into(),
message: message.into(),
location,
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//! el-arch — Architectural rule checker for the Engram language.
//!
//! Runs after type-checking and enforces:
//! - VBD (Volatility-Based Decomposition) layer rules
//! - EBD (Experience-Based Decomposition) experience rules
//! - Swarm containment rules
//! - Security rules
//! - Graph access patterns (N+1, duplicate activate)
pub mod checker;
pub mod error;
pub mod rule;
pub mod rules;
pub use checker::ArchChecker;
pub use error::{ArchDiagnostic, ArchError, ArchWarning, Severity};
#[cfg(test)]
mod tests;
+56
View File
@@ -0,0 +1,56 @@
//! Core trait and context types for architectural rules.
use std::collections::HashMap;
use crate::error::ArchDiagnostic;
/// Information about a single function call within a function body.
#[derive(Debug, Clone)]
pub struct CallInfo {
/// The name of the function being called.
pub callee: String,
/// True if this call appears inside a loop body (for or while).
pub is_in_loop: bool,
}
/// Full context about a single function being checked by arch rules.
pub struct FnContext<'a> {
/// Name of the function under analysis.
pub fn_name: &'a str,
/// Decorator names applied directly to this function (e.g. "accessor", "public").
pub annotations: &'a [String],
/// All calls made from within this function body.
pub body_calls: &'a [CallInfo],
/// Global map of function name → decorator names for the whole program.
pub all_fn_annotations: &'a HashMap<String, Vec<String>>,
/// TypeNames that appear in `activate TypeName where ...` calls in this function.
pub activate_types: &'a [String],
/// Whether this function contains a `sealed { }` block inside a loop.
pub has_sealed_in_loop: bool,
/// The return type of the function as a string (e.g. "Result", "Void", "String").
pub return_type_name: &'a str,
}
impl<'a> FnContext<'a> {
/// Returns true if this function has the given annotation/decorator.
pub fn has_annotation(&self, name: &str) -> bool {
self.annotations.iter().any(|a| a == name)
}
/// Returns true if the named callee has the given annotation in the program.
pub fn callee_has_annotation(&self, callee: &str, ann: &str) -> bool {
self.all_fn_annotations
.get(callee)
.map(|anns| anns.iter().any(|a| a == ann))
.unwrap_or(false)
}
}
/// An architectural rule that can be checked against a function context.
pub trait ArchRule: Send + Sync {
/// Short unique identifier, e.g. "VBD-001".
fn name(&self) -> &str;
/// Human-readable description of what this rule enforces.
fn description(&self) -> &str;
/// Run the rule against a function context, returning any diagnostics.
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic>;
}
+73
View File
@@ -0,0 +1,73 @@
//! Graph access pattern rules (N+1 detection, duplicate activate).
use crate::error::ArchDiagnostic;
use crate::rule::{ArchRule, FnContext};
// ── GRAPH-001: N+1 detection ──────────────────────────────────────────────────
/// GRAPH-001: An activate call inside a loop is an N+1 graph access pattern.
/// Each loop iteration performs a separate graph traversal; consolidate into one query.
pub struct N1Detection;
impl ArchRule for N1Detection {
fn name(&self) -> &str { "GRAPH-001" }
fn description(&self) -> &str {
"activate inside a loop creates an N+1 graph access pattern — hoist outside the loop"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
// We detect activate-in-loop via the body_calls with is_in_loop = true
// AND via the activate_types combined with loop context tracked by the checker.
// The checker sets a separate field for this.
let has_activate_in_loop = ctx.body_calls
.iter()
.any(|c| c.is_in_loop && c.callee.starts_with("__activate__"));
if has_activate_in_loop {
vec![ArchDiagnostic::warning(
self.name(),
format!(
"function '{}' performs activate inside a loop — N+1 graph access pattern",
ctx.fn_name
),
Some(ctx.fn_name.to_string()),
)]
} else {
vec![]
}
}
}
// ── GRAPH-002: Duplicate activate on same type ────────────────────────────────
/// GRAPH-002: Multiple activate calls on the same type within one function
/// should be consolidated into a single query for efficiency.
pub struct DuplicateActivateType;
impl ArchRule for DuplicateActivateType {
fn name(&self) -> &str { "GRAPH-002" }
fn description(&self) -> &str {
"multiple activate calls on the same type in one function — consolidate into one query"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
let mut seen = std::collections::HashMap::new();
for type_name in ctx.activate_types {
*seen.entry(type_name.as_str()).or_insert(0u32) += 1;
}
seen.iter()
.filter(|(_, &count)| count > 1)
.map(|(type_name, count)| ArchDiagnostic::warning(
self.name(),
format!(
"function '{}' activates type '{}' {} times — consolidate into a single query",
ctx.fn_name, type_name, count
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Individual architectural rule implementations.
pub mod graph;
pub mod security;
pub mod swarm;
pub mod vbd;
+104
View File
@@ -0,0 +1,104 @@
//! Security architectural rules.
use crate::error::ArchDiagnostic;
use crate::rule::{ArchRule, FnContext};
// ── SEC-001: Public function with activate inside ─────────────────────────────
/// SEC-001: A @public function must not contain activate expressions.
/// Unauthenticated callers could trigger graph reads, potentially leaking data.
pub struct PublicFnWithActivate;
impl ArchRule for PublicFnWithActivate {
fn name(&self) -> &str { "SEC-001" }
fn description(&self) -> &str {
"@public functions must not contain activate — unauthenticated callers could trigger data reads"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("public") {
return vec![];
}
if !ctx.activate_types.is_empty() {
return vec![ArchDiagnostic::error(
self.name(),
format!(
"@public function '{}' contains activate — data leak risk for unauthenticated callers (types: {})",
ctx.fn_name,
ctx.activate_types.join(", ")
),
Some(ctx.fn_name.to_string()),
)];
}
vec![]
}
}
// ── SEC-002: sealed block inside a loop ──────────────────────────────────────
/// SEC-002: A sealed block inside a loop incurs encryption overhead per iteration.
pub struct SealedInLoop;
impl ArchRule for SealedInLoop {
fn name(&self) -> &str { "SEC-002" }
fn description(&self) -> &str {
"sealed blocks inside loops cause encryption overhead on every iteration"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if ctx.has_sealed_in_loop {
return vec![ArchDiagnostic::warning(
self.name(),
format!(
"function '{}' contains a sealed block inside a loop — encryption overhead in hot path",
ctx.fn_name
),
Some(ctx.fn_name.to_string()),
)];
}
vec![]
}
}
// ── SEC-003: @authenticate without @authorize on mutations ────────────────────
/// SEC-003: Functions whose name suggests mutation (create_*, update_*, delete_*)
/// and carry @authenticate should also carry @authorize, otherwise authn without authz.
pub struct AuthnWithoutAuthz;
impl ArchRule for AuthnWithoutAuthz {
fn name(&self) -> &str { "SEC-003" }
fn description(&self) -> &str {
"@authenticate without @authorize on mutation functions — authentication without authorization"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("authenticate") {
return vec![];
}
if ctx.has_annotation("authorize") {
return vec![];
}
// Heuristic: mutation function names
let is_mutation = ctx.fn_name.starts_with("create_")
|| ctx.fn_name.starts_with("update_")
|| ctx.fn_name.starts_with("delete_")
|| ctx.fn_name.starts_with("write_")
|| ctx.fn_name.starts_with("mutate_");
if is_mutation {
return vec![ArchDiagnostic::warning(
self.name(),
format!(
"function '{}' has @authenticate but not @authorize — authn without authz on a mutation",
ctx.fn_name
),
Some(ctx.fn_name.to_string()),
)];
}
vec![]
}
}
+103
View File
@@ -0,0 +1,103 @@
//! Swarm containment rules — agents are isolated and must not cross boundaries.
use crate::error::ArchDiagnostic;
use crate::rule::{ArchRule, FnContext};
// ── SWARM-001: Swarm agent calling another swarm agent ────────────────────────
/// SWARM-001: @swarm_agent functions must not call other @swarm_agent functions.
/// Agents are isolated units; cross-agent calls break containment.
pub struct SwarmAgentIsolation;
impl ArchRule for SwarmAgentIsolation {
fn name(&self) -> &str { "SWARM-001" }
fn description(&self) -> &str {
"@swarm_agent must not call another @swarm_agent (agents are isolated)"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("swarm_agent") {
return vec![];
}
ctx.body_calls
.iter()
.filter(|call| ctx.callee_has_annotation(&call.callee, "swarm_agent"))
.map(|call| ArchDiagnostic::error(
self.name(),
format!(
"@swarm_agent '{}' calls @swarm_agent '{}' — agents must be isolated",
ctx.fn_name, call.callee
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
// ── SWARM-002: Swarm agent initiating a spawn/swarm ──────────────────────────
/// SWARM-002: @swarm_agent must not initiate spawning of other agents
/// (calls to functions named `spawn` or containing "swarm" in the name).
pub struct SwarmAgentNoSpawn;
impl ArchRule for SwarmAgentNoSpawn {
fn name(&self) -> &str { "SWARM-002" }
fn description(&self) -> &str {
"@swarm_agent must not call spawn/swarm functions (agents cannot initiate sub-swarms)"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("swarm_agent") {
return vec![];
}
ctx.body_calls
.iter()
.filter(|call| {
let c = call.callee.as_str();
c == "spawn" || c.contains("swarm") || c.starts_with("spawn_")
})
.map(|call| ArchDiagnostic::error(
self.name(),
format!(
"@swarm_agent '{}' calls '{}' — agents cannot initiate spawning",
ctx.fn_name, call.callee
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
// ── SWARM-003: Swarm agent accessing shared mutable state ─────────────────────
/// SWARM-003: @swarm_agent must not access shared mutable state.
/// Heuristic: calls to functions with "shared" in the name suggest shared state access.
pub struct SwarmAgentNoSharedState;
impl ArchRule for SwarmAgentNoSharedState {
fn name(&self) -> &str { "SWARM-003" }
fn description(&self) -> &str {
"@swarm_agent must not access shared mutable state (functions with 'shared' in name)"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("swarm_agent") {
return vec![];
}
ctx.body_calls
.iter()
.filter(|call| call.callee.contains("shared"))
.map(|call| ArchDiagnostic::error(
self.name(),
format!(
"@swarm_agent '{}' accesses shared state via '{}' — agents must not touch shared mutable state",
ctx.fn_name, call.callee
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
+101
View File
@@ -0,0 +1,101 @@
//! VBD (Volatility-Based Decomposition) and EBD (Experience-Based Decomposition) layer rules.
use crate::error::ArchDiagnostic;
use crate::rule::{ArchRule, FnContext};
// ── VBD-001: Accessor must not call Manager ───────────────────────────────────
/// VBD-001: @accessor functions must not call @manager functions.
/// Accessors are read-only, stable-interface components; they must not depend
/// on manager-layer orchestration logic.
pub struct AccessorMustNotCallManager;
impl ArchRule for AccessorMustNotCallManager {
fn name(&self) -> &str { "VBD-001" }
fn description(&self) -> &str {
"@accessor must not call @manager functions (accessor must not depend on manager layer)"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("accessor") {
return vec![];
}
ctx.body_calls
.iter()
.filter(|call| ctx.callee_has_annotation(&call.callee, "manager"))
.map(|call| ArchDiagnostic::error(
self.name(),
format!(
"accessor '{}' calls manager '{}' — accessors must not depend on the manager layer",
ctx.fn_name, call.callee
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
// ── VBD-002: Experience must not directly call Experience ─────────────────────
/// VBD-002 / EBD-001: @experience functions must not call other @experience functions directly.
/// Experiences should communicate via events, not direct calls, to preserve
/// loose coupling between user-facing features.
pub struct ExperienceMustNotCallExperience;
impl ArchRule for ExperienceMustNotCallExperience {
fn name(&self) -> &str { "VBD-002" }
fn description(&self) -> &str {
"@experience must not call another @experience directly (use events instead)"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("experience") {
return vec![];
}
ctx.body_calls
.iter()
.filter(|call| ctx.callee_has_annotation(&call.callee, "experience"))
.map(|call| ArchDiagnostic::error(
self.name(),
format!(
"experience '{}' directly calls experience '{}' — use an event instead",
ctx.fn_name, call.callee
),
Some(ctx.fn_name.to_string()),
))
.collect()
}
}
// ── VBD-003: Experience should return Result<T, E> ────────────────────────────
/// VBD-003: @experience functions should return Result<T, E> for proper error propagation.
pub struct ExperienceShouldReturnResult;
impl ArchRule for ExperienceShouldReturnResult {
fn name(&self) -> &str { "VBD-003" }
fn description(&self) -> &str {
"@experience functions should return Result<T, E> for proper error handling"
}
fn check(&self, ctx: &FnContext<'_>) -> Vec<ArchDiagnostic> {
if !ctx.has_annotation("experience") {
return vec![];
}
// Warn if return type doesn't include "Result"
if !ctx.return_type_name.contains("Result") {
return vec![ArchDiagnostic::warning(
self.name(),
format!(
"experience '{}' returns '{}' instead of Result<T, E> — experiences should propagate errors",
ctx.fn_name, ctx.return_type_name
),
Some(ctx.fn_name.to_string()),
)];
}
vec![]
}
}
+448
View File
@@ -0,0 +1,448 @@
//! Comprehensive tests for the el-arch architectural checker.
use crate::{ArchChecker, ArchDiagnostic, Severity};
// ── Test helpers ──────────────────────────────────────────────────────────────
fn check(src: &str) -> Vec<ArchDiagnostic> {
let tokens = el_lexer::tokenize(src).expect("lex failed");
let prog = el_parser::parse(tokens, src.to_string()).expect("parse failed");
ArchChecker::new().check(&prog)
}
fn errors(src: &str) -> Vec<ArchDiagnostic> {
check(src).into_iter().filter(|d| d.severity == Severity::Error).collect()
}
fn warnings(src: &str) -> Vec<ArchDiagnostic> {
check(src).into_iter().filter(|d| d.severity == Severity::Warning).collect()
}
fn has_rule(diags: &[ArchDiagnostic], rule: &str) -> bool {
diags.iter().any(|d| d.rule == rule)
}
// ── 1. VBD-001: @accessor calling @manager → error ──────────────────────────
#[test]
fn test_accessor_calls_manager_error() {
let src = r#"
@manager fn orchestrate(x: String) -> String { return x }
@accessor fn fetch_user(id: String) -> String { return orchestrate(id) }
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected error when @accessor calls @manager");
assert!(has_rule(&errs, "VBD-001"), "expected VBD-001 rule");
}
// ── 2. @accessor calling @accessor → no VBD-001 error ────────────────────────
#[test]
fn test_accessor_calls_accessor_no_error() {
let src = r#"
@accessor fn get_name(id: String) -> String { return id }
@accessor fn get_user(id: String) -> String { return get_name(id) }
"#;
let errs = errors(src);
let vbd001: Vec<_> = errs.iter().filter(|d| d.rule == "VBD-001").collect();
assert!(vbd001.is_empty(), "accessor->accessor should not trigger VBD-001");
}
// ── 3. @experience calling @experience → VBD-002 error ───────────────────────
#[test]
fn test_experience_calls_experience_error() {
let src = r#"
@experience fn checkout(cart: String) -> Result<String, String> { return cart }
@experience fn payment(amount: String) -> Result<String, String> { return checkout(amount) }
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected error when @experience calls @experience");
assert!(has_rule(&errs, "VBD-002"), "expected VBD-002 rule");
}
// ── 4. @experience calling non-experience → no VBD-002 error ─────────────────
#[test]
fn test_experience_calls_non_experience_no_error() {
let src = r#"
@accessor fn load_cart(id: String) -> String { return id }
@experience fn checkout(cart: String) -> Result<String, String> { return load_cart(cart) }
"#;
let errs = errors(src);
let vbd002: Vec<_> = errs.iter().filter(|d| d.rule == "VBD-002").collect();
assert!(vbd002.is_empty(), "experience->non-experience should not trigger VBD-002");
}
// ── 5. @public function with activate inside → SEC-001 error ─────────────────
#[test]
fn test_public_fn_with_activate_error() {
let src = r#"
@public fn list_users() -> String {
let users: String = activate User where "all users"
return users
}
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected error for @public fn with activate");
assert!(has_rule(&errs, "SEC-001"), "expected SEC-001 rule");
}
// ── 6. @public function without activate → no SEC-001 error ──────────────────
#[test]
fn test_public_fn_without_activate_no_error() {
let src = r#"
@public fn greet(name: String) -> String { return name }
"#;
let errs = errors(src);
let sec001: Vec<_> = errs.iter().filter(|d| d.rule == "SEC-001").collect();
assert!(sec001.is_empty(), "@public without activate should not trigger SEC-001");
}
// ── 7. N+1: activate inside a for loop → GRAPH-001 warning ───────────────────
#[test]
fn test_activate_in_for_loop_n1_warning() {
let src = r#"
fn process_ids(ids: String) -> String {
for id in ids {
let u: String = activate User where "user by id"
}
return "done"
}
"#;
let warns = warnings(src);
assert!(!warns.is_empty(), "expected N+1 warning for activate inside loop");
assert!(has_rule(&warns, "GRAPH-001"), "expected GRAPH-001 rule");
}
// ── 8. activate NOT in loop → no GRAPH-001 warning ───────────────────────────
#[test]
fn test_activate_not_in_loop_no_n1_warning() {
let src = r#"
fn fetch_users() -> String {
let users: String = activate User where "recent users"
return users
}
"#;
let warns = warnings(src);
let graph001: Vec<_> = warns.iter().filter(|d| d.rule == "GRAPH-001").collect();
assert!(graph001.is_empty(), "activate outside loop should not trigger GRAPH-001");
}
// ── 9. Duplicate activate same type → GRAPH-002 warning ──────────────────────
#[test]
fn test_duplicate_activate_same_type_warning() {
let src = r#"
fn inefficient_fn(x: String) -> String {
let a: String = activate User where "active users"
let b: String = activate User where "recent users"
return a
}
"#;
let warns = warnings(src);
assert!(!warns.is_empty(), "expected warning for duplicate activate on same type");
assert!(has_rule(&warns, "GRAPH-002"), "expected GRAPH-002 rule");
}
// ── 10. Different activate types → no GRAPH-002 warning ──────────────────────
#[test]
fn test_different_activate_types_no_duplicate_warning() {
let src = r#"
fn fetch_all(x: String) -> String {
let users: String = activate User where "users"
let orders: String = activate Order where "orders"
return users
}
"#;
let warns = warnings(src);
let graph002: Vec<_> = warns.iter().filter(|d| d.rule == "GRAPH-002").collect();
assert!(graph002.is_empty(), "different activate types should not trigger GRAPH-002");
}
// ── 11. @swarm_agent calling @swarm_agent → SWARM-001 error ──────────────────
#[test]
fn test_swarm_agent_calls_swarm_agent_error() {
let src = r#"
@swarm_agent fn worker_b(x: String) -> String { return x }
@swarm_agent fn worker_a(x: String) -> String { return worker_b(x) }
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected error when @swarm_agent calls @swarm_agent");
assert!(has_rule(&errs, "SWARM-001"), "expected SWARM-001 rule");
}
// ── 12. @swarm_agent in isolation → no SWARM-001 error ───────────────────────
#[test]
fn test_swarm_agent_isolation_no_error() {
let src = r#"
fn utility(x: String) -> String { return x }
@swarm_agent fn worker(x: String) -> String { return utility(x) }
"#;
let errs = errors(src);
let swarm001: Vec<_> = errs.iter().filter(|d| d.rule == "SWARM-001").collect();
assert!(swarm001.is_empty(), "isolated @swarm_agent should not trigger SWARM-001");
}
// ── 13. Multiple rules fire on same function ──────────────────────────────────
#[test]
fn test_multiple_rules_fire_same_function() {
// @swarm_agent calling a swarm_agent AND accessing shared state
let src = r#"
@swarm_agent fn peer(x: String) -> String { return x }
fn get_shared_cache(x: String) -> String { return x }
@swarm_agent fn violator(x: String) -> String {
let a: String = peer(x)
let b: String = get_shared_cache(x)
return a
}
"#;
let errs = errors(src);
// Should fire both SWARM-001 (calls peer) and SWARM-003 (calls get_shared_cache)
assert!(errs.len() >= 2, "expected multiple errors: {:?}", errs.iter().map(|e| &e.rule).collect::<Vec<_>>());
}
// ── 14. ArchChecker::has_errors() → true when errors present ─────────────────
#[test]
fn test_has_errors_true_when_errors_present() {
let src = r#"
@manager fn do_manage(x: String) -> String { return x }
@accessor fn bad_fetch(x: String) -> String { return do_manage(x) }
"#;
let diags = check(src);
assert!(ArchChecker::has_errors(&diags), "has_errors should be true");
}
// ── 15. ArchChecker::has_errors() → false when only warnings ─────────────────
#[test]
fn test_has_errors_false_when_only_warnings() {
let src = r#"
@experience fn sign_up(email: String) -> String { return email }
"#;
// sign_up doesn't return Result so triggers VBD-003 warning
let diags = check(src);
let has_warn = diags.iter().any(|d| d.severity == Severity::Warning);
assert!(has_warn || diags.is_empty(), "expected either warnings or empty");
assert!(!ArchChecker::has_errors(&diags.iter().filter(|d| d.severity == Severity::Warning).cloned().collect::<Vec<_>>()), "has_errors should be false for warnings");
}
// ── 16. Clean function → empty diagnostics ────────────────────────────────────
#[test]
fn test_clean_function_no_diagnostics() {
let src = r#"
fn pure_add(a: String, b: String) -> String { return a }
"#;
let diags = check(src);
assert!(diags.is_empty(), "clean function should produce no diagnostics, got: {:?}", diags.iter().map(|d| &d.rule).collect::<Vec<_>>());
}
// ── 17. @engine function → runs without panic ─────────────────────────────────
#[test]
fn test_engine_function_no_panic() {
let src = r#"
@engine fn compute(data: String) -> String { return data }
"#;
// Just verify no panic — engine rules only produce warnings in some impls
let _diags = check(src);
}
// ── 18. sealed { } not in loop → no SEC-002 warning ──────────────────────────
#[test]
fn test_sealed_not_in_loop_no_warning() {
let src = r#"
fn encrypt_data(secret: String) -> String {
sealed { let key: String = secret }
return secret
}
"#;
let warns = warnings(src);
let sec002: Vec<_> = warns.iter().filter(|d| d.rule == "SEC-002").collect();
assert!(sec002.is_empty(), "sealed not in loop should not trigger SEC-002");
}
// ── 19. Rule names are unique ─────────────────────────────────────────────────
#[test]
fn test_rule_names_are_unique() {
let checker = ArchChecker::new();
let mut names = std::collections::HashSet::new();
for rule in checker.rules() {
let inserted = names.insert(rule.name().to_string());
assert!(inserted, "duplicate rule name: {}", rule.name());
}
}
// ── 20. All rules implement ArchRule (compile-time check) ─────────────────────
#[test]
fn test_all_rules_implement_arch_rule() {
use crate::rule::ArchRule;
use crate::rules::{
vbd::{AccessorMustNotCallManager, ExperienceMustNotCallExperience, ExperienceShouldReturnResult},
security::{PublicFnWithActivate, SealedInLoop, AuthnWithoutAuthz},
graph::{N1Detection, DuplicateActivateType},
swarm::{SwarmAgentIsolation, SwarmAgentNoSpawn, SwarmAgentNoSharedState},
};
fn assert_arch_rule<T: ArchRule>() {}
assert_arch_rule::<AccessorMustNotCallManager>();
assert_arch_rule::<ExperienceMustNotCallExperience>();
assert_arch_rule::<ExperienceShouldReturnResult>();
assert_arch_rule::<PublicFnWithActivate>();
assert_arch_rule::<SealedInLoop>();
assert_arch_rule::<AuthnWithoutAuthz>();
assert_arch_rule::<N1Detection>();
assert_arch_rule::<DuplicateActivateType>();
assert_arch_rule::<SwarmAgentIsolation>();
assert_arch_rule::<SwarmAgentNoSpawn>();
assert_arch_rule::<SwarmAgentNoSharedState>();
}
// ── 21. FnContext builds correctly for a decorated function ───────────────────
#[test]
fn test_fn_context_from_decorated_function() {
// Run checker and verify the location field is set to the function name
let src = r#"
@accessor fn fetch_item(id: String) -> String { return id }
"#;
let diags = check(src);
// No violations — but verify the checker doesn't panic and processes it
// (accessor with no calls should produce no errors)
let _: Vec<_> = diags;
}
// ── 22. @experience without Result return type → VBD-003 warning ─────────────
#[test]
fn test_experience_non_result_return_warning() {
let src = r#"
@experience fn show_profile(user: String) -> String { return user }
"#;
let warns = warnings(src);
assert!(!warns.is_empty(), "expected warning for @experience not returning Result");
assert!(has_rule(&warns, "VBD-003"), "expected VBD-003 rule");
}
// ── 23. @authenticate without @authorize on mutation → SEC-003 warning ────────
#[test]
fn test_authenticate_without_authorize_on_mutation_warning() {
let src = r#"
@authenticate fn create_account(email: String) -> String { return email }
"#;
let warns = warnings(src);
assert!(!warns.is_empty(), "expected SEC-003 warning");
assert!(has_rule(&warns, "SEC-003"), "expected SEC-003 rule");
}
// ── 24. Two @experience functions checked independently ───────────────────────
#[test]
fn test_two_experience_functions_checked_independently() {
let src = r#"
@experience fn sign_up(email: String) -> Result<String, String> { return email }
@experience fn log_in(token: String) -> String { return token }
"#;
let warns = warnings(src);
// sign_up returns Result — no VBD-003 for it
// log_in returns String — VBD-003 fires for it
let vbd003: Vec<_> = warns.iter().filter(|d| d.rule == "VBD-003").collect();
assert_eq!(vbd003.len(), 1, "only log_in should trigger VBD-003, got {:?}", vbd003.iter().map(|d| &d.location).collect::<Vec<_>>());
assert!(vbd003[0].location.as_deref() == Some("log_in"), "VBD-003 should point to log_in");
}
// ── 25. Mixed errors and warnings → has_errors() returns true ─────────────────
#[test]
fn test_mixed_errors_and_warnings_has_errors_true() {
let src = r#"
@manager fn manage_data(x: String) -> String { return x }
@accessor fn bad_read(x: String) -> String { return manage_data(x) }
@experience fn display(x: String) -> String { return x }
"#;
let diags = check(src);
assert!(ArchChecker::has_errors(&diags), "should have errors (VBD-001)");
let has_warn = diags.iter().any(|d| d.severity == Severity::Warning);
assert!(has_warn, "should also have warnings (VBD-003 for display)");
}
// ── 26. @swarm_agent calling spawn → SWARM-002 error ─────────────────────────
#[test]
fn test_swarm_agent_no_spawn() {
let src = r#"
fn spawn(agent: String) -> String { return agent }
@swarm_agent fn initiator(x: String) -> String { return spawn(x) }
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected SWARM-002 error for calling spawn");
assert!(has_rule(&errs, "SWARM-002"), "expected SWARM-002 rule");
}
// ── 27. @swarm_agent accessing shared state → SWARM-003 error ────────────────
#[test]
fn test_swarm_agent_no_shared_state() {
let src = r#"
fn get_shared_counter(x: String) -> String { return x }
@swarm_agent fn agent(x: String) -> String { return get_shared_counter(x) }
"#;
let errs = errors(src);
assert!(!errs.is_empty(), "expected SWARM-003 error for shared state access");
assert!(has_rule(&errs, "SWARM-003"), "expected SWARM-003 rule");
}
// ── 28. sealed block inside for loop → SEC-002 warning ───────────────────────
#[test]
fn test_sealed_in_for_loop_warning() {
let src = r#"
fn encrypt_many(items: String) -> String {
for item in items {
sealed { let x: String = item }
}
return items
}
"#;
let warns = warnings(src);
assert!(!warns.is_empty(), "expected SEC-002 warning for sealed in loop");
assert!(has_rule(&warns, "SEC-002"), "expected SEC-002 rule");
}
// ── 29. @authenticate with @authorize → no SEC-003 warning ───────────────────
#[test]
fn test_authenticate_with_authorize_no_warning() {
let src = r#"
@authenticate @authorize fn create_post(content: String) -> String { return content }
"#;
let warns = warnings(src);
let sec003: Vec<_> = warns.iter().filter(|d| d.rule == "SEC-003").collect();
assert!(sec003.is_empty(), "@authenticate + @authorize should not trigger SEC-003");
}
// ── 30. @experience returning Result → no VBD-003 warning ────────────────────
#[test]
fn test_experience_returns_result_no_warning() {
let src = r#"
@experience fn register(email: String) -> Result<String, String> { return email }
"#;
let warns = warnings(src);
let vbd003: Vec<_> = warns.iter().filter(|d| d.rule == "VBD-003").collect();
assert!(vbd003.is_empty(), "@experience returning Result should not trigger VBD-003");
}