tidy: rustdoc-sweep + drift-test-narrowing — autonomous batch

Two unrelated hygiene iters bundled because they shipped together
in one autonomous-while-Boss-away batch:

- iter rustdoc-sweep: cleared all 23 `cargo doc --workspace
  --no-deps` warnings (17 in ailang-check + 4 in ailang-core + 2
  in ailang-surface). Two warning classes: pub-doc-comment links
  to pub(crate)/private items (15 hits — replaced with plain
  backtick-code-spans), and unresolved/ambiguous links (8 hits
  — fully-qualified, disambiguated to fn-form, or escaped).
  No behaviour change; tests 562 → 562.

- iter drift-test-narrowing: design_schema_drift.rs now scans
  §"Data model" only via new helper data_model_section(),
  instead of full DESIGN.md. Closes the audit-form-a-precursor
  [high] "anchor-elsewhere-passes-silently" failure mode. All
  38 anchors verified pre-edit to live in §"Data model" so the
  tightening doesn't regress to red. New pin
  data_model_section_is_bounded guards the extractor against
  silent regression. Tests 562 → 563.
This commit is contained in:
2026-05-13 13:22:37 +02:00
parent 48b1f77487
commit b638abf1e2
11 changed files with 192 additions and 31 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ pub struct SuggestedRewrite {
/// Form-A AILang snippet to substitute at the offending site. For
/// the linearity-check diagnostics this is typically `(clone <n>)`
/// or a small enclosing rewrite. The string MUST parse via the
/// surface `parse_term` entrypoint; [`crate::linearity`] tests
/// surface `parse_term` entrypoint; `crate::linearity` tests
/// guard the round-trip.
pub replacement: String,
}
+7 -7
View File
@@ -2016,7 +2016,7 @@ pub(crate) fn is_fully_concrete(t: &Type) -> bool {
}
/// Iter 22b.2 (Task 9): per-call-site residual class constraint
/// recorded by [`synth`] when a `Term::Var` resolves to a class method.
/// recorded by `synth` when a `Term::Var` resolves to a class method.
/// Carries the method name (for diagnostic rendering), the class, and
/// the type the class is applied to at the call site. The type is
/// typically a fresh metavar (which unification then ties to the
@@ -2290,7 +2290,7 @@ pub fn refine_multi_candidate_residual(
}
/// Iter 23.4: per-call-site polymorphic-free-fn observation recorded by
/// [`synth`] when a `Term::Var` resolves to a `Type::Forall`-quantified
/// `synth` when a `Term::Var` resolves to a `Type::Forall`-quantified
/// top-level def (NOT a class method — those are pushed to
/// [`ResidualConstraint`] instead). Carries the bare name, the owning
/// module (resolved through the same lookup ladder as `synth`'s Var arm),
@@ -2298,7 +2298,7 @@ pub fn refine_multi_candidate_residual(
/// fresh metavars instantiated for those vars at the call site.
///
/// Consumed by the mono pass: after `synth` completes a body, each
/// `metas[i]` is `subst.apply`'d to recover the concrete type at vars[i].
/// `metas\[i\]` is `subst.apply`'d to recover the concrete type at `vars\[i\]`.
/// Fully-concrete observations become [`crate::mono::MonoTarget::FreeFn`]
/// targets; non-concrete observations are silently dropped (covered by
/// the missing-constraint check on residuals if relevant).
@@ -3693,7 +3693,7 @@ pub struct Env {
/// actual-module-name). Sibling-shape to [`Self::module_globals`];
/// populated workspace-flat by [`crate::build_check_env`] so the
/// mono pass's per-fn entry points can re-seed the local
/// [`Self::imports`] before re-running [`synth`] on a body. Both
/// [`Self::imports`] before re-running `synth` on a body. Both
/// `check_in_workspace` and `mono::build_workspace_env` consume
/// the same env shape; `check_in_workspace` then overwrites
/// [`Self::imports`] with the current module's map at the call
@@ -3718,7 +3718,7 @@ pub struct Env {
/// [`ModuleGlobals::class_methods`], re-keyed to
/// `(qualified-class-name, method-name)` so post-retirement of
/// `MethodNameCollision` two classes can share a method name.
/// Consumed by [`synth`]'s `Term::Var` arm via the resolved class
/// Consumed by `synth`'s `Term::Var` arm via the resolved class
/// (from `method_to_candidate_classes` + dispatch) plus the method
/// name. Mono's presence checks consult
/// [`Self::method_to_candidate_classes`] (method-keyed natively).
@@ -3742,13 +3742,13 @@ pub struct Env {
/// Iter 22b.2 (Task 9): one-step superclass expansion table.
/// Maps each class name to its superclass class name. Absence from
/// the map means the class has no superclass — populated only for
/// classes that declare one. Used by [`check_fn`] to expand
/// classes that declare one. Used by `check_fn` to expand
/// `Forall.constraints` for the missing-constraint check
/// (Decision 11: max one step). The schema permits at most one
/// superclass per class.
pub class_superclasses: BTreeMap<String, String>,
/// Iter 22b.2 (Task 10): workspace-wide instance registry, threaded
/// through from [`Workspace::registry`]. Read by [`check_fn`] in the
/// through from [`Workspace::registry`]. Read by `check_fn` in the
/// concrete-residual path: when a class-method call at a fully-
/// concrete type produces a residual `(class, type_)` whose
/// `(class, type_hash(type_))` key is absent from `registry.entries`,
+7 -7
View File
@@ -23,16 +23,16 @@
//!
//! See `docs/plans/2026-05-09-22b3-monomorphisation.md` for the
//! full task list. The eventual contract is for the pass to
//! consume the workspace's typeclass [`Registry`] and the per-
//! consume the workspace's typeclass [`ailang_core::workspace::Registry`] and the per-
//! module class-method tables (already populated by
//! [`crate::check_in_workspace`]) and produce a workspace with:
//! `crate::check_in_workspace`) and produce a workspace with:
//!
//! 1. Synthesised [`Def::Fn`] entries — one per unique
//! `(class, method, type-hash)` triple observed at any
//! class-method call site — appended to the [`Registry`]'s
//! class-method call site — appended to the registry's
//! `defining_module` for that instance.
//! 2. Rewritten call sites — every `Term::Var { name }` whose
//! `name` resolves through [`Env::class_methods`] is replaced
//! `name` resolves through `crate::Env::class_methods` is replaced
//! by the corresponding mono-symbol name (qualified with the
//! instance's `defining_module` iff that module differs from
//! the calling module).
@@ -403,7 +403,7 @@ fn workspace_has_specialisable_targets(ws: &Workspace) -> bool {
///
/// Determinism: `ailang_core::canonical::type_hash` is the same
/// function `workspace::build_registry` uses to key
/// [`Registry::entries`], so a registry-key match implies a
/// [`ailang_core::workspace::Registry::entries`], so a registry-key match implies a
/// `mono_symbol` match.
pub fn mono_symbol(base: &str, ty: &Type) -> String {
mono_symbol_n(base, std::slice::from_ref(ty))
@@ -511,7 +511,7 @@ pub fn mono_target_key(t: &MonoTarget) -> (String, String, String) {
/// and apply per-fn overlay (current_module, globals from
/// module_globals, imports from module_imports, rigid_vars) plus the
/// per-module env.types overlay
/// (see [`apply_per_module_types_overlay`]).
/// (see `apply_per_module_types_overlay`).
pub fn build_workspace_env(ws: &Workspace) -> crate::Env {
crate::build_check_env(ws)
}
@@ -773,7 +773,7 @@ fn contains_rigid_var(t: &Type) -> bool {
/// 1. has name [`mono_symbol`]`(target.method, target.type_)`,
/// 2. has type = the class's method type with the class param
/// substituted to `target.type_` (rigid-var substitution via
/// [`crate::substitute_rigids`]); the result is a plain
/// `crate::substitute_rigids`); the result is a plain
/// `Type::Fn` with no `Forall`,
/// 3. has params + body taken from the instance's matching
/// `InstanceMethod.body` if present and Lam-shaped; from the
+2 -2
View File
@@ -2,7 +2,7 @@
//!
//! This pass classifies every binder of every fn body as either
//! [`Uniqueness::Unique`] or [`Uniqueness::Shared`]. Unlike the
//! [`crate::linearity`] check it is **not** a user-facing diagnostic
//! `crate::linearity` check it is **not** a user-facing diagnostic
//! pass — it is internal codegen input. Codegen consults the side
//! table to decide where to emit `ailang_rc_inc` and `ailang_rc_dec`
//! calls under `--alloc=rc` (Iter 18c.3).
@@ -39,7 +39,7 @@
//!
//! ## Position model
//!
//! Identical to [`crate::linearity`]'s, modulo that this pass has no
//! Identical to `crate::linearity`'s, modulo that this pass has no
//! activation gate (it runs on every fn). See that module's docs for
//! the propagation rules. `Term::Lam` captures are conservatively
//! treated as Consume — the same conservative decision the linearity
+1 -1
View File
@@ -79,7 +79,7 @@
//! desugar pass eliminates it before typecheck/codegen by **lifting**
//! the LetRec to a synthetic top-level fn in the same module. The
//! lifted name has the form `<hint>$lr_N`, fresh against both the
//! existing module-top-level def names and the [`Desugarer::used`] set.
//! existing module-top-level def names and the `Desugarer::used` set.
//! Inside the LetRec's `body` and `in_term`, every reference to the
//! original local name is rewritten via [`subst_var`] to the lifted
//! name. The lifted [`FnDef`] is appended to the module's `defs` so
+3 -3
View File
@@ -90,7 +90,7 @@ pub struct Registry {
/// defining module. Used by [`Self::normalize_type_for_lookup`] to
/// rewrite a bare `Type::Con.name` to its always-qualified form
/// before computing the registry key. Primitives are not present.
/// Populated in [`build_registry`] from the same scan that builds
/// Populated in `build_registry` from the same scan that builds
/// the entry map.
///
/// Keyed by `(owning_module, bare_name)`, value is the
@@ -99,7 +99,7 @@ pub struct Registry {
/// module M is `(M, "Foo")`, bare `Foo` from module N is
/// `(N, "Foo")`, and the two carry distinct canonical
/// qualifications under
/// [`normalize_type_for_registry`]. Pre-ctt.2 the key was
/// `normalize_type_for_registry`. Pre-ctt.2 the key was
/// the bare name alone, and a workspace with two `type Foo`
/// declarations silently overwrote one entry, then tripped
/// `DuplicateInstance` on the loser-side instance after
@@ -441,7 +441,7 @@ pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError
/// Load a workspace using a caller-supplied per-module loader.
///
/// The standard entry point [`load_workspace`] is a thin wrapper that
/// passes [`load_one`] as the loader and so only accepts `.ail.json`
/// passes `load_one` as the loader and so only accepts `.ail.json`
/// files. Pass a different loader (for example, an extension-dispatching
/// loader from `ailang-surface`) to accept `.ail` source files as well.
///
@@ -8,7 +8,11 @@
//! The exhaustive `match` per enum is the load-bearing mechanism: adding a
//! new variant without a matching arm fails compilation before the test runs.
//! Once the variant is matched, the test asserts the anchor is present in
//! DESIGN.md.
//! §"Data model" specifically — not anywhere in DESIGN.md. The narrow scope
//! is load-bearing: before this tightening, the test scanned the whole
//! document, so an anchor that appeared only in §"Decision 11" (or any
//! other discussion section) was treated as "present" even though
//! §"Data model" — the canonical schema reference — was missing it.
use ailang_core::ast::{
ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, InstanceDef,
@@ -17,6 +21,21 @@ use ailang_core::ast::{
const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md");
/// Slice of `DESIGN_MD` covering only §"Data model": from the `## Data model`
/// header to the next top-level `## ` header. Returned as a `&'static str`
/// because `DESIGN_MD` is itself static. Panics if §"Data model" is missing
/// — that itself would be drift.
fn data_model_section() -> &'static str {
let start = DESIGN_MD
.find("## Data model")
.expect("DESIGN.md must contain `## Data model` header");
let from_start = &DESIGN_MD[start..];
match from_start.find("\n## ") {
Some(end) => &from_start[..end],
None => from_start,
}
}
/// Every `Term` variant must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". An LLM author cannot produce a term variant
/// whose `"t"` tag is absent from the canonical schema document.
@@ -139,7 +158,7 @@ fn design_md_anchors_every_term_variant() {
Term::ReuseAs { .. } => "reuse-as",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Term variant — \
add it to docs/DESIGN.md"
);
@@ -169,7 +188,7 @@ fn design_md_anchors_every_pattern_variant() {
Pattern::Ctor { .. } => "ctor",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Pattern variant"
);
}
@@ -202,7 +221,7 @@ fn design_md_anchors_every_type_variant() {
Type::Forall { .. } => "forall",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Type variant"
);
}
@@ -231,7 +250,7 @@ fn design_md_anchors_every_literal_variant() {
Literal::Float { .. } => "float",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Literal variant"
);
}
@@ -302,7 +321,7 @@ fn design_md_anchors_every_def_kind() {
Def::Instance(_) => "instance",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Def kind"
);
}
@@ -326,7 +345,7 @@ fn design_md_anchors_every_parammode_variant() {
ParamMode::Borrow => "borrow",
};
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing anchor `{anchor}` for a ParamMode variant"
);
}
@@ -363,8 +382,35 @@ fn design_md_anchors_nested_struct_keys() {
for anchor in anchors {
assert!(
DESIGN_MD.contains(anchor),
data_model_section().contains(anchor),
"DESIGN.md §Data-model is missing nested-struct-key anchor `{anchor}`"
);
}
}
/// Pin the §"Data model" section extractor itself: it must return a
/// non-empty slice starting with `## Data model`, and it must NOT bleed
/// into the next top-level section (`## Pipeline` today). A bug in the
/// extractor would otherwise silently widen every drift test's scope
/// back toward full-document scanning — the failure mode this file's
/// tightening exists to prevent.
#[test]
fn data_model_section_is_bounded() {
let section = data_model_section();
assert!(
section.starts_with("## Data model"),
"section must start at the §Data model header; got first 40 bytes: {:?}",
&section[..40.min(section.len())]
);
assert!(
!section.contains("\n## Pipeline"),
"section must stop before the next top-level header (\\n## Pipeline); \
extractor regressed to whole-document scanning"
);
assert!(
section.len() > 1000,
"§Data model is the canonical schema reference; a slice shorter than \
1 KB suggests the extractor truncated; got {} bytes",
section.len()
);
}
+2 -2
View File
@@ -3,7 +3,7 @@
//! AILang has two file forms today: Form A (`.ail`, the LLM
//! authoring surface) and Form B (`.ail.json`, the canonical
//! JSON-AST). These loaders accept both: for `.ail` they read the
//! source text and parse it via [`crate::parse`]; for `.ail.json`
//! source text and parse it via [`crate::parse()`]; for `.ail.json`
//! they delegate to [`ailang_core::load_module`].
//!
//! These functions live in `ailang-surface` rather than
@@ -24,7 +24,7 @@ fn is_ail_source(path: &Path) -> bool {
///
/// Dispatches on file extension:
///
/// - `.ail` — read source text, parse via [`crate::parse`], return
/// - `.ail` — read source text, parse via [`crate::parse()`], return
/// the in-memory [`Module`].
/// - anything else — delegate to [`ailang_core::load_module`] and
/// convert its `Error` into the corresponding