Iter 14c: ailang-surface ships — form (A) parser + pretty-printer
Strictly additive new crate per Decision 6 architectural pin. JSON-AST stays the source of truth; ailang-surface is one producer/consumer of ailang_core::ast::Module values. ailang-check and ailang-codegen unchanged. Crate contents: - src/lex.rs (~264 LOC): 3-rule lexical core. Whitespace and parens delimit tokens; semicolon to EOL is comment; first-character classifier (digit -> int, " -> string, else -> ident). - src/parse.rs (~1041 LOC): hand-written recursive descent. One Rust fn per EBNF production. No parser-combinator dep. - src/print.rs (~371 LOC): deterministic pretty-printer. Round-trip contract with parse() is the surface's correctness gate. - tests/round_trip.rs (~128 LOC): integration test runs every examples/*.ail.json fixture through print -> parse -> canonical JSON, asserts canonical-byte equality with the original. Two AST-driven form widenings beyond the 14b sketch (both folded into DESIGN.md Decision 6): - lam-term carries (typed name type) params, ret type, and optional effects (Term::Lam has parallel param_tys/ret_ty/ effects fields). - import-clause admits (import name (as alias)?) (Import.alias is Option<String>). Production count ~28, under 30-rule budget. Constraint 1 (formalisable for foreign LLM) intact. Verification: - cargo build --workspace green. - cargo test --workspace: 76 tests green (was 64; +9 surface unit, +2 round-trip integration). All 17 fixtures round-trip byte-identical at canonical level. 3 hand-written .ailx exhibits parse to canonical JSON identical to their .ail.json siblings. - cargo doc --no-deps zero warnings (DESIGN.md item 6 invariant). Manual smoke test (ail parse → ail run): hello, box, list_map_poly all produce expected output through the form-(A) authoring lane end-to-end. CLI: ail parse <file.ailx> [-o <file.ail.json>]. .ail.json remains a first-class input to every existing subcommand. Plan 14d: stdlib (std_list.ailx with length/filter/fold/concat/ reverse/head/tail), authored in form (A) from day one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
//! Pretty-printer for form (A).
|
||||
//!
|
||||
//! Mirrors the parser in [`mod@crate::parse`]. Output is deterministic
|
||||
//! and parseable by the parser — round-trip is the gating contract
|
||||
//! (see `tests/round_trip.rs`).
|
||||
//!
|
||||
//! Indentation is informational only (the lexer ignores it). Two-space
|
||||
//! per level. Comments are NOT emitted.
|
||||
|
||||
use ailang_core::ast::{
|
||||
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, Term, Type, TypeDef,
|
||||
};
|
||||
|
||||
/// Print a module in form (A).
|
||||
pub fn print(module: &Module) -> String {
|
||||
let mut out = String::new();
|
||||
out.push('(');
|
||||
out.push_str("module ");
|
||||
out.push_str(&module.name);
|
||||
for imp in &module.imports {
|
||||
out.push('\n');
|
||||
write_import(&mut out, imp, 1);
|
||||
}
|
||||
for def in &module.defs {
|
||||
out.push('\n');
|
||||
write_def(&mut out, def, 1);
|
||||
}
|
||||
out.push(')');
|
||||
out.push('\n');
|
||||
out
|
||||
}
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
fn indent(out: &mut String, level: usize) {
|
||||
for _ in 0..level {
|
||||
out.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string_lit(out: &mut String, s: &str) {
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
// ---- imports & defs -------------------------------------------------------
|
||||
|
||||
fn write_import(out: &mut String, imp: &Import, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("(import ");
|
||||
out.push_str(&imp.module);
|
||||
if let Some(alias) = &imp.alias {
|
||||
out.push_str(" as ");
|
||||
out.push_str(alias);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
fn write_def(out: &mut String, def: &Def, level: usize) {
|
||||
match def {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("(data ");
|
||||
out.push_str(&td.name);
|
||||
if !td.vars.is_empty() {
|
||||
out.push_str(" (vars");
|
||||
for v in &td.vars {
|
||||
out.push(' ');
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
if let Some(doc) = &td.doc {
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(doc ");
|
||||
write_string_lit(out, doc);
|
||||
out.push(')');
|
||||
}
|
||||
for c in &td.ctors {
|
||||
out.push('\n');
|
||||
write_ctor(out, c, level + 1);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
fn write_ctor(out: &mut String, c: &Ctor, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("(ctor ");
|
||||
out.push_str(&c.name);
|
||||
for f in &c.fields {
|
||||
out.push(' ');
|
||||
write_type(out, f);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("(fn ");
|
||||
out.push_str(&fd.name);
|
||||
if let Some(doc) = &fd.doc {
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(doc ");
|
||||
write_string_lit(out, doc);
|
||||
out.push(')');
|
||||
}
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(type ");
|
||||
write_type(out, &fd.ty);
|
||||
out.push(')');
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(params");
|
||||
for p in &fd.params {
|
||||
out.push(' ');
|
||||
out.push_str(p);
|
||||
}
|
||||
out.push(')');
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(body ");
|
||||
write_term(out, &fd.body, level + 2);
|
||||
out.push(')');
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("(const ");
|
||||
out.push_str(&cd.name);
|
||||
if let Some(doc) = &cd.doc {
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(doc ");
|
||||
write_string_lit(out, doc);
|
||||
out.push(')');
|
||||
}
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(type ");
|
||||
write_type(out, &cd.ty);
|
||||
out.push(')');
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(body ");
|
||||
write_term(out, &cd.value, level + 2);
|
||||
out.push(')');
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
// ---- types ----------------------------------------------------------------
|
||||
|
||||
fn write_type(out: &mut String, t: &Type) {
|
||||
match t {
|
||||
Type::Var { name } => out.push_str(name),
|
||||
Type::Con { name, args } => {
|
||||
out.push_str("(con ");
|
||||
out.push_str(name);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_type(out, a);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Type::Fn {
|
||||
params,
|
||||
ret,
|
||||
effects,
|
||||
} => {
|
||||
out.push_str("(fn-type (params");
|
||||
for p in params {
|
||||
out.push(' ');
|
||||
write_type(out, p);
|
||||
}
|
||||
out.push_str(") (ret ");
|
||||
write_type(out, ret);
|
||||
out.push(')');
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" (effects");
|
||||
for e in effects {
|
||||
out.push(' ');
|
||||
out.push_str(e);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Type::Forall { vars, body } => {
|
||||
out.push_str("(forall (vars");
|
||||
for v in vars {
|
||||
out.push(' ');
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push_str(") ");
|
||||
write_type(out, body);
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- terms ----------------------------------------------------------------
|
||||
|
||||
fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
match t {
|
||||
Term::Lit { lit } => write_lit(out, lit),
|
||||
Term::Var { name } => out.push_str(name),
|
||||
Term::App { callee, args } => {
|
||||
out.push_str("(app ");
|
||||
write_term(out, callee, level);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
out.push_str("(let ");
|
||||
out.push_str(name);
|
||||
out.push(' ');
|
||||
write_term(out, value, level);
|
||||
out.push(' ');
|
||||
write_term(out, body, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
out.push_str("(if ");
|
||||
write_term(out, cond, level);
|
||||
out.push(' ');
|
||||
write_term(out, then, level);
|
||||
out.push(' ');
|
||||
write_term(out, else_, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
out.push_str("(do ");
|
||||
out.push_str(op);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Ctor { type_name, ctor, args } => {
|
||||
out.push_str("(term-ctor ");
|
||||
out.push_str(type_name);
|
||||
out.push(' ');
|
||||
out.push_str(ctor);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Match { scrutinee, arms } => {
|
||||
out.push_str("(match ");
|
||||
write_term(out, scrutinee, level);
|
||||
for arm in arms {
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
write_arm(out, arm, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Lam {
|
||||
params,
|
||||
param_tys,
|
||||
ret_ty,
|
||||
effects,
|
||||
body,
|
||||
} => {
|
||||
out.push_str("(lam (params");
|
||||
for (name, ty) in params.iter().zip(param_tys.iter()) {
|
||||
out.push_str(" (typed ");
|
||||
out.push_str(name);
|
||||
out.push(' ');
|
||||
write_type(out, ty);
|
||||
out.push(')');
|
||||
}
|
||||
out.push_str(") (ret ");
|
||||
write_type(out, ret_ty);
|
||||
out.push(')');
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" (effects");
|
||||
for e in effects {
|
||||
out.push(' ');
|
||||
out.push_str(e);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
out.push_str(" (body ");
|
||||
write_term(out, body, level);
|
||||
out.push_str("))");
|
||||
}
|
||||
Term::Seq { lhs, rhs } => {
|
||||
out.push_str("(seq ");
|
||||
write_term(out, lhs, level);
|
||||
out.push(' ');
|
||||
write_term(out, rhs, level);
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_arm(out: &mut String, arm: &Arm, level: usize) {
|
||||
out.push_str("(case ");
|
||||
write_pattern(out, &arm.pat);
|
||||
out.push(' ');
|
||||
write_term(out, &arm.body, level + 1);
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
fn write_lit(out: &mut String, lit: &Literal) {
|
||||
match lit {
|
||||
Literal::Int { value } => out.push_str(&value.to_string()),
|
||||
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
|
||||
Literal::Str { value } => write_string_lit(out, value),
|
||||
Literal::Unit => out.push_str("(lit-unit)"),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pattern(out: &mut String, p: &Pattern) {
|
||||
match p {
|
||||
Pattern::Wild => out.push('_'),
|
||||
Pattern::Var { name } => out.push_str(name),
|
||||
Pattern::Lit { lit } => {
|
||||
out.push_str("(pat-lit ");
|
||||
// Inside pat-lit, write the bare literal form (no `(lit-unit)`
|
||||
// — Unit is not allowed in pat-lit per the grammar).
|
||||
match lit {
|
||||
Literal::Int { value } => out.push_str(&value.to_string()),
|
||||
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
|
||||
Literal::Str { value } => write_string_lit(out, value),
|
||||
Literal::Unit => {
|
||||
// Unit is not a valid pat-lit; fall back to a bare unit-lit
|
||||
// form. This is unreachable for any well-formed AST that
|
||||
// came through the typechecker.
|
||||
out.push_str("(lit-unit)");
|
||||
}
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Pattern::Ctor { ctor, fields } => {
|
||||
out.push_str("(pat-ctor ");
|
||||
out.push_str(ctor);
|
||||
for f in fields {
|
||||
out.push(' ');
|
||||
write_pattern(out, f);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user