iter nullary-app.1 (DONE 5/5): accept (app f) as canonical zero-arg call
Resolves Gitea #12 — the design fork from the 2026-05-20 /boss session, surfaced independently by two prior fieldtests (mut-local F3 + loop-recur `run_forever`). Mechanics, layer by layer: Parser — `crates/ailang-surface/src/parse.rs:1322-1331` drops the 4-line `expected at least one argument` guard in `parse_app_body`. Grammar comments at file top change `term+` to `term*` on both `app-term` and `tail-app-term`; doc comment on `parse_app_body` changes `1+ args` to `0+ args`. Inline comment cites #12 and notes which layers below already accept empty args. Serde — `crates/ailang-core/src/ast.rs:419-425` annotates `Term::App.args` with `#[serde(default)]`, mirroring `Term::Ctor.args`'s *actual* attrs verbatim (no `skip_serializing_if`). Write behaviour unchanged — canonical JSON still emits `"args":[]` for nullary calls; only read behaviour gains tolerance for the `args` key being absent. Verified hash-impact-free at plan time: `grep -rn '"args":\[\]' examples/*.ail.json` returns zero matches, so no existing fixture's canonical bytes shift. Doc-honesty — `design/contracts/data-model.md`: (a) the `app` jsonc shape gains an explicit "args may be empty / read-tolerant" note; (b) the existing `ctor` comment claiming "args omitted when empty" was factually false (Ctor.args has no `skip_serializing_if`; a serde probe at plan time confirmed writes always emit `"args":[]`). Rewritten to describe the actual write/read asymmetry, with a cross-reference to the new `app` note. E2E pin — `crates/ail/tests/nullary_app_e2e.rs` + `examples/nullary_app_smoke.ail`. Defines `greet : fn() -> Unit !IO` and calls it as `(app greet)`; asserts stdout `"hello\n"`. RED→GREEN pin for the parser change AND the milestone-protecting E2E for nullary call surface going forward. Fixture literal is `"hello"` (no `\n`) because `io/print_str` lowers via `puts` which appends a newline — the plan body had a `"hello\n"` literal which would have yielded `"hello\n\n"`; the implementer caught and aligned to the canonical pattern in `examples/hello.ail` while writing the fixture, fix scoped to that single file. Layers below parser untouched: typechecker's arity check at `crates/ailang-check/src/lib.rs:3300` is `args.len() != params.len()` which is `0 != 0 → false` for nullary; codegen's `args.iter().zip(sig.params.iter())` at `crates/ailang-codegen/src/lib.rs:2410` is an empty loop; LLVM `call @ail_<mod>_<name>()` with empty arglist is valid; surface printer's arg-emit loop at `crates/ailang-surface/src/print.rs:430-434` writes nothing for empty args, producing exactly `(app f)`. Verification: full workspace `cargo test --workspace --quiet` green (0 failed across all crates); drift pins `design_index_pin 5/5` + `design_schema_drift 8/8`; round-trip `2/2`; new `nullary_app_e2e 1/1`. Stats file: `bench/orchestrator-stats/2026-05-20-iter-nullary-app.1.json`. closes #12
This commit is contained in:
@@ -45,8 +45,8 @@
|
||||
//! str-lit ::= string ; string atom
|
||||
//! bool-lit ::= "true" | "false"
|
||||
//! unit-lit ::= "(" "lit-unit" ")"
|
||||
//! app-term ::= "(" "app" term term+ ")"
|
||||
//! tail-app-term ::= "(" "tail-app" term term+ ")" ; Iter 14e
|
||||
//! app-term ::= "(" "app" term term* ")"
|
||||
//! tail-app-term ::= "(" "tail-app" term term* ")" ; Iter 14e
|
||||
//! ctor-term ::= "(" "term-ctor" ident ident term* ")"
|
||||
//! match-term ::= "(" "match" term case-arm+ ")"
|
||||
//! case-arm ::= "(" "case" pattern term ")"
|
||||
@@ -1313,22 +1313,18 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
|
||||
/// Body shared by [`Self::parse_app`] and [`Self::parse_tail_app`]:
|
||||
/// callee + 1+ args + closing `)`.
|
||||
/// callee + 0+ args + closing `)`.
|
||||
fn parse_app_body(
|
||||
&mut self,
|
||||
tail: bool,
|
||||
production: &'static str,
|
||||
) -> Result<Term, ParseError> {
|
||||
let callee = self.parse_term()?;
|
||||
// 1+ args
|
||||
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production,
|
||||
message: "expected at least one argument".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
// 0+ args — empty arg list is the nullary call shape `(app f)`,
|
||||
// resolution of Gitea #12. The schema permits empty `args` on
|
||||
// `Term::App` (data-model.md: `args: [Term...]` is zero-or-more);
|
||||
// typechecker, codegen, and surface printer already handle
|
||||
// empty `args` cleanly today.
|
||||
let mut args = Vec::new();
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
args.push(self.parse_term()?);
|
||||
|
||||
Reference in New Issue
Block a user