Files
AILang/crates/ailang-core/tests/workspace_kernel.rs
T
Brummel 9339279181 iter prep.3-kernel-tier-modules (DONE 9/9): kernel-tier modules + param-in + stub crate — closes #33
Terminal iteration of the kernel-extension-mechanics milestone. Ships
the four language-level mechanisms named in the spec's § Goal:
Module.kernel + TypeDef.param-in schema, their Form-A surface,
flag-driven kernel-tier auto-injection, and generic param-in checker
enforcement with a new diagnostic.

Schema (Tasks 1+2). Module gains a `kernel: bool` field
(skip_serializing_if = is_false), TypeDef gains a
`param_in: BTreeMap<String, BTreeSet<String>>` field
(skip-if-empty, kebab-renamed to "param-in"). Both fields are
strictly additive — every pre-existing fixture's canonical-JSON hash
is bit-stable except `prelude.ail`, which intentionally gains
`(kernel)`. The struct-literal sweep covered ~104 Module sites and
~35 TypeDef sites across the workspace; the additive serde-default
covers JSON deserialise paths, only Rust struct literals broke.

Form-A surface (Tasks 3+4). `(kernel)` is a bare module-header
attribute; `(param-in (a Int Float) (b Str))` is one outer
TypeDef-body clause carrying one or more inner var-lists (OQ1
decision — mirrors `(ctors …)`, one parser arm, deterministic
BTreeMap iteration). Both round-trip Form-A → JSON → Form-A
bit-identical.

Workspace-load migration (Task 5). The hardcoded `&["prelude"]`
literal at loader.rs:108 became a `modules.values().filter(|m|
m.kernel)` derivation; `parse_prelude()` injection stays because
the prelude has no on-disk manifest in user workspaces. Prelude
now carries `(kernel)` in its source, so the new filter picks it
up automatically. Code-path migration only — observable behaviour
is identical (prelude_free_fns.rs stays green). prelude hash
re-pinned (af372f28c726f29f) with Honesty-Rule provenance comment.
WorkspaceLoadError::ReservedModuleName diagnostic prose
repurposed: any built-in kernel module name is reserved
(currently prelude + kernel_stub), not specifically prelude. CLI
mapping at main.rs updated in lockstep.

Stub crate (Task 6). New `crates/ailang-kernel-stub/` is a
zero-dependency leaf crate carrying only `pub const STUB_AIL:
&str` with the Form-A source of the kernel_stub module (one
parametric TypeDef with param-in, one ctor). The parse hop —
`parse_kernel_stub()` — lives in ailang-surface next to
parse_prelude, keeping the crate-dependency graph acyclic
(`ailang-surface → ailang-kernel-stub → ailang-core`, no
back-edge). The stub is injected unconditionally in all builds as
the ratifying fixture for the kernel-extension mechanism; future
base extensions may add more or retire the stub. Drift-pinned by
`kernel_stub_module_round_trips`.

Checker (Task 7). New `CheckError::ParamNotInRestrictedSet`
variant + code() + ctx() arms + enforcement in
`check_type_well_formed`'s Type::Con arm — generic, data-driven
from the TypeDef, mentions no specific extension type. Two
in-source tests pin both the rejection (`Str` outside `{Int,
Float}`) and the acceptance (`Int` inside) paths.

Workspace-load integration tests (Task 8). New
`workspace_kernel.rs` integration-test crate with three tests:
auto-import without explicit `(import …)` declaration, two
kernel-tier modules co-load, explicit-import-overrides-auto-
import precedence preserved. Loader is import-tree-only so the
auto-import tests use a bridge module that brings the kernel
module into the workspace via the import graph — docstring
captures the reachability nuance for future readers.

Doc-state transitions (Task 9). INDEX.md kernel-extensions row
annotation transitions from "design accepted 2026-05-28; impl in
progress" to "mechanisms milestone closed 2026-05-28; raw-buf and
series milestones pending". Whitepaper STATUS + auto-import +
param-in sections transitioned forward→present for shipped
mechanisms; forward-tense survives only in sections describing
the still-pending raw-buf/series milestones (per Honesty-Rule).
data-model contract gains anchor blocks for both new schema
fields.

Side-effect: every binary's IR snapshot now contains ~52 lines
for `drop_kernel_stub_StubT` because the stub is auto-injected
into every workspace load. Snapshots refreshed; e2e expects 4
modules per workspace (prelude + kernel_stub + entry + zero or
more user modules) instead of the previous 3.

Plan defects scrubbed in the implementation (folded back into
the planner template via the planner's self-review checklist
next time): Task 4 sample test src used fictional
`(ctors (MkT a))` list form (project grammar is per-`(ctor MkT
a)`); Task 6 original wiring would have created a cycle
ailang-surface → ailang-kernel-stub → ailang-surface (inverted —
stub crate is zero-dep, parse hop lives in surface); Task 7 in-
source tests referenced a fictional `check_type_in_module`
helper (used the existing Workspace + check_workspace
convention); Task 8 first integration test expected loader to
auto-load kernel modules from disk (loader is import-tree-only;
tests use a bridge module).

Concern-5 fix folded in pre-commit: workspace.rs ReservedModuleName
doc-prose initially said "in test/dev builds" for kernel_stub —
but stub is unconditionally injected in all builds. Doc copy
tightened to present-state per Honesty-Rule.

Stats: 0 spec-review-loops, 0 quality-review-loops, 2 sweep-script
retries on Task 2 (brace-depth bug on nested vec![Ctor{…}],
recovered via per-file checkout + rewritten anchor-on-existing-
field sweep), 1 e2e-snapshot refresh on Task 6.
2026-05-28 18:43:42 +02:00

163 lines
5.9 KiB
Rust

//! prep.3 (kernel-extension-mechanics): workspace-load tests for
//! kernel-tier auto-import. These tests exercise the flag-driven
//! generalisation of the prior hardcoded `&["prelude"]` literal —
//! every workspace-loaded module with `kernel: true` joins the
//! implicit-imports list and therefore makes its types visible bare
//! to every consumer, even consumers that did not name it under
//! `(import …)`.
//!
//! Reachability: a user `(kernel)` module enters the workspace's
//! modules map only if some module in the import chain reaches it
//! (the standard import-graph DFS). The kernel-tier flag does NOT
//! cause unreachable `.ail` files in the entry's directory to be
//! loaded — see Concern in the iter prep.3 end-report. The
//! built-in `prelude` and `kernel_stub` are always loaded because
//! the loader injects them programmatically; user kernel-tier
//! modules need a path from the entry.
use ailang_surface::load_workspace;
use std::path::{Path, PathBuf};
/// Write a Form-A source file into the temp dir and return its path.
fn write_module(dir: &Path, name: &str, src: &str) -> PathBuf {
let path = dir.join(format!("{name}.ail"));
std::fs::write(&path, src).expect("write fixture");
path
}
/// prep.3: a workspace-loaded `(kernel)` module makes its types
/// visible bare to consumers that did NOT name it under
/// `(import …)`. Property: the kernel-flag filter derives the
/// implicit-imports list from every `kernel: true` module in the
/// modules map — `consumer` references `KT` bare and validates
/// because `bridge` brought `k_mod` into the workspace.
#[test]
fn kernel_tier_module_auto_imports_without_explicit_import() {
let dir = tempfile::tempdir().expect("tempdir");
// k_mod carries (kernel) and declares KT.
write_module(
dir.path(),
"k_mod",
"(module k_mod\n (kernel)\n (data KT (vars a) (ctor KCtor a)))\n",
);
// bridge imports k_mod so it enters the workspace.
write_module(
dir.path(),
"bridge",
concat!(
"(module bridge\n",
" (import k_mod)\n",
" (fn anchor\n",
" (type (forall (vars a) (fn-type (params (con KT a)) (ret (con Unit)))))\n",
" (params k) (body unit)))\n",
),
);
// The entry imports bridge (transitively pulling k_mod), and
// uses `(con KT (var a))` bare — no explicit import of k_mod.
let entry = write_module(
dir.path(),
"consumer",
concat!(
"(module consumer\n",
" (import bridge)\n",
" (fn use_kt\n",
" (type (forall (vars a) (fn-type (params (con KT a)) (ret (con Unit)))))\n",
" (params k) (body unit)))\n",
),
);
let ws = load_workspace(&entry).expect("workspace loads (kernel-tier auto-import)");
assert!(
ws.modules.contains_key("k_mod"),
"k_mod was loaded transitively via bridge"
);
assert!(
ws.modules.get("k_mod").unwrap().kernel,
"k_mod is marked kernel-tier"
);
}
/// prep.3: two `(kernel)` modules co-load without conflict; both
/// enter the implicit-imports list and both are visible to a
/// consumer that imported neither directly. Property: the filter
/// `modules.values().filter(|m| m.kernel)` walks ALL modules (not
/// just the first match), so every kernel-tier module gets
/// auto-imported.
#[test]
fn two_kernel_tier_modules_coload() {
let dir = tempfile::tempdir().expect("tempdir");
write_module(
dir.path(),
"k_a",
"(module k_a\n (kernel)\n (data A (ctor AC)))\n",
);
write_module(
dir.path(),
"k_b",
"(module k_b\n (kernel)\n (data B (ctor BC)))\n",
);
// bridge imports both kernel modules; consumer transitively
// sees both via the kernel-flag filter without explicit import.
write_module(
dir.path(),
"bridge",
concat!(
"(module bridge\n",
" (import k_a)\n",
" (import k_b)\n",
" (fn anchor\n",
" (type (fn-type (params (con A) (con B)) (ret (con Unit))))\n",
" (params a b) (body unit)))\n",
),
);
let entry = write_module(
dir.path(),
"consumer",
concat!(
"(module consumer\n",
" (import bridge)\n",
" (fn use_ab\n",
" (type (fn-type (params (con A) (con B)) (ret (con Unit))))\n",
" (params a b) (body unit)))\n",
),
);
let ws = load_workspace(&entry).expect("workspace with two kernel modules");
assert!(ws.modules.contains_key("k_a"));
assert!(ws.modules.contains_key("k_b"));
assert!(ws.modules.get("k_a").unwrap().kernel);
assert!(ws.modules.get("k_b").unwrap().kernel);
}
/// prep.3: an explicit `(import …)` for a kernel-tier module
/// coexists with the auto-import — no double-resolution error, no
/// behaviour drift. Property: the implicit-imports list and the
/// explicit-imports list are both consulted by the validator;
/// having the same name in both does not produce a duplicate-
/// import diagnostic.
#[test]
fn explicit_import_takes_precedence_over_auto_import() {
let dir = tempfile::tempdir().expect("tempdir");
write_module(
dir.path(),
"k_mod",
"(module k_mod\n (kernel)\n (data KT (ctor KC)))\n",
);
// consumer explicitly imports k_mod AND uses KT bare.
let entry = write_module(
dir.path(),
"consumer",
concat!(
"(module consumer\n",
" (import k_mod)\n",
" (fn use_kt\n",
" (type (fn-type (params (con KT)) (ret (con Unit))))\n",
" (params k) (body unit)))\n",
),
);
let ws = load_workspace(&entry).expect("explicit + auto coexist");
assert!(ws.modules.contains_key("k_mod"));
assert!(ws.modules.get("k_mod").unwrap().kernel);
}