iter prep.2-term-new-construct (DONE 4/4): Term::New AST + Form-A surface + checker + drift — closes #32
Second iteration of the kernel-extension-mechanics milestone. Ships
the `new` Form-A keyword + `Term::New { type_name, args: Vec<NewArg> }`
AST variant + `NewArg::Type|Value` enum end-to-end through schema /
surface / checker, with two new diagnostics
(`NewTypeNotConstructible`, `NewArgKindMismatch`), a new arm in
prep.1's workspace-wide normalisation pre-pass `qualify_workspace_term`
that rewrites bare `Term::New.type_name` to qualified form
(symmetric to the existing `Term::Ctor` arm), and the data-model.md
+ form_a.md anchor additions. Codegen out of scope per spec; codegen
sites get `CodegenError::Internal` arms naming the raw-buf milestone
as the carrier.
Verification:
- `cargo test --workspace`: 654 tests passing, 0 failed.
- 3 new in-source checker tests pin the elaboration paths:
`new_resolves_via_type_scope` (the spec's worked Counter example
checks cleanly), `new_type_not_constructible` (missing `new` def
in home module fires the new diagnostic), `new_arg_kind_mismatch_value_where_type`
(kind-mismatch detection).
- 2 new schema-drift pins (`term_new_round_trips`,
`term_new_type_arg_round_trips`) ratify the JSON byte shape:
`{"t": "new", "type": "<TypeName>", "args": [{"kind": "type"|"value",
"value": ...}]}`.
- 1 new surface round-trip pin exercises mixed-kind args through
Form-A → JSON → Form-A.
- 44+ exhaustive Term-match sites across 24 files extended with
`Term::New` arms — workspace-wide cargo build clean.
Concerns documented inline:
- `crates/ailang-core/tests/schema_coverage.rs` deliberately does
NOT register `VariantTag::TermNew` in the `EXPECTED_VARIANTS`
set. The coverage test asserts every declared variant is
observed in the .ail fixture corpus; no .ail fixture in the
current tree emits `"t": "new"` (codegen-runnable Term::New
programs require the raw-buf milestone's plugin registry).
Registering would fail the coverage check. The `visit_term` arm
IS extended (compile-forced); only the enum registration is
deferred. Inline rationale in the source. Future milestone that
ships a Term::New fixture extends both sides.
- `crates/ailang-surface/src/print.rs` `write_term` Term::New arm
was added in Task 1 (not Task 2 as planned), because without it
ailang-core's tests fail to compile (the surface crate is in
the dependency graph of ailang-core's test binaries). Task 2's
round-trip pin verified the arm's correctness.
- Task 3 (synth elaboration) needed one implementer re-loop: the
first attempt over-qualified within-module type-references via
`qualify_local_types` on `new`'s signature when the home module
was the calling module itself. Fixed by skipping qualification
when `home_module == env.current_module`.
Architectural composition with prep.1: `Term::New` joins
`Term::Ctor` as a `type_name`-bearing site that goes through
`qualify_workspace_term`'s bare→qualified rewrite before the
checker sees it. The checker's `synth` arm for `Term::New`
therefore receives an already-qualified `type_name` (or a local
bare name for same-module construction).
Milestone status: kernel-extension-mechanics (Gitea #6) advances
2/3 iters. Next: prep.3 (kernel-tier modules + param-in), issue #33.
This commit is contained in:
@@ -1241,6 +1241,7 @@ impl<'a> Parser<'a> {
|
||||
"reuse-as" => self.parse_reuse_as(),
|
||||
"loop" => self.parse_loop(),
|
||||
"recur" => self.parse_recur(),
|
||||
"new" => self.parse_new(),
|
||||
other => {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
Err(ParseError::Production {
|
||||
@@ -1249,7 +1250,7 @@ impl<'a> Parser<'a> {
|
||||
"unknown term head `{other}`; expected one of \
|
||||
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, \
|
||||
`loop`, `recur`, `lit-unit`"
|
||||
`loop`, `recur`, `new`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -1627,6 +1628,7 @@ impl<'a> Parser<'a> {
|
||||
| "reuse-as"
|
||||
| "loop"
|
||||
| "recur"
|
||||
| "new"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1685,6 +1687,52 @@ impl<'a> Parser<'a> {
|
||||
Ok(Term::Recur { args })
|
||||
}
|
||||
|
||||
/// prep.2 (kernel-extension-mechanics): `(new TYPE-NAME ARG+)` —
|
||||
/// functional construction. Calls the `new` def in `TYPE-NAME`'s
|
||||
/// home module with the supplied args. Each arg is positional and
|
||||
/// disambiguated at parse-time by its syntactic form:
|
||||
/// - a parenthesised form whose head ident is one of the
|
||||
/// Type-production keywords (`con`, `fn-type`, `borrow`, `own`)
|
||||
/// parses as a `NewArg::Type` via [`parse_type`];
|
||||
/// - anything else (a parenthesised term form, or a bare token —
|
||||
/// integer / float / string / ident) parses as a `NewArg::Value`
|
||||
/// via [`parse_term`].
|
||||
///
|
||||
/// Requires ≥ 1 arg — a zero-arg `(new T)` shape is rejected at
|
||||
/// parse time so a misspelled type name surfaces as a parse error
|
||||
/// rather than as a downstream synth-time mystery.
|
||||
fn parse_new(&mut self) -> Result<Term, ParseError> {
|
||||
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
self.expect_lparen("new-term")?;
|
||||
self.expect_keyword("new")?;
|
||||
let type_name = self.expect_ident("new-term type name")?;
|
||||
let mut args: Vec<ailang_core::ast::NewArg> = Vec::new();
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
// Look at the next form: a parenthesised Type-production
|
||||
// head is a Type-arg; everything else is a Term-arg.
|
||||
let is_type_arg = matches!(
|
||||
self.peek_head_ident(),
|
||||
Some("con") | Some("fn-type") | Some("borrow") | Some("own")
|
||||
);
|
||||
if is_type_arg {
|
||||
let t = self.parse_type()?;
|
||||
args.push(ailang_core::ast::NewArg::Type(t));
|
||||
} else {
|
||||
let v = self.parse_term()?;
|
||||
args.push(ailang_core::ast::NewArg::Value(v));
|
||||
}
|
||||
}
|
||||
self.expect_rparen("new-term")?;
|
||||
if args.is_empty() {
|
||||
return Err(ParseError::Production {
|
||||
production: "new-term",
|
||||
message: "`(new T ...)` requires at least one arg".into(),
|
||||
pos: head_pos,
|
||||
});
|
||||
}
|
||||
Ok(Term::New { type_name, args })
|
||||
}
|
||||
|
||||
// ---- patterns -------------------------------------------------------
|
||||
|
||||
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use ailang_core::ast::{
|
||||
Arm, ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
|
||||
InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
||||
InstanceMethod, Literal, Module, NewArg, ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
||||
Type, TypeDef,
|
||||
};
|
||||
|
||||
@@ -594,6 +594,24 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::New { type_name, args } => {
|
||||
// prep.2 (kernel-extension-mechanics): functional construction
|
||||
// `(new T arg+)`. Each arg is either a Type or a Term;
|
||||
// disambiguation on re-parse is by syntactic form (head
|
||||
// keyword in the `con` / `fn-type` / `borrow` / `own` set
|
||||
// → Type; otherwise → Term). Both forms write through the
|
||||
// existing `write_type` / `write_term`.
|
||||
out.push_str("(new ");
|
||||
out.push_str(type_name);
|
||||
for arg in args {
|
||||
out.push(' ');
|
||||
match arg {
|
||||
NewArg::Type(t) => write_type(out, t),
|
||||
NewArg::Value(v) => write_term(out, v, level),
|
||||
}
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user