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:
@@ -171,6 +171,17 @@ enum Cmd {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Parses a `.ailx` source file (form (A)) into canonical
|
||||
/// `.ail.json`. Iter 14c addition; symmetric to `render`.
|
||||
///
|
||||
/// The form-(A) projection is one of potentially many producers of
|
||||
/// `Module` values. The JSON-AST remains the source of truth and
|
||||
/// is what every other subcommand consumes.
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -531,6 +542,31 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Parse { path, output } => {
|
||||
// Read .ailx, parse via the surface crate, emit canonical
|
||||
// JSON. Symmetric to `render` (which goes the other way).
|
||||
let src = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("reading {}", path.display()))?;
|
||||
let module = match ailang_surface::parse(&src) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
eprintln!("parse error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let bytes = ailang_core::canonical::to_bytes(&module);
|
||||
match output {
|
||||
Some(p) => {
|
||||
std::fs::write(&p, &bytes)?;
|
||||
eprintln!("wrote {}", p.display());
|
||||
}
|
||||
None => {
|
||||
use std::io::Write;
|
||||
std::io::stdout().write_all(&bytes)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Deps { path, of, json, workspace } => {
|
||||
if workspace {
|
||||
let ws = ailang_core::load_workspace(&path)?;
|
||||
|
||||
Reference in New Issue
Block a user