9339279181
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.
154 lines
6.2 KiB
Rust
154 lines
6.2 KiB
Rust
//! Extension-dispatching loader for AILang source files.
|
|
//!
|
|
//! 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`
|
|
//! they delegate to [`ailang_core::load_module`].
|
|
//!
|
|
//! These functions live in `ailang-surface` rather than
|
|
//! `ailang-core` because `ailang-core` cannot import `ailang-surface`
|
|
//! without creating a circular crate dependency. The cross-crate
|
|
//! injection point is [`ailang_core::workspace::load_modules_with`]
|
|
//! (DFS-only loader) composed with [`ailang_core::workspace::build_workspace`]
|
|
//! (validation + registry). Surface owns the prelude inject step
|
|
//! between the two; the implicit-imports list `&["prelude"]` is
|
|
//! supplied at the surface call site. Introduced in iter pd.2 (post-
|
|
//! ext-cli.1's `load_workspace_with` shim retirement).
|
|
|
|
use ailang_core::ast::Module;
|
|
use ailang_core::workspace::{Workspace, WorkspaceLoadError};
|
|
use std::path::Path;
|
|
|
|
/// pd.2 (`prelude-decouple` milestone): the prelude bytes are embedded
|
|
/// here at compile time. The single source of truth is
|
|
/// `examples/prelude.ail` (Form A). Pre-pd.2, the embed lived in
|
|
/// `ailang-core` and used `prelude.ail.json`; that path retired with
|
|
/// the loader-split.
|
|
pub const PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail");
|
|
|
|
/// pd.2: parse the embedded prelude bytes into a `Module`.
|
|
///
|
|
/// Panics on parse failure — the prelude is build-time-validated by
|
|
/// every test run, so a parse failure here is a build-correctness
|
|
/// bug, not a runtime concern.
|
|
///
|
|
/// Mirrors the semantics of the retired `ailang_core::workspace::load_prelude`
|
|
/// (which deserialised `prelude.ail.json` via `serde_json::from_str`);
|
|
/// equivalence between the two parse paths is pinned by
|
|
/// `crates/ailang-surface/tests/prelude_module_hash_pin.rs`'s
|
|
/// cross-form-identity preflight.
|
|
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")
|
|
}
|
|
|
|
/// Load a single module from either `.ail` or `.ail.json`.
|
|
///
|
|
/// Dispatches on file extension:
|
|
///
|
|
/// - `.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
|
|
/// [`WorkspaceLoadError`] variant.
|
|
///
|
|
/// Errors are returned as [`WorkspaceLoadError`] so that single-
|
|
/// module callers and workspace callers can share the same error
|
|
/// type (and `workspace_error_to_diagnostic` in the binary can
|
|
/// route them through one channel).
|
|
pub fn load_module(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
|
if is_ail_source(path) {
|
|
let src = std::fs::read_to_string(path).map_err(|e| WorkspaceLoadError::Io {
|
|
path: path.to_path_buf(),
|
|
source: e,
|
|
})?;
|
|
crate::parse(&src).map_err(|e| WorkspaceLoadError::SurfaceParse {
|
|
path: path.to_path_buf(),
|
|
message: format!("{e}"),
|
|
})
|
|
} else {
|
|
match ailang_core::load_module(path) {
|
|
Ok(m) => Ok(m),
|
|
Err(ailang_core::Error::Io(e)) => Err(WorkspaceLoadError::Io {
|
|
path: path.to_path_buf(),
|
|
source: e,
|
|
}),
|
|
Err(e) => Err(WorkspaceLoadError::Schema {
|
|
path: path.to_path_buf(),
|
|
source: e,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load a workspace from an entry path, accepting either `.ail` or
|
|
/// `.ail.json` for the entry and for every transitive import.
|
|
///
|
|
/// Import resolution prefers a sibling `<module>.ail` over
|
|
/// `<module>.ail.json`; this is the new precedence rule baked into
|
|
/// `core::workspace::visit`. Mixed-extension workspaces (entry is
|
|
/// `.ail`, some imports are `.ail.json`) are valid.
|
|
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,
|
|
&implicit_imports,
|
|
)
|
|
}
|