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:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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(¶ms_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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user