Iter 14h: cross-module parameterised-ADT import (15a unblocked)
The 15a tester surfaced a real compiler limitation: cross-module
type and ctor references were not implemented. Iter 5b only
carried fns + consts via module_globals; types/ctors stayed
module-local with an explicit DESIGN comment. This iter completes
the cross-module mechanism using the Iter-5b convention:
qualified-only access via module.Name.
Implementation:
- ailang-check: Env.module_types populated by build_module_types.
Qualified resolution in Type::Con, Term::Ctor, with cross-module
fallback for pat-ctor (local wins, multi-import collision -> new
ambiguous-ctor diagnostic). Four new unit tests.
- ailang-codegen: workspace-level module_ctor_index replaces
per-Emitter table. lookup_ctor_by_type / lookup_ctor_in_pattern
thread qualified type names through box-tag and field-type
resolution.
- examples/std_maybe_demo.{ailx,ail.json}: type-name slots now
qualified (std_maybe.Maybe).
- New e2e test cross_module_maybe_demo asserts the demo prints
["7","99","true","true","42"].
Net diff ~550 LOC. Tests 80 -> 85. All Iter 14a regressions
(parameterised_box_round_trip, parameterised_maybe_match,
list_map_poly_inc_then_prints, polymorphic_id_at_int_and_bool)
verified green — the 14h derive_substitution change (default
unpinned forall vars to Unit for monomorphiser) sits on a
different layer than 14a's $u-wildcard fix and they coexist.
Hash invariance: all five std_maybe def hashes unchanged. All
80-test-suite fixtures retain bit-identical hashes — cross-module
support is purely additive at the language level.
Process note: the std_maybe.ailx file landed in the 14g commit
via a sloppy git-add-A; should have spotted it before staging.
Not a correctness issue but a hygiene one.
Implementer flagged Unit-default monomorphisation as wasteful-
but-correct; rethink if stdlib grows toward overload-resolution-
style cases needing distinct unconstrained instantiations.
std_maybe stdlib effectively ships: module + four combinators +
e2e-tested consumer demo. Plan 15b: std_list importing std_maybe,
exercising Maybe-returning head/tail and tail-call-marked
fold_left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
//! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
|
||||
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
|
||||
//! - `tail-call-not-in-tail-position` (Iter 14e, see Decision 8)
|
||||
//! - `ambiguous-ctor` — `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}` (Iter 15a)
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
|
||||
+426
-19
@@ -401,6 +401,18 @@ pub enum CheckError {
|
||||
/// Code: `tail-call-not-in-tail-position`. See Decision 8.
|
||||
#[error("call marked `tail` is not in tail position")]
|
||||
TailCallNotInTailPosition,
|
||||
|
||||
/// Iter 15a: a bare `Pattern::Ctor.ctor` did not resolve against the
|
||||
/// current module's `ctor_index` and resolved against ctors in two
|
||||
/// or more imported modules. The author must qualify the scrutinee
|
||||
/// type so that ctor lookup is unambiguous (e.g. write
|
||||
/// `(con std_maybe.Maybe (con Int))` for the scrutinee). Code:
|
||||
/// `ambiguous-ctor`. `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}`.
|
||||
#[error("ambiguous constructor `{ctor}`: declared in {candidates:?}")]
|
||||
AmbiguousCtor {
|
||||
ctor: String,
|
||||
candidates: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, CheckError>;
|
||||
@@ -437,6 +449,7 @@ impl CheckError {
|
||||
CheckError::UnknownImport { .. } => "unknown-import",
|
||||
CheckError::InvalidDefName { .. } => "invalid-def-name",
|
||||
CheckError::TailCallNotInTailPosition => "tail-call-not-in-tail-position",
|
||||
CheckError::AmbiguousCtor { .. } => "ambiguous-ctor",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +484,9 @@ impl CheckError {
|
||||
CheckError::InvalidDefName { name } => {
|
||||
serde_json::json!({"name": name, "reason": "contains-dot"})
|
||||
}
|
||||
CheckError::AmbiguousCtor { ctor, candidates } => {
|
||||
serde_json::json!({"ctor": ctor, "candidates": candidates})
|
||||
}
|
||||
_ => serde_json::Value::Object(serde_json::Map::new()),
|
||||
}
|
||||
}
|
||||
@@ -638,6 +654,32 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
Ok(CheckedModule { symbols })
|
||||
}
|
||||
|
||||
/// Iter 15a: builds the ADT type-def table per module. Sibling of
|
||||
/// [`build_module_globals`]: gives the body checker O(1) lookup of any
|
||||
/// type declared anywhere in the workspace, keyed by module name.
|
||||
/// Read by qualified `Type::Con.name` resolution
|
||||
/// (`module.Type`), qualified `Term::Ctor.type_name`, and the
|
||||
/// cross-module `Pattern::Ctor` fallback. Duplicate type / ctor errors
|
||||
/// inside a single module surface during the per-module body-check
|
||||
/// phase, not here.
|
||||
fn build_module_types(
|
||||
ws: &Workspace,
|
||||
) -> BTreeMap<String, IndexMap<String, TypeDef>> {
|
||||
let mut out: BTreeMap<String, IndexMap<String, TypeDef>> = BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let mut tys = IndexMap::new();
|
||||
for def in &m.defs {
|
||||
if let Def::Type(td) = def {
|
||||
// Last definition wins on duplicate; the per-module
|
||||
// body-check phase reports `duplicate-type` separately.
|
||||
tys.insert(td.name.clone(), td.clone());
|
||||
}
|
||||
}
|
||||
out.insert(mname.clone(), tys);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Builds the top-level symbol table per module (for cross-module lookup),
|
||||
/// without checking bodies. Duplicates and dot-in-def names are reported
|
||||
/// here as errors immediately — they would taint all further diagnostics.
|
||||
@@ -754,6 +796,7 @@ fn check_in_workspace(
|
||||
}
|
||||
env.imports = import_map;
|
||||
env.module_globals = module_globals.clone();
|
||||
env.module_types = build_module_types(ws);
|
||||
env.current_module = m.name.clone();
|
||||
// Workspace isn't directly needed in the env; cross-module lookup uses
|
||||
// only `module_globals`. But we keep the ws reference in the
|
||||
@@ -805,7 +848,26 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(td) = env.types.get(name) {
|
||||
// Iter 15a: a qualified type name `module.Type` resolves
|
||||
// through the import map and the per-module type table. The
|
||||
// bare-name path is unchanged.
|
||||
let td_opt: Option<&TypeDef> = if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||
let target_module = match env.imports.get(prefix) {
|
||||
Some(m) => m.as_str(),
|
||||
None => {
|
||||
return Err(CheckError::UnknownModule {
|
||||
module: prefix.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
env.module_types
|
||||
.get(target_module)
|
||||
.and_then(|tys| tys.get(suffix))
|
||||
} else {
|
||||
env.types.get(name)
|
||||
};
|
||||
if let Some(td) = td_opt {
|
||||
if td.vars.len() != args.len() {
|
||||
return Err(CheckError::UnknownType(format!(
|
||||
"{name} expects {} type arg(s), got {}",
|
||||
@@ -1053,12 +1115,23 @@ fn synth(
|
||||
module: target_module.clone(),
|
||||
}
|
||||
})?;
|
||||
g.get(suffix).cloned().ok_or_else(|| {
|
||||
let raw_ty = g.get(suffix).cloned().ok_or_else(|| {
|
||||
CheckError::UnknownImport {
|
||||
module: target_module,
|
||||
module: target_module.clone(),
|
||||
name: suffix.to_string(),
|
||||
}
|
||||
})?
|
||||
})?;
|
||||
// Iter 15a: qualify any bare type-cons referring to a
|
||||
// type defined in the owning module so that signatures
|
||||
// pulled across the boundary unify against
|
||||
// qualified-form ctors and types in the consumer
|
||||
// module.
|
||||
let owner_types = env
|
||||
.module_types
|
||||
.get(&target_module)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
qualify_local_types(&raw_ty, &target_module, &owner_types)
|
||||
} else {
|
||||
return Err(CheckError::UnknownIdent(name.clone()));
|
||||
};
|
||||
@@ -1152,11 +1225,31 @@ fn synth(
|
||||
Ok(sig.ret)
|
||||
}
|
||||
Term::Ctor { type_name, ctor, args } => {
|
||||
let td = env
|
||||
.types
|
||||
.get(type_name)
|
||||
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
|
||||
.clone();
|
||||
// Iter 15a: a qualified `type_name` (`module.Type`) resolves
|
||||
// through the import map; the ctor name stays bare and is
|
||||
// looked up inside the resolved TypeDef. The bare-name path
|
||||
// is the original Iter 13 behaviour.
|
||||
let td = if type_name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = type_name.split_once('.').expect("checked");
|
||||
let target_module = match env.imports.get(prefix) {
|
||||
Some(m) => m.clone(),
|
||||
None => {
|
||||
return Err(CheckError::UnknownModule {
|
||||
module: prefix.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
env.module_types
|
||||
.get(&target_module)
|
||||
.and_then(|tys| tys.get(suffix))
|
||||
.cloned()
|
||||
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
|
||||
} else {
|
||||
env.types
|
||||
.get(type_name)
|
||||
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
|
||||
.clone()
|
||||
};
|
||||
let cdef = td
|
||||
.ctors
|
||||
.iter()
|
||||
@@ -1248,9 +1341,25 @@ fn synth(
|
||||
}
|
||||
|
||||
if !has_open_arm {
|
||||
match &s_ty {
|
||||
Type::Con { name, .. } if env.types.contains_key(name) => {
|
||||
let td = &env.types[name];
|
||||
// Iter 15a: a qualified scrutinee type (`module.Type`,
|
||||
// produced by a cross-module ctor) resolves through
|
||||
// `env.module_types`; bare names use `env.types` as
|
||||
// before.
|
||||
let td_opt: Option<&TypeDef> = match &s_ty {
|
||||
Type::Con { name, .. } => {
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||
env.module_types
|
||||
.get(prefix)
|
||||
.and_then(|tys| tys.get(suffix))
|
||||
} else {
|
||||
env.types.get(name)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match (td_opt, &s_ty) {
|
||||
(Some(td), Type::Con { name, .. }) => {
|
||||
let missing: Vec<String> = td
|
||||
.ctors
|
||||
.iter()
|
||||
@@ -1325,6 +1434,52 @@ fn maybe_instantiate(t: Type, counter: &mut u32) -> Type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 15a: rewrites bare `Type::Con` references that resolve against
|
||||
/// `local_types` into qualified `module.Type` form. Used when pulling a
|
||||
/// fn type across module boundaries: a `Maybe a` declared inside
|
||||
/// `std_maybe` becomes `std_maybe.Maybe a` when seen from a consumer
|
||||
/// module — otherwise it would fail to unify with terms whose types
|
||||
/// the consumer module already qualifies.
|
||||
///
|
||||
/// Already-qualified names, primitives (`Int`, `Bool`, `Unit`, `Str`),
|
||||
/// rigid type vars, and type names that are not in `local_types` pass
|
||||
/// through unchanged.
|
||||
fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<String, TypeDef>) -> Type {
|
||||
match t {
|
||||
Type::Con { name, args } => {
|
||||
let qualified_name = if name.contains('.') {
|
||||
name.clone()
|
||||
} else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
|
||||
name.clone()
|
||||
} else if local_types.contains_key(name) {
|
||||
format!("{owner_module}.{name}")
|
||||
} else {
|
||||
name.clone()
|
||||
};
|
||||
Type::Con {
|
||||
name: qualified_name,
|
||||
args: args
|
||||
.iter()
|
||||
.map(|a| qualify_local_types(a, owner_module, local_types))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
Type::Fn { params, ret, effects } => Type::Fn {
|
||||
params: params
|
||||
.iter()
|
||||
.map(|p| qualify_local_types(p, owner_module, local_types))
|
||||
.collect(),
|
||||
ret: Box::new(qualify_local_types(ret, owner_module, local_types)),
|
||||
effects: effects.clone(),
|
||||
},
|
||||
Type::Forall { vars, body } => Type::Forall {
|
||||
vars: vars.clone(),
|
||||
body: Box::new(qualify_local_types(body, owner_module, local_types)),
|
||||
},
|
||||
Type::Var { .. } => t.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks a pattern against an expected type and returns the bindings
|
||||
/// introduced by the pattern.
|
||||
fn type_check_pattern(
|
||||
@@ -1354,15 +1509,49 @@ fn type_check_pattern(
|
||||
return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone()));
|
||||
}
|
||||
}
|
||||
let cref = env
|
||||
.ctor_index
|
||||
.get(ctor)
|
||||
.ok_or_else(|| CheckError::UnknownCtorInPattern(ctor.clone()))?;
|
||||
// Iter 15a: try local ctor_index first; if the bare name
|
||||
// doesn't resolve locally, fall back to scanning imported
|
||||
// modules' type defs. The fallback lookup keys on the
|
||||
// `module.Type` form so it lines up with what the typechecker
|
||||
// produces for qualified `Term::Ctor`s. Multiple imported
|
||||
// candidates → `ambiguous-ctor` (local always wins on
|
||||
// conflict, hence the "imported only if local missing" order).
|
||||
let resolved_type_name: String;
|
||||
let resolved_td: TypeDef;
|
||||
if let Some(cref) = env.ctor_index.get(ctor) {
|
||||
resolved_type_name = cref.type_name.clone();
|
||||
resolved_td = env.types[&cref.type_name].clone();
|
||||
} else {
|
||||
let mut hits: Vec<(String, TypeDef)> = Vec::new();
|
||||
for imp in env.imports.values() {
|
||||
if let Some(tys) = env.module_types.get(imp) {
|
||||
for (tname, td) in tys {
|
||||
if td.ctors.iter().any(|c| &c.name == ctor) {
|
||||
hits.push((format!("{imp}.{tname}"), td.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match hits.len() {
|
||||
0 => return Err(CheckError::UnknownCtorInPattern(ctor.clone())),
|
||||
1 => {
|
||||
let (qname, td) = hits.into_iter().next().expect("len == 1");
|
||||
resolved_type_name = qname;
|
||||
resolved_td = td;
|
||||
}
|
||||
_ => {
|
||||
return Err(CheckError::AmbiguousCtor {
|
||||
ctor: ctor.clone(),
|
||||
candidates: hits.into_iter().map(|(q, _)| q).collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// expected must be this ADT. For parameterised ADTs, capture
|
||||
// the type-args so we can substitute them into the cdef's
|
||||
// field types when binding sub-patterns.
|
||||
let scrutinee_args: Vec<Type> = match expected {
|
||||
Type::Con { name, args } if name == &cref.type_name => args.clone(),
|
||||
Type::Con { name, args } if name == &resolved_type_name => args.clone(),
|
||||
_ => {
|
||||
return Err(CheckError::PatternTypeMismatch {
|
||||
ctor: ctor.clone(),
|
||||
@@ -1370,7 +1559,7 @@ fn type_check_pattern(
|
||||
});
|
||||
}
|
||||
};
|
||||
let td = &env.types[&cref.type_name];
|
||||
let td = &resolved_td;
|
||||
let cdef = td
|
||||
.ctors
|
||||
.iter()
|
||||
@@ -1378,7 +1567,7 @@ fn type_check_pattern(
|
||||
.expect("indexed ctor exists");
|
||||
if fields.len() != cdef.fields.len() {
|
||||
return Err(CheckError::CtorArity {
|
||||
ty: cref.type_name.clone(),
|
||||
ty: resolved_type_name.clone(),
|
||||
ctor: ctor.clone(),
|
||||
expected: cdef.fields.len(),
|
||||
got: fields.len(),
|
||||
@@ -1459,6 +1648,12 @@ pub struct Env {
|
||||
/// Top-level symbol table per module of the workspace.
|
||||
/// `check_in_workspace` populates this from `build_module_globals`.
|
||||
pub module_globals: BTreeMap<String, IndexMap<String, Type>>,
|
||||
/// Iter 15a: ADT type definitions per module of the workspace. Used
|
||||
/// to resolve qualified type references (`module.Type` in
|
||||
/// `Type::Con.name`) and qualified `Term::Ctor.type_name`, and to
|
||||
/// fall back when a bare `Pattern::Ctor.ctor` cannot be resolved
|
||||
/// against the local module. Populated by `check_in_workspace`.
|
||||
pub module_types: BTreeMap<String, IndexMap<String, TypeDef>>,
|
||||
/// Name of the currently checked module. Used during var lookup to
|
||||
/// treat self-references (module name == own name) as local globals,
|
||||
/// without touching the `imports` channel.
|
||||
@@ -2188,6 +2383,218 @@ mod tests {
|
||||
assert_eq!(err.code(), "tail-call-not-in-tail-position", "{err}");
|
||||
}
|
||||
|
||||
// ----- Iter 15a: cross-module type / ctor resolution ------------------
|
||||
|
||||
/// Helper for the cross-module tests: build a workspace whose entry
|
||||
/// imports `lib` and exposes its types via qualified names.
|
||||
fn cross_module_ws(consumer: Module) -> Workspace {
|
||||
// `lib` declares `data Box a = MkBox(a)` and `data Bag a = Pack(a)`.
|
||||
// The second type lets us exercise ambiguous-ctor in dedicated tests.
|
||||
let lib = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "lib".into(),
|
||||
imports: vec![],
|
||||
defs: vec![
|
||||
Def::Type(TypeDef {
|
||||
name: "Box".into(),
|
||||
vars: vec!["a".into()],
|
||||
ctors: vec![Ctor {
|
||||
name: "MkBox".into(),
|
||||
fields: vec![Type::Var { name: "a".into() }],
|
||||
}],
|
||||
doc: None,
|
||||
}),
|
||||
Def::Type(TypeDef {
|
||||
name: "Bag".into(),
|
||||
vars: vec!["a".into()],
|
||||
ctors: vec![Ctor {
|
||||
name: "Pack".into(),
|
||||
fields: vec![Type::Var { name: "a".into() }],
|
||||
}],
|
||||
doc: None,
|
||||
}),
|
||||
],
|
||||
};
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("lib".into(), lib);
|
||||
modules.insert(consumer.name.clone(), consumer.clone());
|
||||
Workspace {
|
||||
entry: consumer.name,
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 15a, path 1: a qualified `Type::Con` (`lib.Box`) resolves
|
||||
/// through `module_types` rather than the local-module `types`.
|
||||
#[test]
|
||||
fn cross_module_qualified_type_in_param_resolves() {
|
||||
let consumer = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "use_lib".into(),
|
||||
imports: vec![Import { module: "lib".into(), alias: None }],
|
||||
defs: vec![fn_def(
|
||||
"noop",
|
||||
Type::Fn {
|
||||
params: vec![Type::Con {
|
||||
name: "lib.Box".into(),
|
||||
args: vec![Type::int()],
|
||||
}],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec!["b"],
|
||||
Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
)],
|
||||
};
|
||||
let ws = cross_module_ws(consumer);
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
||||
}
|
||||
|
||||
/// Iter 15a, path 2: a qualified `Term::Ctor.type_name`
|
||||
/// (`lib.Box`) resolves and constructs `lib.Box<Int>`.
|
||||
#[test]
|
||||
fn cross_module_qualified_term_ctor_resolves() {
|
||||
let consumer = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "use_lib".into(),
|
||||
imports: vec![Import { module: "lib".into(), alias: None }],
|
||||
defs: vec![fn_def(
|
||||
"make",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::Con {
|
||||
name: "lib.Box".into(),
|
||||
args: vec![Type::int()],
|
||||
}),
|
||||
effects: vec![],
|
||||
},
|
||||
vec![],
|
||||
Term::Ctor {
|
||||
type_name: "lib.Box".into(),
|
||||
ctor: "MkBox".into(),
|
||||
args: vec![Term::Lit { lit: Literal::Int { value: 7 } }],
|
||||
},
|
||||
)],
|
||||
};
|
||||
let ws = cross_module_ws(consumer);
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
||||
}
|
||||
|
||||
/// Iter 15a, path 3: a bare `Pattern::Ctor.ctor` falls back through
|
||||
/// imports when the ctor isn't local. The scrutinee carries the
|
||||
/// qualified type so the pattern check finds the right ADT.
|
||||
#[test]
|
||||
fn cross_module_pat_ctor_fallback_resolves() {
|
||||
let consumer = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "use_lib".into(),
|
||||
imports: vec![Import { module: "lib".into(), alias: None }],
|
||||
defs: vec![fn_def(
|
||||
"open",
|
||||
Type::Fn {
|
||||
params: vec![Type::Con {
|
||||
name: "lib.Box".into(),
|
||||
args: vec![Type::int()],
|
||||
}],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec!["b"],
|
||||
Term::Match {
|
||||
scrutinee: Box::new(Term::Var { name: "b".into() }),
|
||||
arms: vec![Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "MkBox".into(),
|
||||
fields: vec![Pattern::Var { name: "x".into() }],
|
||||
},
|
||||
body: Term::Var { name: "x".into() },
|
||||
}],
|
||||
},
|
||||
)],
|
||||
};
|
||||
let ws = cross_module_ws(consumer);
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
||||
}
|
||||
|
||||
/// Iter 15a, path 4: a bare ctor name that resolves in two
|
||||
/// imported modules surfaces as `ambiguous-ctor` with both
|
||||
/// candidates listed.
|
||||
#[test]
|
||||
fn cross_module_pat_ctor_ambiguous_errors() {
|
||||
// Two different libs, each declaring a ctor `Mk` (intentional clash).
|
||||
let lib_a = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "lib_a".into(),
|
||||
imports: vec![],
|
||||
defs: vec![Def::Type(TypeDef {
|
||||
name: "TA".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![Ctor { name: "Mk".into(), fields: vec![] }],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let lib_b = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "lib_b".into(),
|
||||
imports: vec![],
|
||||
defs: vec![Def::Type(TypeDef {
|
||||
name: "TB".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![Ctor { name: "Mk".into(), fields: vec![] }],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let consumer = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "use_both".into(),
|
||||
imports: vec![
|
||||
Import { module: "lib_a".into(), alias: None },
|
||||
Import { module: "lib_b".into(), alias: None },
|
||||
],
|
||||
defs: vec![fn_def(
|
||||
"f",
|
||||
Type::Fn {
|
||||
params: vec![Type::Con {
|
||||
name: "lib_a.TA".into(),
|
||||
args: vec![],
|
||||
}],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec!["t"],
|
||||
Term::Match {
|
||||
scrutinee: Box::new(Term::Var { name: "t".into() }),
|
||||
arms: vec![Arm {
|
||||
// Bare `Mk` is ambiguous between lib_a and lib_b.
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "Mk".into(),
|
||||
fields: vec![],
|
||||
},
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
}],
|
||||
},
|
||||
)],
|
||||
};
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("lib_a".into(), lib_a);
|
||||
modules.insert("lib_b".into(), lib_b);
|
||||
modules.insert("use_both".into(), consumer);
|
||||
let ws = Workspace {
|
||||
entry: "use_both".into(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
};
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(
|
||||
diags.iter().any(|d| d.code == "ambiguous-ctor"),
|
||||
"expected ambiguous-ctor diagnostic; got {diags:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 14e: a `Term::App { tail: true, .. }` that genuinely sits
|
||||
/// in tail position (as the rhs of a `Seq` that is the body of a
|
||||
/// `Match` arm that is the body of the fn) must pass.
|
||||
|
||||
Reference in New Issue
Block a user