Files
AILang/crates/ailang-core/src/pretty.rs
T
Brummel 6e6b6a14fb Iter 2: verschachteltes if, Strings, JSON-Output, deps
- Codegen: current_block-Tracking ersetzt die Heuristik im phi-Lowering;
  verschachtelte if-Ausdrücke produzieren jetzt korrekte LLVM IR.
  examples/max3.ail.json + Test schützt gegen Regression.
- Strings: Lit::Str / Type Str / io/print_str Effekt-Op; Strings sind im
  MVP immutable Konstanten. examples/hello.ail.json als zweiter E2E-Test.
- CLI: --json für manifest und builtins; neuer deps-Subcommand listet
  statische Symbol-Referenzen pro Definition. Effekt-Ops mit Prefix
  effect: markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:28:16 +02:00

261 lines
7.6 KiB
Rust

//! Pretty-Printer: AST → menschenlesbare Textform.
//!
//! Die Textform ist als Diff- und Review-Werkzeug gedacht. Die
//! kanonische Quelle bleibt die JSON-Form. Jede pretty-Ausgabe ist
//! deterministisch.
use crate::ast::*;
use std::fmt::Write;
pub fn module(m: &Module) -> String {
let mut s = String::new();
writeln!(s, "(module {}", m.name).unwrap();
if !m.imports.is_empty() {
for imp in &m.imports {
match &imp.alias {
Some(a) => writeln!(s, " (import {} as {})", imp.module, a).unwrap(),
None => writeln!(s, " (import {})", imp.module).unwrap(),
}
}
}
for (i, def) in m.defs.iter().enumerate() {
if i > 0 {
s.push('\n');
}
let body = def_block(def, 2);
s.push_str(&body);
s.push('\n');
}
s.push(')');
s.push('\n');
s
}
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)),
};
writeln!(
s,
" {kw:5} {name:<width$} :: {ty} [{h}]",
kw = kw,
name = def.name(),
width = max_name,
ty = ty,
h = h,
)
.unwrap();
}
s
}
fn def_block(def: &Def, indent: usize) -> String {
let pad = " ".repeat(indent);
match def {
Def::Fn(f) => {
let params = if f.params.is_empty() {
"[]".to_string()
} else {
format!("[{}]", f.params.join(" "))
};
let mut s = format!(
"{pad}(fn {name} :: {ty} {params}\n",
pad = pad,
name = f.name,
ty = type_to_string(&f.ty),
params = params,
);
s.push_str(&term_block(&f.body, indent + 2));
s.push(')');
s
}
Def::Const(c) => {
let mut s = format!(
"{pad}(const {name} :: {ty}\n",
pad = pad,
name = c.name,
ty = type_to_string(&c.ty),
);
s.push_str(&term_block(&c.value, indent + 2));
s.push(')');
s
}
}
}
fn term_block(t: &Term, indent: usize) -> String {
let pad = " ".repeat(indent);
match t {
Term::Lit { lit } => format!("{pad}{}", lit_to_string(lit)),
Term::Var { name } => format!("{pad}{name}"),
Term::App { callee, args } => {
let mut s = format!("{pad}(");
s.push_str(&term_inline(callee));
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Let { name, value, body } => {
let mut s = format!("{pad}(let {name}\n");
s.push_str(&term_block(value, indent + 2));
s.push('\n');
s.push_str(&term_block(body, indent + 2));
s.push(')');
s
}
Term::If { cond, then, else_ } => {
let mut s = format!("{pad}(if\n");
s.push_str(&term_block(cond, indent + 2));
s.push('\n');
s.push_str(&term_block(then, indent + 2));
s.push('\n');
s.push_str(&term_block(else_, indent + 2));
s.push(')');
s
}
Term::Do { op, args } => {
let mut s = format!("{pad}(do {op}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
}
}
fn term_inline(t: &Term) -> String {
match t {
Term::Lit { lit } => lit_to_string(lit),
Term::Var { name } => name.clone(),
Term::App { callee, args } => {
let mut s = String::from("(");
s.push_str(&term_inline(callee));
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Do { op, args } => {
let mut s = format!("(do {op}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
// Strukturelle Terms in Inline-Form rekursiv schwer; fallback:
Term::Let { name, value, body } => {
format!(
"(let {name} {} {})",
term_inline(value),
term_inline(body)
)
}
Term::If { cond, then, else_ } => {
format!(
"(if {} {} {})",
term_inline(cond),
term_inline(then),
term_inline(else_)
)
}
}
}
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 escapt für uns; das Ergebnis ist ein gültiges
// JSON-String-Literal, was für uns als kanonische Form ausreicht.
serde_json::to_string(value).unwrap()
}
Literal::Unit => "()".to_string(),
}
}
pub fn type_to_string(t: &Type) -> String {
match t {
Type::Con { name } => name.clone(),
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() },
],
},
doc: None,
}),
],
}
}
#[test]
fn pretty_print_does_not_panic() {
let s = module(&sample_module());
assert!(s.contains("(module sample"));
assert!(s.contains("(fn add"));
assert!(s.contains("(+ a b)"));
}
#[test]
fn manifest_contains_type_and_hash() {
let s = manifest(&sample_module());
assert!(s.contains("add"));
assert!(s.contains("(Int, Int) -> Int"));
}
}