//! 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 { 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 = 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 = 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")); } // Component definition (UI/reactive components) — emit as-is placeholder. Stmt::ComponentDef { name, methods, .. } => { out.push_str(&format!("{ind}component {name} {{\n")); for s in methods { self.fmt_stmt(out, s, depth + 1); } out.push_str(&format!("{ind}}}\n")); } // Catch-all: unknown/future statement kinds are emitted as a comment. #[allow(unreachable_patterns)] _ => { out.push_str(&format!("{ind}// [unformatted statement]\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::UnaryBitNot(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 = 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 = 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))); } // JSX expressions — emit minimal JSX syntax. Expr::JsxElement { tag, attrs, children, self_closing } => { out.push('<'); out.push_str(tag); for (key, val) in attrs { out.push(' '); out.push_str(key); match val { el_parser::JsxAttrValue::Str(s) => out.push_str(&format!("=\"{s}\"")), el_parser::JsxAttrValue::Expr(e) => { out.push_str("={"); self.fmt_expr(out, e, depth); out.push('}'); } } } if *self_closing { out.push_str(" />"); } else { out.push('>'); for child in children { self.fmt_expr(out, child, depth); } out.push_str(&format!("")); } } Expr::JsxExpr(inner) => { out.push('{'); self.fmt_expr(out, inner, depth); out.push('}'); } Expr::JsxText(text) => { out.push_str(text); } // Catch-all: unknown/future expression kinds. #[allow(unreachable_patterns)] _ => { out.push_str("/* [unformatted expr] */"); } } } 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 => "||", BinOp::Mod => "%", BinOp::BitAnd => "&", BinOp::BitOr => "|", BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", BinOp::NullCoalesce => "??", } } 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(), } } }