use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use crate::ast::Schema;
use crate::diagnostic::Diagnostic;
use crate::ir::CompiledSchema;
use crate::lower::DependencyContext;
use crate::resolve::{LoadError, SchemaLoader};
#[derive(Debug, Clone, PartialEq)]
pub enum ProjectError {
CircularImport { chain: Vec<String> },
Load(LoadError),
ParseError { namespace: String, message: String },
}
impl fmt::Display for ProjectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProjectError::CircularImport { chain } => {
write!(f, "circular import detected: {}", chain.join(" → "))
}
ProjectError::Load(err) => write!(f, "load error: {err}"),
ProjectError::ParseError { namespace, message } => {
write!(f, "parse error in `{namespace}`: {message}")
}
}
}
}
impl From<LoadError> for ProjectError {
fn from(err: LoadError) -> Self {
ProjectError::Load(err)
}
}
#[derive(Debug, Clone)]
pub struct ImportGraph {
pub schemas: HashMap<String, (Schema, String, PathBuf)>,
pub edges: HashMap<String, Vec<String>>,
pub topo_order: Vec<String>,
}
pub fn build_import_graph(
root_source: &str,
root_path: &Path,
loader: &dyn SchemaLoader,
) -> Result<ImportGraph, ProjectError> {
let mut graph = ImportGraph {
schemas: HashMap::new(),
edges: HashMap::new(),
topo_order: Vec::new(),
};
let root_schema = parse_source(root_source, "<root>")?;
let root_ns = namespace_string(&root_schema)?;
graph.schemas.insert(
root_ns.clone(),
(
root_schema.clone(),
root_source.to_owned(),
root_path.to_path_buf(),
),
);
let mut visited: HashSet<String> = HashSet::new();
let mut stack: Vec<String> = Vec::new();
dfs(
&root_ns,
&root_schema,
loader,
&mut graph,
&mut visited,
&mut stack,
)?;
Ok(graph)
}
#[derive(Debug, Clone)]
pub struct ProjectResult {
pub schemas: Vec<(String, CompiledSchema)>,
pub diagnostics: Vec<Diagnostic>,
}
pub fn compile_project(
root_source: &str,
root_path: &Path,
loader: &dyn SchemaLoader,
) -> Result<ProjectResult, ProjectError> {
let mut graph = build_import_graph(root_source, root_path, loader)?;
let mut compiled_schemas: HashMap<String, CompiledSchema> = HashMap::new();
let mut all_diagnostics: Vec<Diagnostic> = Vec::new();
let mut result_schemas: Vec<(String, CompiledSchema)> = Vec::new();
let topo_order = graph.topo_order.clone();
for ns in &topo_order {
let (schema, _source_text, path) = match graph.schemas.remove(ns) {
Some(entry) => entry,
None => continue,
};
let dep_edges = graph.edges.get(ns).cloned().unwrap_or_default();
let dep_ctx = DependencyContext {
schemas: dep_edges
.iter()
.filter_map(|dep_ns| {
compiled_schemas
.get(dep_ns)
.map(|cs| (dep_ns.clone(), cs.clone()))
})
.collect(),
};
let (compiled, lower_diags) = crate::lower::lower_with_deps(&schema, Some(&dep_ctx));
all_diagnostics.extend(lower_diags.into_iter().map(|d| d.with_file(path.clone())));
if let Some(mut compiled) = compiled {
let check_diags = crate::typeck::check(&mut compiled);
all_diagnostics.extend(check_diags.into_iter().map(|d| d.with_file(path.clone())));
compiled_schemas.insert(ns.clone(), compiled.clone());
result_schemas.push((ns.clone(), compiled));
}
}
Ok(ProjectResult {
schemas: result_schemas,
diagnostics: all_diagnostics,
})
}
fn dfs(
ns: &str,
schema: &Schema,
loader: &dyn SchemaLoader,
graph: &mut ImportGraph,
visited: &mut HashSet<String>,
stack: &mut Vec<String>,
) -> Result<(), ProjectError> {
stack.push(ns.to_owned());
let deps = import_namespaces(schema);
graph.edges.insert(ns.to_owned(), deps.clone());
for dep_ns in &deps {
if let Some(cycle_start) = stack.iter().position(|s| s == dep_ns) {
let mut chain: Vec<String> = stack[cycle_start..].to_vec();
chain.push(dep_ns.clone());
return Err(ProjectError::CircularImport { chain });
}
if visited.contains(dep_ns.as_str()) {
continue;
}
let seg: Vec<&str> = dep_ns.split('.').collect();
let (src, path) = loader.load(&seg)?;
let dep_schema = parse_source(&src, dep_ns)?;
graph
.schemas
.insert(dep_ns.clone(), (dep_schema.clone(), src, path));
dfs(dep_ns, &dep_schema, loader, graph, visited, stack)?;
}
stack.pop();
visited.insert(ns.to_owned());
graph.topo_order.push(ns.to_owned());
Ok(())
}
fn parse_source(source: &str, namespace_hint: &str) -> Result<Schema, ProjectError> {
let result = crate::parse(source);
let first_error = result
.diagnostics
.iter()
.find(|d| d.severity == crate::diagnostic::Severity::Error)
.map(|d| d.message.clone());
if let Some(msg) = first_error {
return Err(ProjectError::ParseError {
namespace: namespace_hint.to_owned(),
message: msg,
});
}
result.schema.ok_or_else(|| ProjectError::ParseError {
namespace: namespace_hint.to_owned(),
message: "no schema produced".to_owned(),
})
}
fn namespace_string(schema: &Schema) -> Result<String, ProjectError> {
schema
.namespace
.as_ref()
.map(|ns| {
ns.node
.path
.iter()
.map(|s| s.node.as_str())
.collect::<Vec<_>>()
.join(".")
})
.ok_or_else(|| ProjectError::ParseError {
namespace: "<unknown>".to_owned(),
message: "schema must declare a namespace".to_owned(),
})
}
fn import_namespaces(schema: &Schema) -> Vec<String> {
schema
.imports
.iter()
.map(|imp| {
imp.node
.path
.iter()
.map(|s| s.node.as_str())
.collect::<Vec<_>>()
.join(".")
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::{ErrorClass, Severity};
use crate::resolve::InMemoryLoader;
use std::path::PathBuf;
fn make_loader(entries: &[(&str, &str)]) -> InMemoryLoader {
let mut loader = InMemoryLoader::new();
for (ns, src) in entries {
loader.schemas.insert(ns.to_string(), src.to_string());
}
loader
}
#[test]
fn simple_import_graph() {
let loader = make_loader(&[("b", "namespace b")]);
let root_src = "namespace a\nimport b";
let root_path = PathBuf::from("<memory>/a");
let graph = build_import_graph(root_src, &root_path, &loader)
.expect("should build graph without error");
assert_eq!(graph.schemas.len(), 2, "should have a and b");
assert_eq!(graph.topo_order.len(), 2);
let pos_a = graph.topo_order.iter().position(|s| s == "a").unwrap();
let pos_b = graph.topo_order.iter().position(|s| s == "b").unwrap();
assert!(pos_b < pos_a, "b must appear before a in topo_order");
}
#[test]
fn direct_cycle_detected() {
let loader = make_loader(&[
("b", "namespace b\nimport a"),
("a", "namespace a\nimport b"),
]);
let root_src = loader.schemas["a"].clone();
let root_path = PathBuf::from("<memory>/a");
let err = build_import_graph(&root_src, &root_path, &loader)
.expect_err("should detect circular import");
assert!(
matches!(err, ProjectError::CircularImport { .. }),
"expected CircularImport, got {err:?}"
);
}
#[test]
fn transitive_cycle_detected() {
let loader = make_loader(&[
("b", "namespace b\nimport c"),
("c", "namespace c\nimport a"),
("a", "namespace a\nimport b"),
]);
let root_src = loader.schemas["a"].clone();
let root_path = PathBuf::from("<memory>/a");
let err = build_import_graph(&root_src, &root_path, &loader)
.expect_err("should detect transitive cycle");
assert!(
matches!(err, ProjectError::CircularImport { .. }),
"expected CircularImport, got {err:?}"
);
}
#[test]
fn diamond_dependency() {
let loader = make_loader(&[
("b", "namespace b\nimport d"),
("c", "namespace c\nimport d"),
("d", "namespace d"),
]);
let root_src = "namespace a\nimport b\nimport c";
let root_path = PathBuf::from("<memory>/a");
let graph = build_import_graph(root_src, &root_path, &loader)
.expect("should build graph without error");
assert_eq!(graph.schemas.len(), 4, "should have a, b, c, d");
assert_eq!(
graph.topo_order.len(),
4,
"topo_order should have 4 entries"
);
let pos_d = graph.topo_order.iter().position(|s| s == "d").unwrap();
let pos_a = graph.topo_order.iter().position(|s| s == "a").unwrap();
assert!(pos_d < pos_a, "d must appear before a in topo_order");
}
#[test]
fn compile_project_simple_a_imports_b() {
let mut loader = InMemoryLoader::new();
loader.schemas.insert(
"b".to_string(),
"namespace b\nmessage Dep { y @0 : u32 }".to_string(),
);
let root = "namespace a\nimport { Dep } from b\nmessage Root { d @0 : Dep }";
let result = compile_project(root, &PathBuf::from("<test>"), &loader).unwrap();
assert_eq!(result.schemas.len(), 2);
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
}
#[test]
fn import_version_requirement_accepts_matching_schema() {
let loader = make_loader(&[(
"dep",
"@version(\"1.4.2\")\nnamespace dep\nmessage Value { value @0 : u8 }",
)]);
let root =
"namespace app\nimport { Value } from dep @ ^1.2.0\nmessage Root { value @0 : Value }";
let result = compile_project(root, &PathBuf::from("<memory>/app.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
}
#[test]
fn import_version_requirement_rejects_mismatch_on_import_span() {
let loader = make_loader(&[(
"dep",
"@version(\"2.0.0\")\nnamespace dep\nmessage Value { value @0 : u8 }",
)]);
let root =
"namespace app\nimport dep @ ^1.2.0 as Dep\nmessage Root { value @0 : Dep.Value }";
let result = compile_project(root, &PathBuf::from("<memory>/app.vexil"), &loader).unwrap();
let mismatch = result
.diagnostics
.iter()
.find(|diagnostic| diagnostic.class == ErrorClass::ImportVersionMismatch)
.expect("version mismatch");
assert_eq!(mismatch.code.as_str(), "E134");
assert_eq!(root[mismatch.span.range()].trim_end(), "@ ^1.2.0");
assert_eq!(
mismatch.source_file,
Some(PathBuf::from("<memory>/app.vexil"))
);
}
#[test]
fn import_version_requirement_warns_when_schema_is_unversioned() {
let loader = make_loader(&[("dep", "namespace dep\nmessage Value { value @0 : u8 }")]);
let root = "namespace app\nimport dep @ ^1.0.0";
let result = compile_project(root, &PathBuf::from("<memory>/app.vexil"), &loader).unwrap();
let warning = result
.diagnostics
.iter()
.find(|diagnostic| diagnostic.class == ErrorClass::ImportVersionUnavailable)
.expect("missing-version warning");
assert_eq!(warning.severity, Severity::Warning);
assert_eq!(warning.code.as_str(), "W135");
}
#[test]
fn transitive_and_diamond_import_requirements_are_enforced_per_edge() {
let loader = make_loader(&[
(
"base",
"@version(\"1.5.0\")\nnamespace base\nmessage Value { value @0 : u8 }",
),
(
"left",
"@version(\"1.0.0\")\nnamespace left\nimport base @ ^1.0.0",
),
(
"right",
"@version(\"1.0.0\")\nnamespace right\nimport base @ ^2.0.0",
),
]);
let root = "namespace app\nimport left @ ^1.0.0\nimport right @ ^1.0.0";
let result = compile_project(root, &PathBuf::from("<memory>/app.vexil"), &loader).unwrap();
let mismatches: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.class == ErrorClass::ImportVersionMismatch)
.collect();
assert_eq!(mismatches.len(), 1, "{:#?}", result.diagnostics);
assert_eq!(
mismatches[0].source_file,
Some(PathBuf::from("<memory>/right"))
);
}
#[test]
fn typed_tombstone_metadata_preserves_imported_type_identity() {
let loader = make_loader(&[(
"history",
"namespace history\nmessage RemovedValue { value @0 : u32 }",
)]);
let root = r#"
namespace current
import { RemovedValue } from history
message Current {
live @0 : bool
@removed(1, reason: "historical") : array<RemovedValue>
next @2 : u64
}
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/current.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:?}",
result.diagnostics
);
let (_, current) = result.schemas.last().expect("root schema");
let (_, current_def) = current.find_type("Current").expect("Current message");
let crate::ir::TypeDef::Message(current_def) = current_def else {
panic!("expected Current message");
};
let Some(crate::ir::ResolvedType::Array(original_type)) =
¤t_def.tombstones[0].original_type
else {
panic!("expected array tombstone metadata");
};
let crate::ir::ResolvedType::Named(removed_value_id) = **original_type else {
panic!("expected imported named metadata type");
};
assert_eq!(
current.registry.origin(removed_value_id),
Some(("history", "RemovedValue"))
);
}
#[test]
fn compile_project_diamond() {
let mut loader = InMemoryLoader::new();
loader.schemas.insert(
"d".to_string(),
"namespace d\nmessage Base { z @0 : u32 }".to_string(),
);
loader.schemas.insert(
"b".to_string(),
"namespace b\nimport { Base } from d\nmessage Left { b @0 : Base }".to_string(),
);
loader.schemas.insert(
"c".to_string(),
"namespace c\nimport { Base } from d\nmessage Right { b @0 : Base }".to_string(),
);
let root = "namespace a\nimport { Left } from b\nimport { Right } from c\nmessage Root { l @0 : Left\n r @1 : Right }";
let result = compile_project(root, &PathBuf::from("<test>"), &loader).unwrap();
assert_eq!(result.schemas.len(), 4);
let positions: HashMap<&str, usize> = result
.schemas
.iter()
.enumerate()
.map(|(i, (ns, _))| (ns.as_str(), i))
.collect();
assert!(positions["d"] < positions["b"]);
assert!(positions["d"] < positions["c"]);
assert!(positions["b"] < positions["a"]);
assert!(positions["c"] < positions["a"]);
}
#[test]
fn diamond_dependency_keeps_owned_impl_only_in_defining_schema() {
let loader = make_loader(&[
(
"d",
"namespace d\ntrait Tagged { value @0 : u32 }\nmessage Base { value @0 : u32 }\nimpl Tagged for Base { }",
),
(
"b",
"namespace b\nimport { Base } from d\nmessage Left { value @0 : Base }",
),
(
"c",
"namespace c\nimport { Base } from d\nmessage Right { value @0 : Base }",
),
]);
let root = "namespace a\nimport { Left } from b\nimport { Right } from c\nmessage Root { left @0 : Left right @1 : Right }";
let result = compile_project(root, &PathBuf::from("<test>"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
let owners = result
.schemas
.iter()
.flat_map(|(namespace, schema)| schema.impls().map(move |_| namespace.as_str()))
.collect::<Vec<_>>();
assert_eq!(owners, vec!["d"]);
}
#[test]
fn compile_project_single_file_no_imports() {
let loader = InMemoryLoader::new();
let root = "namespace solo\nmessage Msg { x @0 : u32 }";
let result = compile_project(root, &PathBuf::from("<test>"), &loader).unwrap();
assert_eq!(result.schemas.len(), 1);
assert_eq!(result.schemas[0].0, "solo");
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
}
#[test]
fn compile_project_diagnostics_have_source_file() {
let mut loader = InMemoryLoader::new();
loader.schemas.insert(
"b".to_string(),
"namespace b\nmessage Dep { y @0 : u32 }".to_string(),
);
let root = "namespace a\nimport { Dep } from b\nmessage Root { d @0 : Dep }";
let result = compile_project(root, &PathBuf::from("root.vxl"), &loader).unwrap();
for diag in &result.diagnostics {
assert!(
diag.source_file.is_some(),
"diagnostic missing source_file: {diag:?}"
);
}
}
#[test]
fn aliased_imported_trait_resolves_to_defining_identity() {
let loader = make_loader(&[(
"traits.contract",
"namespace traits.contract\ntrait Tagged<T> { value @0 : T }",
)]);
let root = r#"
namespace app.root
import traits.contract as Contract
message Event { value @0 : u64 }
impl Contract.Tagged<u64> for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "unexpected errors: {errors:#?}");
let (_, compiled) = result
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.expect("root schema");
let (impl_id, implementation) = compiled.impls().next().expect("local impl");
assert_eq!(implementation.trait_name, "Contract.Tagged");
let trait_id = compiled
.registry
.impl_trait_id(impl_id)
.expect("resolved trait identity");
let Some(crate::ir::TypeDef::Trait(trait_def)) = compiled.registry.get(trait_id) else {
panic!("resolved identity is not a trait");
};
assert_eq!(trait_def.name, "Tagged");
assert_eq!(
compiled.registry.origin(trait_id),
Some(("traits.contract", "Tagged"))
);
}
#[test]
fn trait_import_spelling_does_not_change_hash_or_compatibility() {
let loader = make_loader(&[(
"contracts",
"namespace contracts\ntrait Tagged { value @0 : u64 }",
)]);
let named = compile_project(
"namespace app.root\nimport { Tagged } from contracts\nmessage Event { value @0 : u64 }\nimpl Tagged for Event { }",
&PathBuf::from("<memory>/named/root.vexil"),
&loader,
)
.unwrap();
let aliased = compile_project(
"namespace app.root\nimport contracts as Contracts\nmessage Event { value @0 : u64 }\nimpl Contracts.Tagged for Event { }",
&PathBuf::from("<memory>/aliased/root.vexil"),
&loader,
)
.unwrap();
for result in [&named, &aliased] {
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
}
let named_root = named
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.map(|(_, schema)| schema)
.expect("named root schema");
let aliased_root = aliased
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.map(|(_, schema)| schema)
.expect("aliased root schema");
assert_eq!(
crate::canonical::schema_hash(named_root),
crate::canonical::schema_hash(aliased_root)
);
let report = crate::compat::check(named_root, aliased_root);
assert_eq!(report.result, crate::compat::CompatResult::Compatible);
assert!(report.changes.is_empty(), "{:?}", report.changes);
}
#[test]
fn transitive_trait_is_not_visible_without_explicit_root_import() {
let loader = make_loader(&[
(
"traits.contract",
"namespace traits.contract\ntrait Tagged { value @0 : u64 }",
),
(
"middle",
"namespace middle\nimport { Tagged } from traits.contract\nmessage Marker { ok @0 : bool }",
),
]);
let root = r#"
namespace app.root
import { Marker } from middle
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "expected one primary error: {errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(errors[0].message, "impl references unknown trait 'Tagged'");
}
#[test]
fn distinct_wildcard_trait_origins_are_ambiguous_on_use() {
let loader = make_loader(&[
("left", "namespace left\ntrait Tagged { value @0 : u64 }"),
("right", "namespace right\ntrait Tagged { value @0 : u64 }"),
]);
let root = r#"
namespace app.root
import left
import right
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "expected one primary error: {errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(
errors[0].message,
"trait name 'Tagged' is ambiguous across wildcard imports"
);
}
#[test]
fn direct_named_trait_is_visible_without_copying_imported_impls() {
let loader = make_loader(&[(
"traits.contract",
"namespace traits.contract\ntrait Tagged { value @0 : u64 }\nmessage Source { value @0 : u64 }\nimpl Tagged for Source { }",
)]);
let root = r#"
namespace app.root
import { Tagged } from traits.contract
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
let (_, compiled) = result
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.expect("root schema");
assert_eq!(
compiled.impls().count(),
1,
"imported impl must remain owned by its schema"
);
let (impl_id, _) = compiled.impls().next().expect("local impl");
let trait_id = compiled
.registry
.impl_trait_id(impl_id)
.expect("trait identity");
assert_eq!(
compiled.registry.origin(trait_id),
Some(("traits.contract", "Tagged"))
);
}
#[test]
fn unique_wildcard_trait_is_visible_unqualified() {
let loader = make_loader(&[(
"traits.contract",
"namespace traits.contract\ntrait Tagged { value @0 : u64 }",
)]);
let root = r#"
namespace app.root
import traits.contract
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
}
#[test]
fn named_trait_import_overrides_distinct_wildcard_candidates() {
let loader = make_loader(&[
("left", "namespace left\ntrait Tagged { value @0 : u64 }"),
("right", "namespace right\ntrait Tagged { value @0 : u64 }"),
]);
let root = r#"
namespace app.root
import left
import right
import { Tagged } from right
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
let (_, compiled) = result
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.expect("root schema");
let (impl_id, _) = compiled.impls().next().expect("local impl");
let trait_id = compiled
.registry
.impl_trait_id(impl_id)
.expect("trait identity");
assert_eq!(
compiled.registry.origin(trait_id),
Some(("right", "Tagged"))
);
}
#[test]
fn local_trait_shadows_wildcard_candidate() {
let loader = make_loader(&[(
"external",
"namespace external\ntrait Tagged { external @0 : u64 }",
)]);
let root = r#"
namespace app.root
import external
trait Tagged { value @0 : u64 }
message Event { value @0 : u64 }
impl Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
let (_, compiled) = result
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.expect("root schema");
let (impl_id, _) = compiled.impls().next().expect("local impl");
let trait_id = compiled
.registry
.impl_trait_id(impl_id)
.expect("trait identity");
assert_eq!(
compiled.registry.origin(trait_id),
Some(("app.root", "Tagged"))
);
}
#[test]
fn unused_distinct_wildcard_name_collision_is_not_an_error() {
let loader = make_loader(&[
("left", "namespace left\ntrait Tagged { left @0 : u64 }"),
("right", "namespace right\ntrait Tagged { right @0 : u64 }"),
]);
let root =
"namespace app.root\nimport left\nimport right\nmessage Event { value @0 : u64 }";
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
}
#[test]
fn qualified_trait_failures_have_one_primary_diagnostic_on_the_reference() {
let loader = make_loader(&[(
"contracts",
"namespace contracts\nmessage NotATrait { value @0 : u64 }",
)]);
for (reference, expected) in [
(
"Missing.Tagged",
"unknown import alias 'Missing' in impl trait reference",
),
(
"Contracts.Tagged",
"import alias 'Contracts' has no member 'Tagged'",
),
(
"Contracts.NotATrait",
"'Contracts.NotATrait' does not name a trait",
),
] {
let root = format!(
"namespace app.root\nimport contracts as Contracts\nmessage Event {{ value @0 : u64 }}\nimpl {reference} for Event {{ }}"
);
let result =
compile_project(&root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "{reference}: {errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(errors[0].message, expected);
assert_eq!(&root[errors[0].span.range()], reference);
}
}
#[test]
fn import_alias_local_collision_is_one_deterministic_error() {
let loader = make_loader(&[(
"contracts",
"namespace contracts\ntrait Tagged { value @0 : u64 }",
)]);
let root =
"namespace app.root\nimport contracts as Event\nmessage Event { value @0 : u64 }";
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "{errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(
errors[0].message,
"import alias 'Event' conflicts with a local declaration"
);
assert_eq!(&root[errors[0].span.range()], "Event");
}
#[test]
fn qualified_generic_trait_arity_failure_is_one_primary_diagnostic() {
let loader = make_loader(&[(
"contracts",
"namespace contracts\ntrait Tagged<T> { value @0 : T }",
)]);
let root = r#"
namespace app.root
import contracts as Contracts
message Event { value @0 : u64 }
impl Contracts.Tagged for Event { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "{errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(
errors[0].message,
"trait 'Contracts.Tagged' has 1 type parameters but impl provides 0"
);
assert_eq!(&root[errors[0].span.range()], "Contracts.Tagged");
}
#[test]
fn duplicate_import_alias_is_one_deterministic_error_on_the_second_alias() {
let loader = make_loader(&[
("left", "namespace left\ntrait LeftTrait { value @0 : u64 }"),
(
"right",
"namespace right\ntrait RightTrait { value @0 : u64 }",
),
]);
let root =
"namespace app.root\nimport left as Contracts\nimport right as Contracts\nmessage Event { value @0 : u64 }";
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "{errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(errors[0].message, "duplicate import alias 'Contracts'");
assert_eq!(&root[errors[0].span.range()], "Contracts");
assert_eq!(
errors[0].span.offset as usize,
root.rfind("Contracts").expect("second alias")
);
}
#[test]
fn qualification_bypasses_unqualified_local_trait_shadowing() {
let loader = make_loader(&[(
"contracts",
"namespace contracts\ntrait Tagged { external @0 : u64 }",
)]);
let root = r#"
namespace app.root
import contracts as Contracts
trait Tagged { local @0 : u64 }
message ExternalEvent { external @0 : u64 }
message LocalEvent { local @0 : u64 }
impl Contracts.Tagged for ExternalEvent { }
impl Tagged for LocalEvent { }
"#;
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
assert!(
result
.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity != Severity::Error),
"{:#?}",
result.diagnostics
);
let (_, compiled) = result
.schemas
.iter()
.find(|(namespace, _)| namespace == "app.root")
.expect("root schema");
let origins = compiled
.impls()
.map(|(impl_id, _)| {
let trait_id = compiled
.registry
.impl_trait_id(impl_id)
.expect("trait identity");
compiled.registry.origin(trait_id).expect("trait origin")
})
.collect::<Vec<_>>();
assert_eq!(
origins,
vec![("contracts", "Tagged"), ("app.root", "Tagged")]
);
}
#[test]
fn distinct_direct_named_imports_with_same_spelling_are_rejected() {
let loader = make_loader(&[
("left", "namespace left\ntrait Tagged { left @0 : u64 }"),
("right", "namespace right\ntrait Tagged { right @0 : u64 }"),
]);
let root = "namespace app.root\nimport { Tagged } from left\nimport { Tagged } from right\nmessage Event { value @0 : u64 }";
let result =
compile_project(root, &PathBuf::from("<memory>/app/root.vexil"), &loader).unwrap();
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
assert_eq!(errors.len(), 1, "{errors:#?}");
assert_eq!(errors[0].class, ErrorClass::UnresolvedType);
assert_eq!(
errors[0].message,
"named import 'Tagged' conflicts between namespaces 'left' and 'right'"
);
assert_eq!(&root[errors[0].span.range()], "Tagged");
assert_eq!(
errors[0].span.offset as usize,
root.rfind("Tagged").expect("second named import")
);
}
}