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.
This commit is contained in:
@@ -36,6 +36,6 @@ pub mod loader;
|
||||
pub mod parse;
|
||||
pub mod print;
|
||||
|
||||
pub use loader::{load_module, load_workspace, parse_prelude, PRELUDE_AIL};
|
||||
pub use loader::{load_module, load_workspace, parse_kernel_stub, parse_prelude, PRELUDE_AIL};
|
||||
pub use parse::{parse, parse_term, ParseError};
|
||||
pub use print::{print, term_to_form_a, type_to_form_a};
|
||||
|
||||
@@ -42,6 +42,20 @@ pub fn parse_prelude() -> Module {
|
||||
crate::parse(PRELUDE_AIL).expect("examples/prelude.ail must parse as a Module")
|
||||
}
|
||||
|
||||
/// prep.3 (kernel-extension-mechanics): parse the embedded
|
||||
/// kernel-stub bytes into a `Module`. Mirror of [`parse_prelude`].
|
||||
///
|
||||
/// Source-of-truth: `ailang_kernel_stub::STUB_AIL`. The stub
|
||||
/// ratifies the end-to-end kernel-tier path: `Module.kernel`,
|
||||
/// `TypeDef.param-in`, and auto-import without `(import …)`.
|
||||
///
|
||||
/// Panics on parse failure — the stub is build-time-validated by
|
||||
/// every drift test run.
|
||||
pub fn parse_kernel_stub() -> Module {
|
||||
crate::parse(ailang_kernel_stub::STUB_AIL)
|
||||
.expect("ailang_kernel_stub::STUB_AIL must parse as a Module")
|
||||
}
|
||||
|
||||
fn is_ail_source(path: &Path) -> bool {
|
||||
path.extension().and_then(|s| s.to_str()) == Some("ail")
|
||||
}
|
||||
@@ -95,16 +109,45 @@ pub fn load_module(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
||||
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
||||
let (entry_name, root_dir, mut modules) =
|
||||
ailang_core::workspace::load_modules_with(entry, load_module)?;
|
||||
|
||||
// parse_prelude() injects the built-in prelude into the
|
||||
// workspace. The prelude module's source carries `(kernel)`, so
|
||||
// it surfaces in the kernel-tier auto-import set below.
|
||||
if modules.contains_key("prelude") {
|
||||
return Err(WorkspaceLoadError::ReservedModuleName {
|
||||
name: "prelude".to_string(),
|
||||
});
|
||||
}
|
||||
modules.insert("prelude".to_string(), parse_prelude());
|
||||
|
||||
// parse_kernel_stub() injects the ratifying stub kernel module —
|
||||
// exercises Module.kernel + TypeDef.param-in end-to-end. The
|
||||
// kernel-flag filter below picks it up automatically because its
|
||||
// source carries `(kernel)`.
|
||||
if modules.contains_key("kernel_stub") {
|
||||
return Err(WorkspaceLoadError::ReservedModuleName {
|
||||
name: "kernel_stub".to_string(),
|
||||
});
|
||||
}
|
||||
modules.insert("kernel_stub".to_string(), parse_kernel_stub());
|
||||
|
||||
// Derive the implicit-imports list from `kernel: true` modules.
|
||||
// Replaces the previous hardcoded `&["prelude"]` literal: any
|
||||
// workspace-loaded module that carries the kernel flag is now
|
||||
// auto-imported. See prep.3 of the kernel-extension-mechanics
|
||||
// milestone.
|
||||
let kernel_names: Vec<String> = modules
|
||||
.values()
|
||||
.filter(|m| m.kernel)
|
||||
.map(|m| m.name.clone())
|
||||
.collect();
|
||||
let implicit_imports: Vec<&str> =
|
||||
kernel_names.iter().map(String::as_str).collect();
|
||||
|
||||
ailang_core::workspace::build_workspace(
|
||||
entry_name,
|
||||
root_dir,
|
||||
modules,
|
||||
&["prelude"],
|
||||
&implicit_imports,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
//! - The `import` form admits an optional `as` alias to round-trip
|
||||
//! [`ailang_core::ast::Import::alias`].
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ailang_core::ast::{
|
||||
Arm, ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
|
||||
InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
||||
@@ -284,6 +286,7 @@ impl<'a> Parser<'a> {
|
||||
self.expect_lparen("module")?;
|
||||
self.expect_keyword("module")?;
|
||||
let name = self.expect_ident("module name")?;
|
||||
let mut kernel = false;
|
||||
let mut imports: Vec<Import> = Vec::new();
|
||||
let mut defs: Vec<Def> = Vec::new();
|
||||
loop {
|
||||
@@ -307,6 +310,32 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
})?;
|
||||
match head {
|
||||
"kernel" => {
|
||||
// `(kernel)` opt-in attribute — bare flag, no inner content.
|
||||
// A second `(kernel)` clause is a parse error so the
|
||||
// JSON schema's `kernel: bool` round-trips unambiguously.
|
||||
self.expect_lparen("kernel-attr")?;
|
||||
self.expect_keyword("kernel")?;
|
||||
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: "module",
|
||||
message: "kernel takes no arguments; expected `)`"
|
||||
.into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
self.expect_rparen("kernel-attr")?;
|
||||
if kernel {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production: "module",
|
||||
message: "duplicate `kernel` attribute".into(),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
kernel = true;
|
||||
}
|
||||
"import" => imports.push(self.parse_import()?),
|
||||
"data" => defs.push(Def::Type(self.parse_data()?)),
|
||||
"fn" => defs.push(Def::Fn(self.parse_fn()?)),
|
||||
@@ -318,7 +347,7 @@ impl<'a> Parser<'a> {
|
||||
return Err(ParseError::Production {
|
||||
production: "module",
|
||||
message: format!(
|
||||
"unknown def head `{other}`; expected `data`, `fn`, `const`, `class`, `instance`, or `import`"
|
||||
"unknown def head `{other}`; expected `data`, `fn`, `const`, `class`, `instance`, `import`, or `kernel`"
|
||||
),
|
||||
pos,
|
||||
});
|
||||
@@ -329,6 +358,7 @@ impl<'a> Parser<'a> {
|
||||
Ok(Module {
|
||||
schema: SCHEMA.to_string(),
|
||||
name,
|
||||
kernel,
|
||||
imports,
|
||||
defs,
|
||||
})
|
||||
@@ -371,6 +401,7 @@ impl<'a> Parser<'a> {
|
||||
let mut doc: Option<String> = None;
|
||||
let mut ctors: Vec<Ctor> = Vec::new();
|
||||
let mut drop_iterative = false;
|
||||
let mut param_in: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
loop {
|
||||
match self.peek_head_ident() {
|
||||
Some("doc") => {
|
||||
@@ -382,6 +413,44 @@ impl<'a> Parser<'a> {
|
||||
Some("ctor") => {
|
||||
ctors.push(self.parse_ctor()?);
|
||||
}
|
||||
Some("param-in") => {
|
||||
// `(param-in (a Int Float) (b Str) ...)` — one
|
||||
// outer clause carrying one or more inner
|
||||
// var-lists. Mirrors the `(ctors ...)` nested
|
||||
// shape (one outer attribute, list-of-inner).
|
||||
// A duplicate inner var binding (`(a ...) (a ...)`)
|
||||
// is a parse error so the BTreeMap round-trips
|
||||
// unambiguously.
|
||||
if !param_in.is_empty() {
|
||||
return Err(self.duplicate_clause_err(
|
||||
"data-def",
|
||||
&format!("data `{name}`"),
|
||||
"param-in",
|
||||
));
|
||||
}
|
||||
self.expect_lparen("param-in-attr")?;
|
||||
self.expect_keyword("param-in")?;
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
self.expect_lparen("param-in-inner-var-list")?;
|
||||
let var = self.expect_ident("type-variable name")?;
|
||||
let mut allowed: BTreeSet<String> = BTreeSet::new();
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
allowed.insert(self.expect_ident("allowed type-name")?);
|
||||
}
|
||||
self.expect_rparen("param-in-inner-var-list")?;
|
||||
if param_in.insert(var.clone(), allowed).is_some() {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production: "data-def",
|
||||
message: format!(
|
||||
"param-in: duplicate var binding `{var}`"
|
||||
),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.expect_rparen("param-in-attr")?;
|
||||
}
|
||||
Some("drop-iterative") => {
|
||||
// `(drop-iterative)` opt-in annotation.
|
||||
// Takes no arguments — it is a flag. A second
|
||||
@@ -417,7 +486,7 @@ impl<'a> Parser<'a> {
|
||||
return Err(ParseError::Production {
|
||||
production: "data-def",
|
||||
message: format!(
|
||||
"unknown data attribute `{other}`; expected `doc`, `ctor`, or `drop-iterative`"
|
||||
"unknown data attribute `{other}`; expected `doc`, `ctor`, `drop-iterative`, or `param-in`"
|
||||
),
|
||||
pos,
|
||||
});
|
||||
@@ -432,6 +501,7 @@ impl<'a> Parser<'a> {
|
||||
ctors,
|
||||
doc,
|
||||
drop_iterative,
|
||||
param_in,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ pub fn print(module: &Module) -> String {
|
||||
out.push('(');
|
||||
out.push_str("module ");
|
||||
out.push_str(&module.name);
|
||||
if module.kernel {
|
||||
out.push('\n');
|
||||
indent(&mut out, 1);
|
||||
out.push_str("(kernel)");
|
||||
}
|
||||
for imp in &module.imports {
|
||||
out.push('\n');
|
||||
write_import(&mut out, imp, 1);
|
||||
@@ -131,6 +136,25 @@ fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
|
||||
indent(out, level + 1);
|
||||
out.push_str("(drop-iterative)");
|
||||
}
|
||||
// `(param-in (a Int Float) (b Str) ...)` — BTreeMap/BTreeSet
|
||||
// give deterministic alphabetical iteration order. Omitted when
|
||||
// empty so the canonical Form-A bytes are bit-stable for every
|
||||
// TypeDef that does not restrict.
|
||||
if !td.param_in.is_empty() {
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
out.push_str("(param-in");
|
||||
for (var, allowed) in &td.param_in {
|
||||
out.push_str(" (");
|
||||
out.push_str(var);
|
||||
for tname in allowed {
|
||||
out.push(' ');
|
||||
out.push_str(tname);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
|
||||
@@ -724,6 +748,7 @@ mod tests {
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "M".into(),
|
||||
kernel: false,
|
||||
imports: vec![],
|
||||
defs: vec![Def::Class(ClassDef {
|
||||
name: "Foo".into(),
|
||||
@@ -751,6 +776,7 @@ mod tests {
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "M".into(),
|
||||
kernel: false,
|
||||
imports: vec![],
|
||||
defs: vec![Def::Class(ClassDef {
|
||||
name: "Bar".into(),
|
||||
@@ -781,6 +807,7 @@ mod tests {
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "M".into(),
|
||||
kernel: false,
|
||||
imports: vec![],
|
||||
defs: vec![Def::Fn(ailang_core::ast::FnDef {
|
||||
name: "f".into(),
|
||||
@@ -816,6 +843,7 @@ mod tests {
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "M".into(),
|
||||
kernel: false,
|
||||
imports: vec![],
|
||||
defs: vec![Def::Instance(InstanceDef {
|
||||
class: "Foo".into(),
|
||||
@@ -854,6 +882,7 @@ mod tests {
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "M".into(),
|
||||
kernel: false,
|
||||
imports: vec![],
|
||||
defs: vec![Def::Const(ConstDef {
|
||||
name: "k".into(),
|
||||
|
||||
Reference in New Issue
Block a user