0b1b39f829
The CLI help text claimed `render` was symmetric to `parse`, but `render` was wired to ailang_core::pretty::module (an older human-pretty form) while `parse` consumed form (A). Two different "text projections" coexisted under one name. Fix: Cmd::Render and Cmd::Describe (both branches) now call ailang_surface::print, the actual inverse of `parse`. Round-trip gate via `ail render | ail parse` is now an e2e test in crates/ail/tests/e2e.rs in addition to the existing unit-level round-trip across all 25 fixtures in ailang-surface/tests. Cleanup: `ailang_core::pretty::module` and its three private helpers (`def_block`, `term_block`, `term_inline`) deleted — ~260 LOC of duplicate-purpose code removed. pretty.rs goes from 457 to 196 LOC. Surviving surface is intentionally narrow: manifest, type_to_string, pattern_to_string — the diagnostic stringification used in error messages, where one-line ML notation reads better than form (A)'s nested S-expressions. Module-level doc rewritten to nail down the single-purpose framing. Tests: 89/89 (e2e +1, ailang-core unit -1 since the deleted test exercised the deleted fn). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
197 lines
6.5 KiB
Rust
197 lines
6.5 KiB
Rust
//! 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<a> -> 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::<Vec<_>>()
|
|
.join(", ")
|
|
)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" | ");
|
|
let body = if t.vars.is_empty() {
|
|
ctors
|
|
} else {
|
|
format!("forall {}. {}", t.vars.join(" "), ctors)
|
|
};
|
|
("type", body)
|
|
}
|
|
};
|
|
writeln!(
|
|
s,
|
|
" {kw:5} {name:<width$} :: {ty} [{h}]",
|
|
kw = kw,
|
|
name = def.name(),
|
|
width = max_name,
|
|
ty = ty,
|
|
h = h,
|
|
)
|
|
.unwrap();
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Render a [`Pattern`] as a single-line string.
|
|
///
|
|
/// Used by `ailang-check` when constructing diagnostic messages.
|
|
pub fn pattern_to_string(p: &Pattern) -> 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::<Vec<_>>()
|
|
.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<Int>`), 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::<Vec<_>>()
|
|
.join(", ");
|
|
format!("{name}<{xs}>")
|
|
}
|
|
}
|
|
Type::Var { name } => name.clone(),
|
|
Type::Fn { params, ret, effects } => {
|
|
let p = params
|
|
.iter()
|
|
.map(type_to_string)
|
|
.collect::<Vec<_>>()
|
|
.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, 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![],
|
|
},
|
|
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,
|
|
},
|
|
doc: None,
|
|
}),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_contains_type_and_hash() {
|
|
let s = manifest(&sample_module());
|
|
assert!(s.contains("add"));
|
|
assert!(s.contains("(Int, Int) -> Int"));
|
|
}
|
|
}
|