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
+32
View File
@@ -0,0 +1,32 @@
//! Formatter configuration.
#[derive(Debug, Clone)]
pub struct FmtConfig {
pub indent: IndentStyle,
/// Number of spaces per indent level (default 4).
pub indent_width: usize,
/// Maximum line width before wrapping (default 100).
pub max_line_width: usize,
/// Whether to ensure the output ends with a newline (default true).
pub trailing_newline: bool,
/// Whether to emit a space before an opening brace (default true).
pub space_before_brace: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum IndentStyle {
Spaces,
Tabs,
}
impl Default for FmtConfig {
fn default() -> Self {
Self {
indent: IndentStyle::Spaces,
indent_width: 4,
max_line_width: 100,
trailing_newline: true,
space_before_brace: true,
}
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Error types for el-fmt.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FmtError {
#[error("lex error: {0}")]
Lex(String),
#[error("parse error: {0}")]
Parse(String),
}
+474
View File
@@ -0,0 +1,474 @@
//! AST pretty-printer — the core of el-fmt.
use el_parser::{BinOp, Expr, Literal, MatchArm, Pattern, Program, Stmt, TypeExpr};
use crate::{FmtConfig, FmtError};
use crate::config::IndentStyle;
pub struct Formatter {
config: FmtConfig,
}
impl Formatter {
pub fn new(config: FmtConfig) -> Self {
Self { config }
}
pub fn format(&self, program: &Program) -> Result<String, FmtError> {
let mut out = String::new();
for (i, stmt) in program.stmts.iter().enumerate() {
if i > 0 {
out.push('\n');
}
self.fmt_stmt(&mut out, stmt, 0);
}
if self.config.trailing_newline && !out.ends_with('\n') {
out.push('\n');
}
Ok(out)
}
fn indent(&self, depth: usize) -> String {
match self.config.indent {
IndentStyle::Spaces => " ".repeat(depth * self.config.indent_width),
IndentStyle::Tabs => "\t".repeat(depth),
}
}
fn fmt_stmt(&self, out: &mut String, stmt: &Stmt, depth: usize) {
let ind = self.indent(depth);
match stmt {
Stmt::Let { name, type_ann, value, .. } => {
out.push_str(&ind);
out.push_str("let ");
out.push_str(name);
if let Some(ty) = type_ann {
out.push_str(": ");
out.push_str(&self.fmt_type(ty));
}
out.push_str(" = ");
self.fmt_expr(out, value, depth);
out.push('\n');
}
Stmt::Return(expr, _) => {
out.push_str(&format!("{ind}return "));
self.fmt_expr(out, expr, depth);
out.push('\n');
}
Stmt::Expr(expr, _) => {
out.push_str(&ind);
self.fmt_expr(out, expr, depth);
out.push('\n');
}
Stmt::FnDef { name, params, body, decorators, return_type, .. } => {
// Decorators
for dec in decorators {
out.push_str(&format!("{ind}@{}\n", dec.name));
}
// Parameters
let params_str: Vec<String> = params
.iter()
.map(|p| format!("{}: {}", p.name, self.fmt_type(&p.type_ann)))
.collect();
// Always emit return type — the parser requires `->`.
let ret = format!(" -> {}", self.fmt_type(return_type));
let brace_space = if self.config.space_before_brace { " " } else { "" };
out.push_str(&format!(
"{ind}fn {name}({}){}{brace_space}{{\n",
params_str.join(", "),
ret,
));
for s in body {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::TypeDef { name, fields, .. } => {
out.push_str(&format!("{ind}type {name} {{\n"));
for f in fields {
out.push_str(&format!(
"{} {}: {}\n",
ind,
f.name,
self.fmt_type(&f.type_ann)
));
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::EnumDef { name, variants, .. } => {
out.push_str(&format!("{ind}enum {name} {{\n"));
for v in variants {
if let Some(payload) = &v.payload {
out.push_str(&format!(
"{} {}({})\n",
ind,
v.name,
self.fmt_type(payload)
));
} else {
out.push_str(&format!("{} {}\n", ind, v.name));
}
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::TestDef { name, body, .. } => {
out.push_str(&format!("{ind}test {:?} {{\n", name));
for s in body {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::Assert(expr, _) => {
out.push_str(&format!("{ind}assert "));
self.fmt_expr(out, expr, depth);
out.push('\n');
}
Stmt::Import { path, names, alias, .. } => {
if names.is_empty() {
let joined = path.join("::");
if let Some(a) = alias {
out.push_str(&format!("{ind}import {joined} as {a}\n"));
} else {
out.push_str(&format!("{ind}import {joined}\n"));
}
} else {
let joined = path.join("::");
let items = names.join(", ");
out.push_str(&format!("{ind}from {joined} import {{ {items} }}\n"));
}
}
Stmt::ProtocolDef { name, methods, .. } => {
out.push_str(&format!("{ind}protocol {name} {{\n"));
for m in methods {
let params_str: Vec<String> = m
.params
.iter()
.map(|p| format!("{}: {}", p.name, self.fmt_type(&p.type_ann)))
.collect();
out.push_str(&format!(
"{} fn {}({}) -> {}\n",
ind,
m.name,
params_str.join(", "),
self.fmt_type(&m.return_type)
));
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::ImplDef { protocol_name, type_name, methods, .. } => {
out.push_str(&format!("{ind}impl {protocol_name} for {type_name} {{\n"));
for m in methods {
self.fmt_stmt(out, m, depth + 1);
}
out.push_str(&format!("{ind}}}\n"));
}
Stmt::Seed(seed, _) => {
use el_parser::SeedStmt;
match seed {
SeedStmt::Node { node_type, content, importance, tier } => {
let tier_str = tier
.as_deref()
.map(|t| format!(", tier: {t}"))
.unwrap_or_default();
out.push_str(&format!(
"{ind}seed {node_type} {{ content: {:?}, importance: {importance}{tier_str} }}\n",
content
));
}
SeedStmt::Edge { from, to, relation, weight } => {
out.push_str(&format!(
"{ind}seed Edge {{ from: {from}, to: {to}, relation: {relation:?}, weight: {weight} }}\n"
));
}
}
}
Stmt::Retry { count, body, fallback, .. } => {
out.push_str(&format!("{ind}retry "));
self.fmt_expr(out, count, depth);
out.push_str(" times {\n");
for s in body {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{ind}}}"));
if let Some(fb) = fallback {
out.push_str(" fallback {\n");
for s in fb {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{ind}}}"));
}
out.push('\n');
}
Stmt::Deploy { fn_name, route, target, .. } => {
out.push_str(&format!("{ind}deploy {fn_name} to \"{route}\" via {target}\n"));
}
Stmt::While { condition, body, .. } => {
out.push_str(&format!("{ind}while "));
self.fmt_expr(out, condition, depth);
out.push_str(" {\n");
for s in body {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{ind}}}\n"));
}
}
}
fn fmt_expr(&self, out: &mut String, expr: &Expr, depth: usize) {
match expr {
Expr::Literal(lit) => self.fmt_literal(out, lit),
Expr::Ident(name) => out.push_str(name),
Expr::Path { segments } => out.push_str(&segments.join("::")),
Expr::BinOp { op, left, right } => {
self.fmt_expr(out, left, depth);
out.push_str(&format!(" {} ", self.fmt_binop(op)));
self.fmt_expr(out, right, depth);
}
Expr::UnaryNot(inner) => {
out.push('!');
self.fmt_expr(out, inner, depth);
}
Expr::Try(inner) => {
self.fmt_expr(out, inner, depth);
out.push('?');
}
Expr::Call { func, args } => {
self.fmt_expr(out, func, depth);
out.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
self.fmt_expr(out, arg, depth);
}
out.push(')');
}
Expr::Block(stmts) => {
out.push_str("{\n");
for s in stmts {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{}}}", self.indent(depth)));
}
Expr::If { cond, then, else_ } => {
out.push_str("if ");
self.fmt_expr(out, cond, depth);
out.push(' ');
self.fmt_expr(out, then, depth);
if let Some(else_expr) = else_ {
out.push_str(" else ");
self.fmt_expr(out, else_expr, depth);
}
}
Expr::Activate { type_name, query } => {
out.push_str(&format!("activate {type_name} where {:?}", query));
}
Expr::Field { object, field } => {
self.fmt_expr(out, object, depth);
out.push('.');
out.push_str(field);
}
Expr::Index { object, index } => {
self.fmt_expr(out, object, depth);
out.push('[');
self.fmt_expr(out, index, depth);
out.push(']');
}
Expr::Array(elems) => {
out.push('[');
for (i, e) in elems.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
self.fmt_expr(out, e, depth);
}
out.push(']');
}
Expr::MapLiteral(pairs) => {
out.push('{');
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
self.fmt_expr(out, k, depth);
out.push_str(": ");
self.fmt_expr(out, v, depth);
}
out.push('}');
}
Expr::Sealed(stmts) => {
out.push_str("sealed {\n");
for s in stmts {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{}}}", self.indent(depth)));
}
Expr::Match { subject, arms } => {
out.push_str("match ");
self.fmt_expr(out, subject, depth);
out.push_str(" {\n");
for arm in arms {
self.fmt_match_arm(out, arm, depth);
}
out.push_str(&format!("{}}}", self.indent(depth)));
}
Expr::Closure { params, return_type, body, .. } => {
out.push('|');
let params_str: Vec<String> = params
.iter()
.map(|p| format!("{}: {}", p.name, self.fmt_type(&p.type_ann)))
.collect();
out.push_str(&params_str.join(", "));
out.push('|');
if let Some(rt) = return_type {
out.push_str(&format!(" -> {}", self.fmt_type(rt)));
}
out.push(' ');
self.fmt_expr(out, body, depth);
}
Expr::StructLit { type_name, fields, .. } => {
out.push_str(type_name);
out.push_str(" { ");
let fields_str: Vec<String> = fields
.iter()
.map(|(name, val)| {
let mut s = format!("{name}: ");
self.fmt_expr(&mut s, val, depth);
s
})
.collect();
out.push_str(&fields_str.join(", "));
out.push_str(" }");
}
Expr::With { base, updates } => {
self.fmt_expr(out, base, depth);
out.push_str(" with { ");
for (k, v) in updates {
out.push_str(&format!("{k}: "));
self.fmt_expr(out, v, depth);
out.push_str(", ");
}
out.push('}');
}
Expr::Reason { query } => {
out.push_str(&format!("reason {:?}", query));
}
Expr::Parallel { entries } => {
out.push_str("parallel { ");
for (name, e) in entries {
out.push_str(&format!("{name}: "));
self.fmt_expr(out, e, depth);
out.push_str(", ");
}
out.push('}');
}
Expr::Trace { label, body } => {
out.push_str(&format!("trace {:?} {{\n", label));
for s in body {
self.fmt_stmt(out, s, depth + 1);
}
out.push_str(&format!("{}}}", self.indent(depth)));
}
}
}
fn fmt_literal(&self, out: &mut String, lit: &Literal) {
match lit {
Literal::Int(n) => out.push_str(&n.to_string()),
Literal::Float(f) => out.push_str(&f.to_string()),
Literal::Str(s) => out.push_str(&format!("{s:?}")),
Literal::Bool(b) => out.push_str(&b.to_string()),
}
}
fn fmt_binop(&self, op: &BinOp) -> &'static str {
match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Eq => "==",
BinOp::NotEq => "!=",
BinOp::Lt => "<",
BinOp::Gt => ">",
BinOp::LtEq => "<=",
BinOp::GtEq => ">=",
BinOp::And => "&&",
BinOp::Or => "||",
}
}
fn fmt_match_arm(&self, out: &mut String, arm: &MatchArm, depth: usize) {
out.push_str(&format!("{} ", self.indent(depth)));
self.fmt_pattern(out, &arm.pattern);
out.push_str(" => ");
self.fmt_expr(out, &arm.body, depth + 1);
out.push('\n');
}
fn fmt_pattern(&self, out: &mut String, pat: &Pattern) {
match pat {
Pattern::Wildcard => out.push('_'),
Pattern::Binding(name) => out.push_str(name),
Pattern::Literal(lit) => self.fmt_literal(out, lit),
Pattern::EnumVariant { enum_name, variant, payload } => {
out.push_str(&format!("{enum_name}::"));
out.push_str(variant);
if let Some(bind) = payload {
out.push_str(&format!("({bind})"));
}
}
}
}
pub fn fmt_type(&self, ty: &TypeExpr) -> String {
match ty {
TypeExpr::Named(n) => n.clone(),
TypeExpr::Array(inner) => format!("[{}]", self.fmt_type(inner)),
TypeExpr::Optional(inner) => format!("{}?", self.fmt_type(inner)),
TypeExpr::Result { ok, err } => {
format!("Result<{}, {}>", self.fmt_type(ok), self.fmt_type(err))
}
TypeExpr::Map { key, value } => {
format!("Map<{}, {}>", self.fmt_type(key), self.fmt_type(value))
}
TypeExpr::Fn { params, return_type } => {
let ps: Vec<_> = params.iter().map(|p| self.fmt_type(p)).collect();
format!("fn({}) -> {}", ps.join(", "), self.fmt_type(return_type))
}
TypeExpr::TypeParam(n) => n.clone(),
}
}
}
+327
View File
@@ -0,0 +1,327 @@
//! el-fmt — canonical source formatter for el.
//!
//! Formats a `.el` source file into its canonical representation.
//! Parsing an already-formatted file and re-formatting it produces identical output.
pub mod config;
pub mod error;
pub mod formatter;
pub use config::FmtConfig;
pub use error::FmtError;
pub use formatter::Formatter;
/// Format el source code. Returns the canonical formatted version.
pub fn format(source: &str) -> Result<String, FmtError> {
format_with_config(source, &FmtConfig::default())
}
/// Format with an explicit configuration.
pub fn format_with_config(source: &str, config: &FmtConfig) -> Result<String, FmtError> {
let tokens =
el_lexer::tokenize(source).map_err(|e| FmtError::Lex(e.to_string()))?;
let program =
el_parser::parse(tokens, source.to_string()).map_err(|e| FmtError::Parse(e.to_string()))?;
Formatter::new(config.clone()).format(&program)
}
/// Check whether `source` is already in canonical form.
/// Returns `true` if formatting would produce no changes.
pub fn is_canonical(source: &str) -> Result<bool, FmtError> {
let formatted = format(source)?;
Ok(formatted == source)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
fn fmt(src: &str) -> String {
format(src).unwrap()
}
fn idempotent(src: &str) {
let once = fmt(src);
let twice = fmt(&once);
assert_eq!(once, twice, "format not idempotent for:\n{src}");
}
// 1. Integer literal
#[test]
fn test_integer_literal() {
assert_eq!(fmt("42"), "42\n");
}
// 2. Let binding (no type annotation in source → formatter emits inferred type)
// We parse "let x = 1" which gives type_ann from the parser.
// Since el-parser always injects a type_ann, we just check the output is stable.
#[test]
fn test_let_binding_idempotent() {
// Round-trip: parse what we emit and re-emit
let source = "let x: Int = 1\n";
assert_eq!(fmt(source), source);
idempotent(source);
}
// 3. Binary operator spacing
#[test]
fn test_binary_op_spacing() {
let out = fmt("1 + 2\n");
assert!(out.contains("1 + 2"), "expected '1 + 2' in: {out}");
}
// 4. Function definition canonical form
#[test]
fn test_fn_def() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let out = fmt(src);
assert!(out.contains("fn add("), "missing fn signature: {out}");
assert!(out.contains("return a + b"), "missing return: {out}");
idempotent(src);
}
// 5. Nested function has 4-space indent
#[test]
fn test_nested_indent() {
let src = "fn outer() -> Void {\n fn inner() -> Void {\n }\n}\n";
let out = fmt(src);
assert!(out.contains(" fn inner("), "inner fn not indented: {out}");
idempotent(src);
}
// 6. If expression spacing
#[test]
fn test_if_expr() {
let src = "fn f() -> Void {\n if true {\n }\n}\n";
let out = fmt(src);
assert!(out.contains("if true"), "missing if: {out}");
idempotent(src);
}
// 7. If-else expression
#[test]
fn test_if_else() {
let src = "fn f(x: Int) -> Void {\n if x {\n } else {\n }\n}\n";
let out = fmt(src);
assert!(out.contains("else"), "missing else: {out}");
idempotent(src);
}
// 8. Match expression arms on own lines
#[test]
fn test_match_expr() {
let src = "fn f(x: Status) -> Void {\n match x {\n Status::Active(v) => 1\n _ => 0\n }\n}\n";
let out = fmt(src);
assert!(out.contains("match x"), "missing match: {out}");
assert!(out.contains("=>"), "missing arm: {out}");
idempotent(src);
}
// 9. Activate expression
#[test]
fn test_activate() {
let src = "fn f() -> Void {\n activate User where \"active users\"\n}\n";
let out = fmt(src);
assert!(out.contains("activate User where"), "missing activate: {out}");
idempotent(src);
}
// 10. Sealed block
#[test]
fn test_sealed_block() {
let src = "fn f() -> Void {\n sealed {\n let x: Int = 1\n }\n}\n";
let out = fmt(src);
assert!(out.contains("sealed {"), "missing sealed: {out}");
idempotent(src);
}
// 11. Array literal
#[test]
fn test_array_literal() {
let src = "[1, 2, 3]\n";
let out = fmt(src);
assert!(out.contains("[1, 2, 3]"), "missing array: {out}");
idempotent(src);
}
// 12. Field access
#[test]
fn test_field_access() {
let src = "fn f(u: User) -> Void {\n u.name\n}\n";
let out = fmt(src);
assert!(out.contains("u.name"), "missing field access: {out}");
idempotent(src);
}
// 13. Function call with args
#[test]
fn test_fn_call() {
let src = "foo(1, 2)\n";
let out = fmt(src);
assert!(out.contains("foo(1, 2)"), "missing call: {out}");
idempotent(src);
}
// 14. Type definition
#[test]
fn test_type_def() {
let src = "type User {\n name: String\n age: Int\n}\n";
let out = fmt(src);
assert!(out.contains("type User {"), "missing type def: {out}");
assert!(out.contains("name: String"), "missing field: {out}");
idempotent(src);
}
// 15. Enum definition
#[test]
fn test_enum_def() {
let src = "enum Status {\n Active\n Inactive\n}\n";
let out = fmt(src);
assert!(out.contains("enum Status {"), "missing enum def: {out}");
assert!(out.contains("Active"), "missing variant: {out}");
idempotent(src);
}
// 16. Decorator on fn
#[test]
fn test_decorator() {
let src = "@experience\nfn handle() -> Void {\n}\n";
let out = fmt(src);
assert!(out.contains("@experience"), "missing decorator: {out}");
idempotent(src);
}
// 17. Multiple decorators in order
#[test]
fn test_multiple_decorators() {
let src = "@public\n@experience\nfn handle() -> Void {\n}\n";
let out = fmt(src);
let pub_pos = out.find("@public").unwrap();
let exp_pos = out.find("@experience").unwrap();
assert!(pub_pos < exp_pos, "decorators out of order: {out}");
idempotent(src);
}
// 18. Return type annotation
#[test]
fn test_return_type() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let out = fmt(src);
assert!(out.contains("-> Int"), "missing return type: {out}");
idempotent(src);
}
// 19. Result type
#[test]
fn test_result_type() {
let src = "fn load() -> Result<String, Error> {\n return \"ok\"\n}\n";
let out = fmt(src);
assert!(out.contains("Result<String, Error>"), "missing result type: {out}");
idempotent(src);
}
// 20. Optional type
#[test]
fn test_optional_type() {
let src = "fn find() -> String? {\n return \"ok\"\n}\n";
let out = fmt(src);
assert!(out.contains("String?"), "missing optional type: {out}");
idempotent(src);
}
// 21. Trailing newline always present
#[test]
fn test_trailing_newline() {
let out = fmt("42");
assert!(out.ends_with('\n'), "missing trailing newline");
}
// 22. is_canonical returns true for already-canonical source
#[test]
fn test_is_canonical_true() {
let src = "42\n";
assert!(is_canonical(src).unwrap(), "expected canonical");
}
// 23. is_canonical returns false for non-canonical source
#[test]
fn test_is_canonical_false() {
// No trailing newline
let result = is_canonical("42");
// Either it returns false OR the formatter fixes it
// Either way it should not error
assert!(result.is_ok());
}
// 24. Empty program produces just a newline
#[test]
fn test_empty_program() {
// An empty string has no stmts so produces nothing; trailing newline adds one
let out = fmt("");
assert_eq!(out, "\n");
}
// 25. Idempotence for multiple constructs
#[test]
fn test_idempotent_fn_def() {
idempotent("fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n");
idempotent("fn noop() -> Void {\n}\n");
}
#[test]
fn test_idempotent_type_def() {
idempotent("type User {\n name: String\n age: Int\n}\n");
}
#[test]
fn test_idempotent_enum_def() {
idempotent("enum Status {\n Active\n Inactive\n}\n");
}
// 26. Test block
#[test]
fn test_test_block() {
let src = "test \"my test\" {\n assert 1 == 1\n}\n";
let out = fmt(src);
assert!(out.contains("test \"my test\""), "missing test block: {out}");
idempotent(src);
}
// 27. Wildcard pattern in match
#[test]
fn test_wildcard_pattern() {
let src = "fn f(x: Status) -> Void {\n match x {\n _ => 0\n }\n}\n";
let out = fmt(src);
assert!(out.contains("_ =>"), "missing wildcard: {out}");
idempotent(src);
}
// 28. Binding pattern in match
#[test]
fn test_binding_pattern() {
let src = "fn f(x: Int) -> Void {\n match x {\n v => v\n }\n}\n";
let out = fmt(src);
assert!(out.contains("v =>"), "missing binding: {out}");
idempotent(src);
}
// 29. Enum variant with payload
#[test]
fn test_enum_variant_payload() {
let src = "enum Msg {\n Value(Int)\n Empty\n}\n";
let out = fmt(src);
assert!(out.contains("Value(Int)"), "missing payload variant: {out}");
idempotent(src);
}
// 30. for loop
#[test]
fn test_for_loop() {
let src = "fn f(items: [Int]) -> Void {\n for x in items {\n x\n }\n}\n";
let out = fmt(src);
assert!(out.contains("for x in"), "missing for loop: {out}");
idempotent(src);
}
}