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:
2026-05-07 16:22:14 +02:00
parent 5e8342df02
commit 706f90bacd
13 changed files with 2065 additions and 4 deletions
+371
View File
@@ -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(')');
}
}
}