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:
2026-05-28 15:31:13 +02:00
parent c8932fa54c
commit 078c39a76a
25 changed files with 1086 additions and 12 deletions
+37 -1
View File
@@ -80,7 +80,7 @@
//! Precision can be improved later; correctness is the priority for
//! this iter.
use ailang_core::ast::{Pattern, Term};
use ailang_core::ast::{NewArg, Pattern, Term};
use std::collections::BTreeSet;
/// Set of allocation sites (identified by raw pointer address cast to
@@ -201,6 +201,17 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
walk(a, out);
}
}
// prep.2 (kernel-extension-mechanics): walker through
// NewArg::Value subterms; Term::New does not yet have a
// direct codegen path (lower_term returns an Internal error
// via Pattern D until the raw-buf milestone lands).
Term::New { args, .. } => {
for arg in args {
if let NewArg::Value(v) = arg {
walk(v, out);
}
}
}
Term::Lit { .. } | Term::Var { .. } => {}
}
}
@@ -394,6 +405,21 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
}
false
}
// prep.2 (kernel-extension-mechanics): walk every NewArg::Value
// in non-tail mode (call-arg semantics). Term::New itself does
// not yet codegen; the analysis stays conservative against the
// future codegen by treating each value-arg as a non-tail
// expression whose tainted-name flow matters.
Term::New { args, .. } => {
for arg in args {
if let NewArg::Value(v) = arg {
if escapes(v, tainted, false) {
return true;
}
}
}
false
}
}
}
@@ -516,6 +542,16 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
collect_free_vars(a, bound, out);
}
}
// prep.2 (kernel-extension-mechanics): free vars of a
// `(new T args)` are the union of free vars of every
// NewArg::Value; NewArg::Type carries no term.
Term::New { args, .. } => {
for arg in args {
if let NewArg::Value(v) = arg {
collect_free_vars(v, bound, out);
}
}
}
}
}
+10
View File
@@ -492,6 +492,16 @@ impl<'a> Emitter<'a> {
Self::collect_captures(a, bound, captures, captures_set, builtins, top_level);
}
}
// prep.2 (kernel-extension-mechanics): captures of a
// `(new T args)` are the union of captures of every
// NewArg::Value; NewArg::Type carries no runtime term.
Term::New { args, .. } => {
for arg in args {
if let NewArg::Value(v) = arg {
Self::collect_captures(v, bound, captures, captures_set, builtins, top_level);
}
}
}
}
}
+22
View File
@@ -2064,6 +2064,18 @@ impl<'a> Emitter<'a> {
self.block_terminated = true;
Ok(("0".into(), "i8".into()))
}
// prep.2 (kernel-extension-mechanics): Term::New has no
// direct codegen path in this milestone — typecheck
// accepts it via synth's elaboration, but lowering to LLVM
// IR requires a desugar pass that resolves the `new` def
// and rewrites the site as a Term::App. That desugar lands
// in the raw-buf milestone. Until then, codegen of
// Term::New is an internal error: a workspace that types-
// checks should not reach codegen with a surviving Term::New.
Term::New { .. } => Err(CodegenError::Internal(
"Term::New requires the type's `new` def to be desugared first; \
codegen does not yet support Term::New directly — milestone raw-buf".into(),
)),
}
}
@@ -3543,6 +3555,16 @@ impl<'a> Emitter<'a> {
// value at its own position).
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
Term::Recur { .. } => Ok(Type::unit()),
// prep.2 (kernel-extension-mechanics): Term::New has no
// codegen path in this milestone (see lower_term's arm).
// synth_with_extras would only fire if codegen reaches a
// surviving Term::New as a callee or sub-expression — same
// BUG class as a surviving LetRec — so an internal error
// here mirrors lower_term's stance.
Term::New { .. } => Err(CodegenError::Internal(
"Term::New cannot be synthed at codegen — must be desugared first; \
codegen support lands in the raw-buf milestone".into(),
)),
}
}
}