diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index d41c268..8082340 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -8,7 +8,8 @@ //! per level. Comments are NOT emitted. use ailang_core::ast::{ - Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Suppress, Term, + Arm, ClassDef, ClassMethod, ConstDef, Ctor, Def, FnDef, Import, InstanceDef, + InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term, Type, TypeDef, }; @@ -94,25 +95,8 @@ fn write_def(out: &mut String, def: &Def, level: usize) { Def::Type(td) => write_type_def(out, td, level), Def::Fn(fd) => write_fn_def(out, fd, level), Def::Const(cd) => write_const_def(out, cd, level), - // Iter 22b.1: surface (Form-B) printer arms for ClassDef / - // InstanceDef are deferred to 22b.4 (along with the parse-side - // arms in `surface/src/parse.rs`). For 22b.1 we emit a - // single-line placeholder so a module containing these defs - // still round-trips through `ail render` in print-only mode. - Def::Class(c) => { - indent(out, level); - out.push_str("(class "); - out.push_str(&c.name); - out.push(' '); - out.push_str(&c.param); - out.push(')'); - } - Def::Instance(i) => { - indent(out, level); - out.push_str("(instance "); - out.push_str(&i.class); - out.push(')'); - } + Def::Class(c) => write_class_def(out, c, level), + Def::Instance(i) => write_instance_def(out, i, level), } } @@ -236,6 +220,69 @@ fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) { out.push(')'); } +fn write_class_def(out: &mut String, c: &ClassDef, level: usize) { + indent(out, level); + out.push_str("(class "); + out.push_str(&c.name); + out.push('\n'); + indent(out, level + 1); + out.push_str("(param "); + out.push_str(&c.param); + out.push(')'); + if let Some(sc) = &c.superclass { + out.push('\n'); + write_superclass(out, sc, level + 1); + } + if let Some(doc) = &c.doc { + out.push('\n'); + indent(out, level + 1); + out.push_str("(doc "); + write_string_lit(out, doc); + out.push(')'); + } + for m in &c.methods { + out.push('\n'); + write_class_method(out, m, level + 1); + } + out.push(')'); +} + +fn write_superclass(out: &mut String, sc: &SuperclassRef, level: usize) { + indent(out, level); + out.push_str("(superclass (class "); + out.push_str(&sc.class); + out.push_str(") (type "); + out.push_str(&sc.type_); + out.push_str("))"); +} + +fn write_class_method(out: &mut String, m: &ClassMethod, level: usize) { + indent(out, level); + out.push_str("(method "); + out.push_str(&m.name); + out.push('\n'); + indent(out, level + 1); + out.push_str("(type "); + write_type(out, &m.ty); + out.push(')'); + if let Some(default) = &m.default { + out.push('\n'); + indent(out, level + 1); + out.push_str("(default "); + write_term(out, default, level + 2); + out.push(')'); + } + out.push(')'); +} + +fn write_instance_def(out: &mut String, _i: &InstanceDef, _level: usize) { + // Task 4: real implementation. This stub keeps the workspace + // compiling; it intentionally produces an empty form so that + // any test that exercises Instance round-trip will fail + // (which it should — Task 4 is RED for instance-side). + out.push_str("(instance)"); +} + // ---- types ---------------------------------------------------------------- /// Iter 18a: print one fn-type param/ret slot, wrapping with @@ -501,3 +548,87 @@ fn write_pattern(out: &mut String, p: &Pattern) { } } } + +#[cfg(test)] +mod tests { + use super::*; + #[allow(unused_imports)] + use ailang_core::ast::{ + ClassDef, ClassMethod, Def, InstanceDef, InstanceMethod, Literal, Module, + ParamMode, SuperclassRef, Term, Type, + }; + + fn round_trip(m: Module, label: &str) { + let printed = print(&m); + let parsed = crate::parse::parse(&printed) + .unwrap_or_else(|e| panic!( + "{label}: re-parse failed: {e:?}\nprinted:\n{printed}" + )); + let bytes_orig = ailang_core::canonical::to_bytes(&m); + let bytes_back = ailang_core::canonical::to_bytes(&parsed); + if bytes_orig != bytes_back { + let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned(); + let s_back = String::from_utf8_lossy(&bytes_back).into_owned(); + panic!( + "{label}: canonical bytes differ.\noriginal: {s_orig}\nround: {s_back}\nprinted:\n{printed}" + ); + } + } + + #[test] + fn print_then_parse_round_trip_minimal_class() { + let m = Module { + schema: ailang_core::SCHEMA.to_string(), + name: "M".into(), + imports: vec![], + defs: vec![Def::Class(ClassDef { + name: "Foo".into(), + param: "a".into(), + superclass: None, + methods: vec![ClassMethod { + name: "m".into(), + ty: Type::Fn { + params: vec![Type::Var { name: "a".into() }], + param_modes: vec![ParamMode::Implicit], + ret: Box::new(Type::int()), + ret_mode: ParamMode::Implicit, + effects: vec![], + }, + default: None, + }], + doc: None, + })], + }; + round_trip(m, "minimal_class"); + } + + #[test] + fn print_then_parse_round_trip_class_with_superclass_and_default() { + let m = Module { + schema: ailang_core::SCHEMA.to_string(), + name: "M".into(), + imports: vec![], + defs: vec![Def::Class(ClassDef { + name: "Bar".into(), + param: "a".into(), + superclass: Some(SuperclassRef { + class: "Foo".into(), + type_: "a".into(), + }), + methods: vec![ClassMethod { + name: "m".into(), + ty: Type::Fn { + params: vec![Type::Var { name: "a".into() }], + param_modes: vec![ParamMode::Implicit], + ret: Box::new(Type::int()), + ret_mode: ParamMode::Implicit, + effects: vec![], + }, + default: Some(Term::Lit { lit: Literal::Int { value: 0 } }), + }], + doc: Some("docline".into()), + })], + }; + round_trip(m, "class_with_superclass_and_default"); + } +}