|
|
|
@@ -5,11 +5,15 @@
|
|
|
|
|
//! the entry file's directory as `<root_dir>/foo.ail.json`. Module
|
|
|
|
|
//! names must match the file stem (the loader rejects mismatches).
|
|
|
|
|
//!
|
|
|
|
|
//! The single entry point is [`load_workspace`]; it returns a fully
|
|
|
|
|
//! The pd.1 single core entry point is [`load_workspace_with`]; it
|
|
|
|
|
//! takes a caller-supplied per-module loader so surface (`.ail`) and
|
|
|
|
|
//! JSON (`.ail.json`) callers share one pipeline. It returns a fully
|
|
|
|
|
//! populated [`Workspace`] or a structured [`WorkspaceLoadError`].
|
|
|
|
|
//! [`module_hash`] is the module-granularity counterpart to
|
|
|
|
|
//! [`crate::def_hash`] and is used both internally (to detect a module
|
|
|
|
|
//! re-loaded from disk with different content) and by the CLI.
|
|
|
|
|
//! pd.2 will move the public entry point into `ailang-surface` and
|
|
|
|
|
//! retire this shim. [`module_hash`] is the module-granularity
|
|
|
|
|
//! counterpart to [`crate::def_hash`] and is used both internally (to
|
|
|
|
|
//! detect a module re-loaded from disk with different content) and by
|
|
|
|
|
//! the CLI.
|
|
|
|
|
//!
|
|
|
|
|
//! This module is responsible only for **finding** and consistently
|
|
|
|
|
//! **loading** all reachable modules. Cross-module typechecking lives
|
|
|
|
@@ -19,10 +23,15 @@
|
|
|
|
|
//! # Examples
|
|
|
|
|
//!
|
|
|
|
|
//! ```ignore
|
|
|
|
|
//! use ailang_core::load_workspace;
|
|
|
|
|
//! // pd.2 will move the public entry point to ailang-surface;
|
|
|
|
|
//! // pd.1 keeps `load_workspace_with` as the single core entry point.
|
|
|
|
|
//! // Callers supply a per-module loader (e.g.
|
|
|
|
|
//! // `ailang_surface::load_module_dispatching`) that knows how to
|
|
|
|
|
//! // read `.ail` and/or `.ail.json` files.
|
|
|
|
|
//! use ailang_core::workspace::load_workspace_with;
|
|
|
|
|
//! use std::path::Path;
|
|
|
|
|
//!
|
|
|
|
|
//! let ws = load_workspace(Path::new("examples/ws_main.ail.json"))?;
|
|
|
|
|
//! let ws = load_workspace_with(Path::new("examples/ws_main.ail.json"), my_loader)?;
|
|
|
|
|
//! assert_eq!(ws.entry, "ws_main");
|
|
|
|
|
//! // All transitively imported modules are now in `ws.modules`.
|
|
|
|
|
//! for (name, m) in &ws.modules {
|
|
|
|
@@ -33,7 +42,9 @@
|
|
|
|
|
|
|
|
|
|
use crate::ast::{ClassDef, Def, InstanceDef, Module, Term, Type};
|
|
|
|
|
use crate::canonical;
|
|
|
|
|
use crate::{load_module, Error as CoreError};
|
|
|
|
|
use crate::Error as CoreError;
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
use crate::load_module;
|
|
|
|
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
@@ -427,35 +438,24 @@ fn load_prelude() -> Module {
|
|
|
|
|
.expect("examples/prelude.ail.json must parse as a Module")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load entry module plus all transitively reachable modules.
|
|
|
|
|
/// pd.1: extract the DFS pre-amble of `load_workspace_with` into a public
|
|
|
|
|
/// loader-only fn. Returns `(entry_name, root_dir, modules)` with NO prelude
|
|
|
|
|
/// injected and NO validation run. The caller is responsible for injecting
|
|
|
|
|
/// any implicit modules (e.g. prelude) and then handing the result to
|
|
|
|
|
/// `build_workspace` for validation + registry construction.
|
|
|
|
|
///
|
|
|
|
|
/// Surface composes this with a prelude-injection step in pd.2; pd.1 keeps
|
|
|
|
|
/// the shim `load_workspace_with` callable so surface is unchanged.
|
|
|
|
|
///
|
|
|
|
|
/// Algorithm: DFS over `imports`, with two sets:
|
|
|
|
|
/// - `loaded` (= `modules` map): modules whose subtree is already fully
|
|
|
|
|
/// processed. On a re-hit only hash consistency is checked.
|
|
|
|
|
/// - `visiting`: stack of modules whose DFS descent is still running.
|
|
|
|
|
/// A hit here = cycle.
|
|
|
|
|
pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
|
|
|
|
load_workspace_with(entry_path, load_one)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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`
|
|
|
|
|
/// files. Pass a different loader (for example, an extension-dispatching
|
|
|
|
|
/// loader from `ailang-surface`) to accept `.ail` source files as well.
|
|
|
|
|
///
|
|
|
|
|
/// The loader is invoked for the entry module and for every transitive
|
|
|
|
|
/// import. It is responsible for reading bytes, parsing, and returning
|
|
|
|
|
/// the in-memory [`Module`].
|
|
|
|
|
///
|
|
|
|
|
/// ext-cli.1: introduced so `ailang-surface::load_workspace` can plug
|
|
|
|
|
/// in an extension-dispatching loader without `ailang-core` importing
|
|
|
|
|
/// `ailang-surface` (which would close a crate cycle).
|
|
|
|
|
pub fn load_workspace_with<F>(
|
|
|
|
|
pub fn load_modules_with<F>(
|
|
|
|
|
entry_path: &Path,
|
|
|
|
|
loader: F,
|
|
|
|
|
) -> Result<Workspace, WorkspaceLoadError>
|
|
|
|
|
) -> Result<(String, PathBuf, BTreeMap<String, Module>), WorkspaceLoadError>
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy,
|
|
|
|
|
{
|
|
|
|
@@ -465,7 +465,6 @@ where
|
|
|
|
|
.map(Path::to_path_buf)
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("."));
|
|
|
|
|
|
|
|
|
|
// Load entry module + check name<->filename convention.
|
|
|
|
|
let entry_module = loader(&entry_path)?;
|
|
|
|
|
let expected_entry_name = module_name_from_path(&entry_path);
|
|
|
|
|
if entry_module.name != expected_entry_name {
|
|
|
|
@@ -490,34 +489,29 @@ where
|
|
|
|
|
loader,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
// Iter 23.1: inject the prelude module. It is implicitly
|
|
|
|
|
// part of every workspace, so the user's import graph
|
|
|
|
|
// does not name it. A user module that happens to be named
|
|
|
|
|
// "prelude" would collide here; reject explicitly so the
|
|
|
|
|
// failure is informative rather than silent.
|
|
|
|
|
if modules.contains_key("prelude") {
|
|
|
|
|
return Err(WorkspaceLoadError::ReservedModuleName {
|
|
|
|
|
name: "prelude".to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let prelude = load_prelude();
|
|
|
|
|
modules.insert("prelude".to_string(), prelude);
|
|
|
|
|
Ok((entry_name, root_dir, modules))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ct.1: enforce the canonical-form rule on Type::Con and
|
|
|
|
|
// Term::Ctor.type_name. Runs before class-schema validation
|
|
|
|
|
// so a stale bare cross-module ref fires the canonical-form
|
|
|
|
|
// diagnostic instead of a downstream one.
|
|
|
|
|
validate_canonical_type_names(&modules)?;
|
|
|
|
|
|
|
|
|
|
// Iter 22b.2: class-schema validation runs before registry
|
|
|
|
|
// construction so that ill-kinded class definitions are rejected
|
|
|
|
|
// before any instance against them is registered.
|
|
|
|
|
/// pd.1: extract the validation + registry-construction tail of
|
|
|
|
|
/// `load_workspace_with` into a public fn that operates on a pre-assembled
|
|
|
|
|
/// modules map. The caller (typically `surface::load_workspace`, post-pd.2)
|
|
|
|
|
/// is responsible for injecting any implicit modules (e.g. prelude) into
|
|
|
|
|
/// `modules` BEFORE calling this fn, AND for naming those implicit modules
|
|
|
|
|
/// in `implicit_imports` so the diagnostic helpers (`check_class_ref`,
|
|
|
|
|
/// `check_type_con_name`) include them as fallback candidates.
|
|
|
|
|
///
|
|
|
|
|
/// Runs the three-stage pipeline:
|
|
|
|
|
/// 1. `validate_canonical_type_names` (with `implicit_imports`)
|
|
|
|
|
/// 2. `validate_classdefs` (no `implicit_imports` — does not consume it)
|
|
|
|
|
/// 3. `build_registry`
|
|
|
|
|
pub fn build_workspace(
|
|
|
|
|
entry_name: String,
|
|
|
|
|
root_dir: PathBuf,
|
|
|
|
|
modules: BTreeMap<String, Module>,
|
|
|
|
|
implicit_imports: &[&str],
|
|
|
|
|
) -> Result<Workspace, WorkspaceLoadError> {
|
|
|
|
|
validate_canonical_type_names(&modules, implicit_imports)?;
|
|
|
|
|
validate_classdefs(&modules)?;
|
|
|
|
|
|
|
|
|
|
// Iter 22b.1: build the workspace-global instance registry. Three
|
|
|
|
|
// coherence checks fire here (Orphan / Duplicate / Missing-method);
|
|
|
|
|
// any violation is surfaced as a `WorkspaceLoadError`, not as a
|
|
|
|
|
// per-call-site diagnostic.
|
|
|
|
|
let registry = build_registry(&modules)?;
|
|
|
|
|
|
|
|
|
|
Ok(Workspace {
|
|
|
|
@@ -528,6 +522,44 @@ where
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// pd.1: thin compatibility shim composed of `load_modules_with` +
|
|
|
|
|
/// `build_workspace` plus the prelude inject. Preserves today's
|
|
|
|
|
/// `load_workspace_with` semantics so surface (`crates/ailang-surface/src/loader.rs`)
|
|
|
|
|
/// stays unchanged in pd.1. pd.2 will rewire surface to call
|
|
|
|
|
/// `load_modules_with` and `build_workspace` directly and retire this
|
|
|
|
|
/// shim along with the embedded `PRELUDE_JSON` constant.
|
|
|
|
|
///
|
|
|
|
|
/// Algorithm: DFS over `imports` via `load_modules_with`, then inject
|
|
|
|
|
/// the prelude (rejecting collision with a user module of the same
|
|
|
|
|
/// name), then validate + build the typeclass registry via
|
|
|
|
|
/// `build_workspace`.
|
|
|
|
|
///
|
|
|
|
|
/// ext-cli.1: a caller-supplied loader is what allows
|
|
|
|
|
/// `ailang-surface::load_workspace` to plug in an extension-dispatching
|
|
|
|
|
/// loader without `ailang-core` importing `ailang-surface` (which would
|
|
|
|
|
/// close a crate cycle).
|
|
|
|
|
pub fn load_workspace_with<F>(
|
|
|
|
|
entry_path: &Path,
|
|
|
|
|
loader: F,
|
|
|
|
|
) -> Result<Workspace, WorkspaceLoadError>
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy,
|
|
|
|
|
{
|
|
|
|
|
let (entry_name, root_dir, mut modules) = load_modules_with(entry_path, loader)?;
|
|
|
|
|
|
|
|
|
|
// Iter 23.1 (preserved through the shim): the prelude module is
|
|
|
|
|
// implicitly part of every workspace. A user module of the same name
|
|
|
|
|
// would shadow; reject explicitly.
|
|
|
|
|
if modules.contains_key("prelude") {
|
|
|
|
|
return Err(WorkspaceLoadError::ReservedModuleName {
|
|
|
|
|
name: "prelude".to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
modules.insert("prelude".to_string(), load_prelude());
|
|
|
|
|
|
|
|
|
|
build_workspace(entry_name, root_dir, modules, &["prelude"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// mq.1: take a class-ref field value (`InstanceDef.class`,
|
|
|
|
|
/// `SuperclassRef.class`, `Constraint.class`) and a defining context,
|
|
|
|
|
/// and produce the qualified workspace key. Bare ⇒ prepend the
|
|
|
|
@@ -937,6 +969,7 @@ fn normalize_type_for_registry(
|
|
|
|
|
/// forms found by scanning the owning module's imports.
|
|
|
|
|
pub(crate) fn validate_canonical_type_names(
|
|
|
|
|
modules: &BTreeMap<String, Module>,
|
|
|
|
|
implicit_imports: &[&str],
|
|
|
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
|
|
|
// Pre-pass: for each module, build a `BTreeSet<String>` of local
|
|
|
|
|
// TypeDef names. Used for both the owning-module local lookup
|
|
|
|
@@ -979,14 +1012,14 @@ pub(crate) fn validate_canonical_type_names(
|
|
|
|
|
walk_def_types(def, &mut |t: &Type| {
|
|
|
|
|
if let Type::Con { name, .. } = t {
|
|
|
|
|
check_type_con_name(
|
|
|
|
|
name, mod_name, &local_types, &import_names,
|
|
|
|
|
name, mod_name, &local_types, &import_names, implicit_imports,
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
})?;
|
|
|
|
|
walk_def_terms(def, &mut |type_name: &str| {
|
|
|
|
|
check_type_con_name(
|
|
|
|
|
type_name, mod_name, &local_types, &import_names,
|
|
|
|
|
type_name, mod_name, &local_types, &import_names, implicit_imports,
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
check_class_name_fields(def, mod_name)?;
|
|
|
|
@@ -995,16 +1028,16 @@ pub(crate) fn validate_canonical_type_names(
|
|
|
|
|
// migrated class-ref fields.
|
|
|
|
|
match def {
|
|
|
|
|
Def::Instance(id) => {
|
|
|
|
|
check_class_ref(&id.class, mod_name, &local_classes, &import_names)?;
|
|
|
|
|
check_class_ref(&id.class, mod_name, &local_classes, &import_names, implicit_imports)?;
|
|
|
|
|
}
|
|
|
|
|
Def::Class(cd) => {
|
|
|
|
|
if let Some(sc) = &cd.superclass {
|
|
|
|
|
check_class_ref(&sc.class, mod_name, &local_classes, &import_names)?;
|
|
|
|
|
check_class_ref(&sc.class, mod_name, &local_classes, &import_names, implicit_imports)?;
|
|
|
|
|
}
|
|
|
|
|
for cmth in &cd.methods {
|
|
|
|
|
if let Type::Forall { constraints, .. } = &cmth.ty {
|
|
|
|
|
for c in constraints {
|
|
|
|
|
check_class_ref(&c.class, mod_name, &local_classes, &import_names)?;
|
|
|
|
|
check_class_ref(&c.class, mod_name, &local_classes, &import_names, implicit_imports)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1012,7 +1045,7 @@ pub(crate) fn validate_canonical_type_names(
|
|
|
|
|
Def::Fn(fd) => {
|
|
|
|
|
if let Type::Forall { constraints, .. } = &fd.ty {
|
|
|
|
|
for c in constraints {
|
|
|
|
|
check_class_ref(&c.class, mod_name, &local_classes, &import_names)?;
|
|
|
|
|
check_class_ref(&c.class, mod_name, &local_classes, &import_names, implicit_imports)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1043,6 +1076,7 @@ fn check_class_ref(
|
|
|
|
|
owning_module: &str,
|
|
|
|
|
local_classes: &BTreeMap<String, BTreeSet<String>>,
|
|
|
|
|
import_names: &[&str],
|
|
|
|
|
implicit_imports: &[&str],
|
|
|
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
|
|
|
if let Some((owner, bare)) = class_ref.split_once('.') {
|
|
|
|
|
// Rule 1: qualified.
|
|
|
|
@@ -1081,13 +1115,17 @@ fn check_class_ref(
|
|
|
|
|
candidates.push(format!("{imp}.{class_ref}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !import_names.contains(&"prelude")
|
|
|
|
|
&& local_classes
|
|
|
|
|
.get("prelude")
|
|
|
|
|
for implicit in implicit_imports {
|
|
|
|
|
if import_names.contains(implicit) {
|
|
|
|
|
continue; // already added above
|
|
|
|
|
}
|
|
|
|
|
if local_classes
|
|
|
|
|
.get(*implicit)
|
|
|
|
|
.map(|s| s.contains(class_ref))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
|
|
|
|
candidates.push(format!("prelude.{class_ref}"));
|
|
|
|
|
{
|
|
|
|
|
candidates.push(format!("{implicit}.{class_ref}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(WorkspaceLoadError::BareCrossModuleClassRef {
|
|
|
|
|
module: owning_module.to_string(),
|
|
|
|
@@ -1103,6 +1141,7 @@ fn check_type_con_name(
|
|
|
|
|
owning_module: &str,
|
|
|
|
|
local_types: &BTreeMap<String, BTreeSet<String>>,
|
|
|
|
|
import_names: &[&str],
|
|
|
|
|
implicit_imports: &[&str],
|
|
|
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
|
|
|
if let Some((prefix, suffix)) = name.split_once('.') {
|
|
|
|
|
// Rule 1: qualified.
|
|
|
|
@@ -1145,13 +1184,17 @@ fn check_type_con_name(
|
|
|
|
|
candidates.push(format!("{imp}.{name}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !import_names.contains(&"prelude")
|
|
|
|
|
&& local_types
|
|
|
|
|
.get("prelude")
|
|
|
|
|
for implicit in implicit_imports {
|
|
|
|
|
if import_names.contains(implicit) {
|
|
|
|
|
continue; // already added above
|
|
|
|
|
}
|
|
|
|
|
if local_types
|
|
|
|
|
.get(*implicit)
|
|
|
|
|
.map(|s| s.contains(name))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
|
|
|
|
candidates.push(format!("prelude.{name}"));
|
|
|
|
|
{
|
|
|
|
|
candidates.push(format!("{implicit}.{name}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(WorkspaceLoadError::BareCrossModuleTypeRef {
|
|
|
|
|
module: owning_module.to_string(),
|
|
|
|
@@ -1552,6 +1595,7 @@ where
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
fn load_one(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
|
|
|
|
match load_module(path) {
|
|
|
|
|
Ok(m) => Ok(m),
|
|
|
|
@@ -1628,7 +1672,7 @@ mod tests {
|
|
|
|
|
write_module(&dir, "prelude", &[]);
|
|
|
|
|
let entry = dir.join("prelude.ail.json");
|
|
|
|
|
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must reject user prelude");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must reject user prelude");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::ReservedModuleName { name } => {
|
|
|
|
|
assert_eq!(name, "prelude");
|
|
|
|
@@ -1646,7 +1690,7 @@ mod tests {
|
|
|
|
|
write_module(&dir, "b", &["a"]);
|
|
|
|
|
let entry = dir.join("a.ail.json");
|
|
|
|
|
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must error on cycle");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must error on cycle");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::Cycle { path } => {
|
|
|
|
|
assert!(path.contains(&"a".to_string()));
|
|
|
|
@@ -1662,7 +1706,7 @@ mod tests {
|
|
|
|
|
write_module(&dir, "main", &["does_not_exist"]);
|
|
|
|
|
let entry = dir.join("main.ail.json");
|
|
|
|
|
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must error on missing module");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must error on missing module");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::ModuleNotFound { name, expected_path } => {
|
|
|
|
|
assert_eq!(name, "does_not_exist");
|
|
|
|
@@ -1744,7 +1788,7 @@ mod tests {
|
|
|
|
|
let entry = std::path::PathBuf::from(
|
|
|
|
|
"../../examples/test_22b2_kind_mismatch.ail.json",
|
|
|
|
|
);
|
|
|
|
|
let err = load_workspace(&entry)
|
|
|
|
|
let err = load_workspace_with(&entry, load_one)
|
|
|
|
|
.expect_err("must fire canonical-form rejection");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, .. } => {
|
|
|
|
@@ -1767,7 +1811,7 @@ mod tests {
|
|
|
|
|
let entry = std::path::PathBuf::from(
|
|
|
|
|
"../../examples/test_22b2_invalid_superclass_param.ail.json",
|
|
|
|
|
);
|
|
|
|
|
let err = load_workspace(&entry)
|
|
|
|
|
let err = load_workspace_with(&entry, load_one)
|
|
|
|
|
.expect_err("must fire invalid-superclass-param");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::InvalidSuperclassParam {
|
|
|
|
@@ -1807,7 +1851,7 @@ mod tests {
|
|
|
|
|
let entry = std::path::PathBuf::from(
|
|
|
|
|
"../../examples/test_22b2_unbound_constraint_var.ail.json",
|
|
|
|
|
);
|
|
|
|
|
let err = load_workspace(&entry)
|
|
|
|
|
let err = load_workspace_with(&entry, load_one)
|
|
|
|
|
.expect_err("must fire constraint-references-unbound-type-var");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::UnboundConstraintTypeVar {
|
|
|
|
@@ -1910,9 +1954,9 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_validator_accepts_primitive_type_cons() {
|
|
|
|
|
let modules = single_module_with_type_con("m", "Int");
|
|
|
|
|
validate_canonical_type_names(&modules).expect("Int must be accepted");
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"]).expect("Int must be accepted");
|
|
|
|
|
let modules = single_module_with_type_con("m", "Float");
|
|
|
|
|
validate_canonical_type_names(&modules).expect("Float must be accepted");
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"]).expect("Float must be accepted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// ct.1: a bare Type::Con whose `name` matches a local TypeDef in
|
|
|
|
@@ -1921,7 +1965,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_validator_accepts_bare_local_type_con() {
|
|
|
|
|
let modules = single_module_with_local_type_and_ref("m", "Foo");
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("local Foo must be accepted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1931,7 +1975,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_validator_rejects_bare_xmod_no_imports() {
|
|
|
|
|
let modules = single_module_with_type_con("m", "Ordering");
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("Ordering must be rejected");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
|
|
|
@@ -1953,7 +1997,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), module_with_type_def("other", "Ordering"));
|
|
|
|
|
modules.insert("m".to_string(), module_with_import_and_type_con("m", "other", "Ordering"));
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("Ordering must be rejected with candidate");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { name, candidates, .. } => {
|
|
|
|
@@ -1971,7 +2015,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), module_with_type_def("other", "Ordering"));
|
|
|
|
|
modules.insert("m".to_string(), module_with_import_and_type_con("m", "other", "other.Ordering"));
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("other.Ordering must be accepted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1981,7 +2025,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_validator_rejects_bad_qualified_ref() {
|
|
|
|
|
let modules = single_module_with_type_con("m", "Mystery.Type");
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("Mystery.Type must be rejected");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BadCrossModuleTypeRef { module, name } => {
|
|
|
|
@@ -2020,7 +2064,7 @@ mod tests {
|
|
|
|
|
})).unwrap();
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("Lam-embedded Ordering must be rejected");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { name, .. } => {
|
|
|
|
@@ -2058,7 +2102,7 @@ mod tests {
|
|
|
|
|
}],
|
|
|
|
|
})).unwrap();
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("bare Term::Ctor type must be rejected");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
|
|
|
@@ -2088,7 +2132,7 @@ mod tests {
|
|
|
|
|
})).unwrap();
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
let err = validate_canonical_type_names(&modules)
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect_err("qualified ClassDef.name must be rejected");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::QualifiedClassName { module, name, field } => {
|
|
|
|
@@ -2132,7 +2176,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), other);
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("qualified InstanceDef.class is the canonical form post-mq.1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2168,7 +2212,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), other);
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("qualified SuperclassRef.class is the canonical form post-mq.1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2214,7 +2258,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), other);
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("qualified Constraint.class is the canonical form post-mq.1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2433,7 +2477,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("other".to_string(), other);
|
|
|
|
|
modules.insert("m".to_string(), m);
|
|
|
|
|
validate_canonical_type_names(&modules)
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"])
|
|
|
|
|
.expect("qualified Constraint.class in ClassDef-method Forall is the canonical form post-mq.1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2444,7 +2488,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_fixture_bare_xmod_rejected() {
|
|
|
|
|
let entry = examples_dir().join("test_ct1_bare_xmod_rejected.ail.json");
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must reject bare Ordering");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must reject bare Ordering");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
|
|
|
|
assert_eq!(module, "test_ct1_bare_xmod_rejected");
|
|
|
|
@@ -2460,7 +2504,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_fixture_bad_qualifier() {
|
|
|
|
|
let entry = examples_dir().join("test_ct1_bad_qualifier.ail.json");
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must reject Mystery.Type");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must reject Mystery.Type");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BadCrossModuleTypeRef { module, name } => {
|
|
|
|
|
assert_eq!(module, "test_ct1_bad_qualifier");
|
|
|
|
@@ -2483,7 +2527,7 @@ mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn ct1_fixture_qualified_class_orphan_post_mq1() {
|
|
|
|
|
let entry = examples_dir().join("test_ct1_qualified_class_rejected.ail.json");
|
|
|
|
|
let err = load_workspace(&entry).expect_err("must reject (now as Orphan)");
|
|
|
|
|
let err = load_workspace_with(&entry, load_one).expect_err("must reject (now as Orphan)");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::OrphanInstance {
|
|
|
|
|
class, type_repr, defining_module, ..
|
|
|
|
@@ -2608,7 +2652,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("A".to_string(), a);
|
|
|
|
|
modules.insert("B".to_string(), b);
|
|
|
|
|
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"]).expect_err("expected reject");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleClassRef { module, name, candidates } => {
|
|
|
|
|
assert_eq!(module, "B");
|
|
|
|
@@ -2662,7 +2706,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("A".to_string(), a);
|
|
|
|
|
modules.insert("B".to_string(), b);
|
|
|
|
|
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"]).expect_err("expected reject");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => {
|
|
|
|
|
assert_eq!(module, "B");
|
|
|
|
@@ -2703,7 +2747,7 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("A".to_string(), a);
|
|
|
|
|
modules.insert("B".to_string(), b);
|
|
|
|
|
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
|
|
|
|
|
let err = validate_canonical_type_names(&modules, &["prelude"]).expect_err("expected reject");
|
|
|
|
|
match err {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => {
|
|
|
|
|
assert_eq!(module, "B");
|
|
|
|
@@ -2744,6 +2788,152 @@ mod tests {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("A".to_string(), a);
|
|
|
|
|
modules.insert("B".to_string(), b);
|
|
|
|
|
validate_canonical_type_names(&modules).expect("qualified A.Show must be accepted");
|
|
|
|
|
validate_canonical_type_names(&modules, &["prelude"]).expect("qualified A.Show must be accepted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// pd.1 Task 1: `load_modules_with` returns modules without injecting
|
|
|
|
|
/// prelude. The prelude inject is the caller's responsibility (the
|
|
|
|
|
/// shim `load_workspace_with` does it in pd.1; surface owns it from
|
|
|
|
|
/// pd.2 onward).
|
|
|
|
|
#[test]
|
|
|
|
|
fn load_modules_with_returns_modules_without_prelude() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
write_module(dir.path(), "main", &[]);
|
|
|
|
|
let entry = dir.path().join("main.ail.json");
|
|
|
|
|
|
|
|
|
|
let (entry_name, root_dir, modules) =
|
|
|
|
|
load_modules_with(&entry, load_one).expect("load");
|
|
|
|
|
|
|
|
|
|
assert_eq!(entry_name, "main");
|
|
|
|
|
assert_eq!(root_dir, dir.path().to_path_buf());
|
|
|
|
|
assert!(
|
|
|
|
|
!modules.contains_key("prelude"),
|
|
|
|
|
"load_modules_with must not auto-inject prelude (that is build_workspace's caller's job)"
|
|
|
|
|
);
|
|
|
|
|
assert!(modules.contains_key("main"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// pd.1 Task 2: `build_workspace` accepts a pre-assembled modules
|
|
|
|
|
/// map (with the caller having already injected any implicit
|
|
|
|
|
/// modules) and runs the three-stage validation pipeline. The
|
|
|
|
|
/// `implicit_imports` slice is what the diagnostic helpers consult
|
|
|
|
|
/// for fallback candidate suggestions (Task 3 wires it through).
|
|
|
|
|
#[test]
|
|
|
|
|
fn build_workspace_accepts_assembled_modules_and_runs_validation() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
write_module(dir.path(), "main", &[]);
|
|
|
|
|
let entry = dir.path().join("main.ail.json");
|
|
|
|
|
|
|
|
|
|
let (entry_name, root_dir, mut modules) =
|
|
|
|
|
load_modules_with(&entry, load_one).expect("load");
|
|
|
|
|
|
|
|
|
|
// Caller-side prelude inject (mirrors what surface will do in pd.2).
|
|
|
|
|
let prelude = load_prelude();
|
|
|
|
|
modules.insert("prelude".to_string(), prelude);
|
|
|
|
|
|
|
|
|
|
let ws = build_workspace(entry_name, root_dir, modules, &["prelude"])
|
|
|
|
|
.expect("build");
|
|
|
|
|
|
|
|
|
|
assert_eq!(ws.entry, "main");
|
|
|
|
|
assert!(ws.modules.contains_key("main"));
|
|
|
|
|
assert!(ws.modules.contains_key("prelude"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn module_with_bare_classref(name: &str, class_ref: &str) -> Module {
|
|
|
|
|
// A module containing a single Def::Instance whose `class` field is
|
|
|
|
|
// a bare class reference. The instance's `type` field is a local
|
|
|
|
|
// ADT defined in the same module to keep ct.1 happy.
|
|
|
|
|
serde_json::from_value(serde_json::json!({
|
|
|
|
|
"schema": crate::SCHEMA,
|
|
|
|
|
"name": name,
|
|
|
|
|
"imports": [],
|
|
|
|
|
"defs": [
|
|
|
|
|
{ "kind": "type", "name": "Foo", "ctors": [] },
|
|
|
|
|
{
|
|
|
|
|
"kind": "instance",
|
|
|
|
|
"class": class_ref,
|
|
|
|
|
"type": { "k": "con", "name": "Foo" },
|
|
|
|
|
"methods": []
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
})).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn module_with_class_def(name: &str, class_name: &str, param: &str) -> Module {
|
|
|
|
|
serde_json::from_value(serde_json::json!({
|
|
|
|
|
"schema": crate::SCHEMA,
|
|
|
|
|
"name": name,
|
|
|
|
|
"imports": [],
|
|
|
|
|
"defs": [
|
|
|
|
|
{
|
|
|
|
|
"kind": "class",
|
|
|
|
|
"name": class_name,
|
|
|
|
|
"param": param,
|
|
|
|
|
"methods": []
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
})).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// pd.1 Task 3: `implicit_imports` drives whether the diagnostic
|
|
|
|
|
/// helpers offer fallback candidate suggestions for the named
|
|
|
|
|
/// implicit modules. With `&["prelude"]` the today-behaviour
|
|
|
|
|
/// `prelude.Eq` candidate fires; with `&[]` it does not.
|
|
|
|
|
#[test]
|
|
|
|
|
fn implicit_imports_arg_drives_prelude_fallback_in_diagnostics() {
|
|
|
|
|
let mut modules = BTreeMap::new();
|
|
|
|
|
modules.insert("main".to_string(), module_with_bare_classref("main", "Eq"));
|
|
|
|
|
modules.insert(
|
|
|
|
|
"prelude".to_string(),
|
|
|
|
|
module_with_class_def("prelude", "Eq", "a"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let err_with = validate_canonical_type_names(&modules, &["prelude"]).unwrap_err();
|
|
|
|
|
match err_with {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => {
|
|
|
|
|
assert!(
|
|
|
|
|
candidates.iter().any(|c| c == "prelude.Eq"),
|
|
|
|
|
"expected prelude.Eq in candidates, got {:?}", candidates
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let err_without = validate_canonical_type_names(&modules, &[]).unwrap_err();
|
|
|
|
|
match err_without {
|
|
|
|
|
WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => {
|
|
|
|
|
assert!(
|
|
|
|
|
!candidates.iter().any(|c| c == "prelude.Eq"),
|
|
|
|
|
"expected NO prelude.Eq in candidates, got {:?}", candidates
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// pd.1 Task 4: `load_workspace_with` after the shim refit
|
|
|
|
|
/// produces a `Workspace` byte-identical to today's. Pinned by
|
|
|
|
|
/// asserting prelude injection still happens AND the prelude
|
|
|
|
|
/// module's hash matches the freshly-loaded prelude — i.e. the
|
|
|
|
|
/// shim composition (`load_modules_with` + inject + `build_workspace`)
|
|
|
|
|
/// preserves the pre-refit observable behaviour.
|
|
|
|
|
#[test]
|
|
|
|
|
fn load_workspace_with_shim_preserves_today_semantics() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
write_module(dir.path(), "main", &[]);
|
|
|
|
|
let entry = dir.path().join("main.ail.json");
|
|
|
|
|
|
|
|
|
|
let ws = load_workspace_with(&entry, load_one).expect("load");
|
|
|
|
|
|
|
|
|
|
assert_eq!(ws.entry, "main");
|
|
|
|
|
assert!(ws.modules.contains_key("main"));
|
|
|
|
|
assert!(
|
|
|
|
|
ws.modules.contains_key("prelude"),
|
|
|
|
|
"shim must still inject prelude"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
module_hash(&ws.modules["prelude"]),
|
|
|
|
|
module_hash(&load_prelude()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|