//! Diagnostic helpers that stringify AST fragments. //! //! Form (A) — the round-trippable authoring surface — lives in //! `ailang-surface::print`. This module is the asymmetric, lossy //! companion: stringification routines used inside error messages //! and the manifest summary, where one-line ML-style notation //! (`forall a. List -> Int`) reads better than form (A)'s //! `(forall (vars a) (fn-type ...))`. //! //! Public entry points: //! - [`manifest`] — one line per def, used by `ail manifest`. //! - [`type_to_string`] — single-line ML-style type, used by //! `ailang-check` diagnostics, `ailang-codegen` mismatch errors, //! and `ail describe`'s text projection. //! - [`pattern_to_string`] — single-line pattern, used by //! `ailang-check` diagnostics. //! //! All output is deterministic. None of it round-trips back to AST. use crate::ast::*; use std::fmt::Write; /// Render a one-line-per-def manifest of a [`Module`]. /// /// Each line carries the def kind keyword (`fn` / `const` / `type`), /// the name padded to a fixed width, the rendered type, and the /// 16-hex-char [`crate::def_hash`] in brackets. Used by `ail manifest` /// and as the canonical "what's in this module" summary. pub fn manifest(m: &Module) -> String { let mut s = String::new(); writeln!(s, "module {}", m.name).unwrap(); let max_name = m.defs.iter().map(|d| d.name().len()).max().unwrap_or(0); for def in &m.defs { let h = crate::hash::def_hash(def); let (kw, ty) = match def { Def::Fn(f) => ("fn", type_to_string(&f.ty)), Def::Const(c) => ("const", type_to_string(&c.ty)), Def::Type(t) => { let ctors = t .ctors .iter() .map(|c| { if c.fields.is_empty() { c.name.clone() } else { format!( "{}({})", c.name, c.fields .iter() .map(type_to_string) .collect::>() .join(", ") ) } }) .collect::>() .join(" | "); let body = if t.vars.is_empty() { ctors } else { format!("forall {}. {}", t.vars.join(" "), ctors) }; ("type", body) } // Iter 22b.1: one-line summary `class C a` / `instance C T`. // Full pretty rendering of method signatures and bodies // lands in 22b.4 alongside the prose projection arms. Def::Class(c) => ("class", format!("{} {}", c.name, c.param)), Def::Instance(i) => ("instance", format!("{} {}", i.class, type_to_string(&i.type_))), }; writeln!( s, " {kw:5} {name: String { match p { Pattern::Wild => "_".into(), Pattern::Var { name } => name.clone(), Pattern::Lit { lit } => lit_to_string(lit), Pattern::Ctor { ctor, fields } => { if fields.is_empty() { ctor.clone() } else { let fs = fields .iter() .map(pattern_to_string) .collect::>() .join(" "); format!("({ctor} {fs})") } } } } fn lit_to_string(l: &Literal) -> String { match l { Literal::Int { value } => value.to_string(), Literal::Bool { value } => value.to_string(), Literal::Str { value } => { // serde_json escapes for us; the result is a valid // JSON string literal, which is enough as our canonical form. serde_json::to_string(value).unwrap() } Literal::Unit => "()".to_string(), } } /// Render a [`Type`] as a single-line string. /// /// The format mirrors typical ML-family notation: type-constructor /// applications use angle brackets (`List`), function types use /// arrow syntax (`(Int, Int) -> Int`) with optional `!eff1,eff2` /// effect suffixes, and `Forall` becomes `forall a b. ...`. Used by /// [`manifest`] and by `ailang-check` diagnostics. pub fn type_to_string(t: &Type) -> String { match t { Type::Con { name, args } => { if args.is_empty() { name.clone() } else { let xs = args .iter() .map(type_to_string) .collect::>() .join(", "); format!("{name}<{xs}>") } } Type::Var { name } => name.clone(), Type::Fn { params, ret, effects, .. } => { let p = params .iter() .map(type_to_string) .collect::>() .join(", "); let eff = if effects.is_empty() { String::new() } else { format!(" !{}", effects.join(",")) }; format!("({p}) -> {ret}{eff}", ret = type_to_string(ret)) } Type::Forall { vars, constraints: _, body } => { format!("forall {}. {}", vars.join(" "), type_to_string(body)) } } } #[cfg(test)] mod tests { use super::*; fn sample_module() -> Module { Module { schema: crate::SCHEMA.into(), name: "sample".into(), imports: vec![], defs: vec![ Def::Fn(FnDef { name: "add".into(), ty: Type::Fn { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["a".into(), "b".into()], body: Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "a".into() }, Term::Var { name: "b".into() }, ], tail: false, }, suppress: vec![], doc: None, }), ], } } #[test] fn manifest_contains_type_and_hash() { let s = manifest(&sample_module()); assert!(s.contains("add")); assert!(s.contains("(Int, Int) -> Int")); } }