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:
@@ -391,6 +391,44 @@ on the shelf for a future iter only if both fail.
|
||||
`build`, `run`, `manifest`, `deps`, `diff`, `workspace`,
|
||||
`builtins`) keeps its current `.ail.json` interface.
|
||||
|
||||
### Form refinements during Iter 14c implementation
|
||||
|
||||
Two productions in the original 14b sketch had to be widened
|
||||
during implementation to round-trip the existing AST faithfully.
|
||||
Captured here for the spec record:
|
||||
|
||||
1. **`lam-term` carries types and effects.** The AST's `Term::Lam`
|
||||
stores parallel `params`, `param_tys`, `ret_ty`, and `effects`
|
||||
fields. The 14b sketch had only names. The implemented form is
|
||||
|
||||
```
|
||||
lam-term ::= "(" "lam" "(" "params" typed-param* ")"
|
||||
"(" "ret" type ")"
|
||||
effects-clause? body-attr ")"
|
||||
typed-param ::= "(" "typed" ident type ")"
|
||||
```
|
||||
|
||||
This keeps the no-precedence / one-construct-per-token-list
|
||||
invariants and adds no new lexical rules.
|
||||
|
||||
2. **`import-clause` admits an optional alias.** The AST's
|
||||
`Import.alias` is `Option<String>`; the original sketch only
|
||||
supported the `None` case. The implemented form is
|
||||
|
||||
```
|
||||
import-clause ::= "(" "import" ident ("as" ident)? ")"
|
||||
```
|
||||
|
||||
`as` is a bare ident token in this position; no special lexical
|
||||
rule is needed.
|
||||
|
||||
Neither change extends the grammar's rule budget meaningfully:
|
||||
the 30-production ceiling of constraint 1 is intact (the parser
|
||||
implements ~28 named productions). All 17 `examples/*.ail.json`
|
||||
fixtures round-trip identically through `print → parse → canonical
|
||||
JSON`; the three hand-written `.ailx` exhibits parse to canonical
|
||||
JSON identical to their corresponding `.ail.json` files.
|
||||
|
||||
## Mangling scheme (Iter 5c)
|
||||
|
||||
All AILang functions are mangled to `@ail_<module>_<def>` — even in the
|
||||
|
||||
+118
@@ -1627,4 +1627,122 @@ with `length`, `filter`, `fold`, `concat`, `reverse`, `head`,
|
||||
for the still-young parameterised-ADT pipeline. Written in the
|
||||
new surface from day one — no JSON authoring of stdlib.
|
||||
|
||||
## Iter 14c — `ailang-surface` parser + pretty-printer ships
|
||||
|
||||
Implements Decision 6's form (A). New crate `ailang-surface`
|
||||
(~1843 LOC across `lex.rs`, `parse.rs`, `print.rs`, lib root,
|
||||
plus tests) ships as a strictly additive producer of
|
||||
`ailang-core::ast::Module` values. `ailang-check` and
|
||||
`ailang-codegen` were not modified — projection-agnostic, as
|
||||
the architectural pin requires.
|
||||
|
||||
**Implementer dispatch went clean.** Brief gave the EBNF, the
|
||||
fixtures to round-trip, the lexer rule, and the architectural
|
||||
constraints. Implementer hand-wrote a recursive-descent parser
|
||||
(one Rust fn per EBNF production), a deterministic pretty-
|
||||
printer, an integration test that runs the round-trip gate on
|
||||
every fixture, and the `ail parse` CLI subcommand. No
|
||||
parser-combinator dependency. No AST-shape changes.
|
||||
|
||||
**Two AST-driven form refinements** that were not in the 14b
|
||||
sketch (both folded into DESIGN.md Decision 6 by the
|
||||
implementer before commit):
|
||||
|
||||
1. `lam-term` had to carry `param_tys`, `ret_ty`, and
|
||||
`effects` because the AST's `Term::Lam` stores parallel
|
||||
typed-param data. Production became `(lam (params (typed
|
||||
x Int) ...) (ret T) (effects ...) (body ...))`. Same
|
||||
shape-style as the rest of the form; no new lex rules.
|
||||
2. `import-clause` had to admit `Option<String>` aliases.
|
||||
Production became `(import name (as alias)?)` with `as`
|
||||
as a bare ident token. No new lex rules.
|
||||
|
||||
The 14b 30-production budget held: implementer reports ~28
|
||||
named productions in the parser. Constraint 1 (formalisable
|
||||
for foreign LLM) intact.
|
||||
|
||||
**Verification gates, all green:**
|
||||
|
||||
- `cargo build --workspace`: 0 warnings, finished.
|
||||
- `cargo test --workspace`: 76 tests pass (was 64; +9 unit
|
||||
tests in `ailang-surface`, +2 integration tests in
|
||||
`tests/round_trip.rs`, +1 e2e regression preserved). All
|
||||
17 `examples/*.ail.json` fixtures round-trip
|
||||
byte-identical through `print → parse → canonical JSON`;
|
||||
3 hand-written `.ailx` exhibits parse to canonical JSON
|
||||
byte-identical to their `.ail.json` siblings.
|
||||
- `cargo doc --no-deps`: 0 warnings (workspace invariant
|
||||
from 13d/e/f preserved; new crate's rustdoc landed
|
||||
correctly with crate-root `//!` plus all `pub` items
|
||||
documented).
|
||||
|
||||
**Manual smoke test (orchestrator-side after agent reported
|
||||
done):** `ail parse <file.ailx> -o <file.ail.json>` followed
|
||||
by `ail run <file.ail.json>` for all three exhibits:
|
||||
|
||||
```
|
||||
hello.ailx → "Hello, AILang."
|
||||
box.ailx → "42"
|
||||
list_map_poly.ailx → "2\n3\n4"
|
||||
```
|
||||
|
||||
End-to-end pipeline form (A) → AST → typecheck → codegen →
|
||||
clang → binary works on all three.
|
||||
|
||||
**Important non-issue.** `examples/*.ail.json` fixtures on
|
||||
disk are hand-formatted JSON with non-canonical key order;
|
||||
the parser produces canonical (lex-sorted) JSON. Diff at
|
||||
the file-byte level is not zero. Diff at the canonical-byte
|
||||
level is zero — which is the only thing that matters per
|
||||
Decision 1, since hashing uses canonical form. The
|
||||
round-trip test gates on canonical bytes, not file bytes.
|
||||
This is correct behaviour; flagged here so a future reader
|
||||
who runs `diff` doesn't think the surface is broken.
|
||||
|
||||
**Files created:**
|
||||
|
||||
- `crates/ailang-surface/Cargo.toml`
|
||||
- `crates/ailang-surface/src/lib.rs` (~40 LOC, rustdoc heavy)
|
||||
- `crates/ailang-surface/src/lex.rs` (~264 LOC)
|
||||
- `crates/ailang-surface/src/parse.rs` (~1041 LOC, one fn
|
||||
per production)
|
||||
- `crates/ailang-surface/src/print.rs` (~371 LOC)
|
||||
- `crates/ailang-surface/tests/round_trip.rs` (~128 LOC)
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- `Cargo.toml` (workspace) — `ailang-surface` member +
|
||||
workspace dep.
|
||||
- `Cargo.lock` — refresh.
|
||||
- `crates/ail/Cargo.toml` — `ailang-surface` dep.
|
||||
- `crates/ail/src/main.rs` — new `Parse { path, output }`
|
||||
subcommand (~36 LOC).
|
||||
- `docs/DESIGN.md` — form refinements appendix to
|
||||
Decision 6.
|
||||
- `examples/list_map_poly.ailx` — implementer added doc
|
||||
strings to match the JSON original (was a 14b design
|
||||
exhibit, not byte-aligned).
|
||||
|
||||
**Known debt:** none reported. `ailang-check` /
|
||||
`ailang-codegen` untouched per the architectural pin.
|
||||
|
||||
**Plan 14d (next, queued).** With form (A) now ergonomic
|
||||
and round-trip-verified, the std-lib path opens up. First
|
||||
target: `examples/std/std_list.ailx` with `length`,
|
||||
`filter`, `fold` (left and right), `concat`, `reverse`,
|
||||
`head`, `tail`. Each combinator a fresh test vector for
|
||||
the parameterised-ADT pipeline (which 14a opened end-to-
|
||||
end). Authored in form (A); the resulting `.ail.json` is
|
||||
what tests consume. If 14d surfaces more codegen bugs in
|
||||
the parameterised-ADT path (it likely will — 14a found
|
||||
one already), debugger handles them inline.
|
||||
|
||||
The 14b/c form-A hypothesis has held under the empirical
|
||||
test of round-tripping every existing fixture. The
|
||||
documented rollback to form (C) is now off the table for
|
||||
this iter cycle, though the architectural pin keeps it
|
||||
open for future replacement of `ailang-surface` should
|
||||
the form prove inadequate at stdlib scale.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user