diff --git a/crates/stef-benches/benches/compiler.rs b/crates/stef-benches/benches/compiler.rs index bf4ff45..00660ff 100644 --- a/crates/stef-benches/benches/compiler.rs +++ b/crates/stef-benches/benches/compiler.rs @@ -11,7 +11,7 @@ fn main() { fn large_schema(bencher: Bencher) { let schema = stef_benches::generate_schema(N); let schema = stef_parser::Schema::parse(&schema).unwrap(); - stef_compiler::validate_schema(&schema).unwrap(); + stef_compiler::validate_schema("bench", &schema).unwrap(); - bencher.bench(|| stef_compiler::validate_schema(black_box(&schema))) + bencher.bench(|| stef_compiler::validate_schema("bench", black_box(&schema))) } diff --git a/crates/stef-build/src/lib.rs b/crates/stef-build/src/lib.rs index 17196b5..50b5c77 100644 --- a/crates/stef-build/src/lib.rs +++ b/crates/stef-build/src/lib.rs @@ -73,6 +73,7 @@ pub fn compile(schemas: &[impl AsRef], _includes: &[impl AsRef]) -> R glob: schema.to_owned(), })? { let path = schema.map_err(|e| Error::Glob { source: e })?; + let stem = path.file_stem().unwrap().to_str().unwrap(); let input = std::fs::read_to_string(&path).map_err(|source| Error::Read { source, @@ -85,7 +86,7 @@ pub fn compile(schemas: &[impl AsRef], _includes: &[impl AsRef]) -> R file: path.clone(), })?; - stef_compiler::validate_schema(&schema).map_err(|e| Error::Compile { + stef_compiler::validate_schema(stem, &schema).map_err(|e| Error::Compile { report: Report::new(e) .with_source_code(NamedSource::new(path.display().to_string(), input.clone())), file: path.clone(), @@ -94,12 +95,7 @@ pub fn compile(schemas: &[impl AsRef], _includes: &[impl AsRef]) -> R let code = definition::compile_schema(&schema); let code = prettyplease::unparse(&syn::parse2(code).unwrap()); - println!("{code}"); - - let out_file = out_dir.join(format!( - "{}.rs", - path.file_stem().unwrap().to_str().unwrap() - )); + let out_file = out_dir.join(format!("{stem}.rs",)); std::fs::write(out_file, code).unwrap(); } diff --git a/crates/stef-compiler/src/lib.rs b/crates/stef-compiler/src/lib.rs index 60162c0..56f1baf 100644 --- a/crates/stef-compiler/src/lib.rs +++ b/crates/stef-compiler/src/lib.rs @@ -11,11 +11,13 @@ use thiserror::Error; use self::{ generics::InvalidGenericType, names::{DuplicateFieldName, DuplicateName}, + resolve::ResolveError, }; mod generics; mod ids; mod names; +mod resolve; #[derive(Debug, Diagnostic, Error)] pub enum Error { @@ -28,6 +30,9 @@ pub enum Error { #[error("invalid generic type found")] #[diagnostic(transparent)] InvalidGeneric(#[from] InvalidGenericType), + #[error("type resolution failed")] + #[diagnostic(transparent)] + Resolve(#[from] ResolveError), } impl From for Error { @@ -42,9 +47,14 @@ impl From for Error { } } -pub fn validate_schema(value: &Schema<'_>) -> Result<(), Error> { +pub fn validate_schema(name: &str, value: &Schema<'_>) -> Result<(), Error> { names::validate_names_in_module(&value.definitions)?; - value.definitions.iter().try_for_each(validate_definition) + value.definitions.iter().try_for_each(validate_definition)?; + + let module = resolve::resolve_types(name, value); + resolve::resolve_module_definitions(&module)?; + + Ok(()) } fn validate_definition(value: &Definition<'_>) -> Result<(), Error> { diff --git a/crates/stef-compiler/src/resolve.rs b/crates/stef-compiler/src/resolve.rs new file mode 100644 index 0000000..87c80b8 --- /dev/null +++ b/crates/stef-compiler/src/resolve.rs @@ -0,0 +1,285 @@ +use std::{collections::HashMap, ops::Range}; + +use miette::Diagnostic; +use stef_parser::{DataType, Definition, ExternalType, Fields, Generics, Name, Schema, Spanned}; +use thiserror::Error; + +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveError { + #[error("failed resolving type in local modules")] + #[diagnostic(transparent)] + Local(#[from] ResolveLocal), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveLocal { + #[error("module {name} not found")] + #[diagnostic(help("the resolution stopped at module path {path}"))] + MissingModule { + name: String, + path: String, + #[label("used here")] + used: Range, + }, + #[error("definition {name} not found in module {path}")] + MissingDefinition { + name: String, + path: String, + #[label("used here")] + used: Range, + }, + #[error("the definition has {definition} generics but the use side has {usage}")] + #[diagnostic(help("the amount of generics must always match"))] + GenericsCount { + definition: usize, + usage: usize, + #[label("declared here")] + declared: Range, + #[label("used here")] + used: Range, + }, + #[error("definition found, but a {kind} can't be referenced")] + #[diagnostic(help("only struct and enum definitions can be used"))] + InvalidKind { + kind: &'static str, + #[label("declared here")] + declared: Range, + #[label("used here")] + used: Range, + }, +} + +pub struct Module<'a> { + /// Name of this module. + pub name: &'a str, + /// Full path from the root (the schema) till here. + path: String, + /// List of types that are declared in this module. + types: Vec>, + /// Directly submodules located in this module. + modules: HashMap<&'a str, Module<'a>>, + definitions: &'a [Definition<'a>], +} + +pub struct Type<'a> { + kind: TypeKind, + name: Name<'a>, +} + +pub enum TypeKind { + Struct { generics: usize }, + Enum { generics: usize }, + Const, +} + +impl<'a> Module<'a> { + pub fn resolve_local(&self, ty: &ExternalType<'_>) -> Result<(), ResolveLocal> { + let module = if ty.path.is_empty() { + self + } else { + ty.path.iter().try_fold(self, |module, name| { + module + .modules + .get(name.get()) + .ok_or_else(|| ResolveLocal::MissingModule { + name: name.get().to_owned(), + path: module.path.clone(), + used: ty.name.span().into(), + }) + })? + }; + + let definition = module + .types + .iter() + .find(|type_def| type_def.name.get() == ty.name.get()) + .ok_or_else(|| ResolveLocal::MissingDefinition { + name: ty.name.get().to_owned(), + path: module.path.clone(), + used: ty.name.span().into(), + })?; + + match definition.kind { + TypeKind::Struct { generics } | TypeKind::Enum { generics } + if generics != ty.generics.len() => + { + Err(ResolveLocal::GenericsCount { + definition: generics, + usage: ty.generics.len(), + declared: definition.name.span().into(), + used: ty.name.span().into(), + }) + } + TypeKind::Const => Err(ResolveLocal::InvalidKind { + kind: "const", + declared: definition.name.span().into(), + used: ty.name.span().into(), + }), + _ => Ok(()), + } + } +} + +pub(crate) fn resolve_module_definitions(module: &Module<'_>) -> Result<(), ResolveError> { + fn resolve( + ty: &DataType<'_>, + generics: &Generics<'_>, + module: &Module<'_>, + ) -> Result<(), ResolveLocal> { + visit_externals(ty, &mut |external| { + if external.generics.is_empty() + && external.path.is_empty() + && generics + .0 + .iter() + .any(|gen| gen.get() == external.name.get()) + { + Ok(()) + } else { + module.resolve_local(external) + } + }) + } + + for def in module.definitions { + match def { + Definition::Struct(s) => match &s.fields { + Fields::Named(named) => { + for field in named { + resolve(&field.ty, &s.generics, module)?; + } + } + Fields::Unnamed(unnamed) => { + for field in unnamed { + resolve(&field.ty, &s.generics, module)?; + } + } + Fields::Unit => {} + }, + Definition::Enum(e) => { + for variant in &e.variants { + match &variant.fields { + Fields::Named(named) => { + for field in named { + resolve(&field.ty, &e.generics, module)?; + } + } + Fields::Unnamed(unnamed) => { + for field in unnamed { + resolve(&field.ty, &e.generics, module)?; + } + } + Fields::Unit => {} + } + } + } + _ => {} + } + } + + for module in module.modules.values() { + resolve_module_definitions(module)?; + } + + Ok(()) +} + +pub(crate) fn resolve_types<'a>(name: &'a str, value: &'a Schema<'_>) -> Module<'a> { + visit_module_tree(name, "", &value.definitions) +} + +/// Build up modules from the given one all the way down to all submodules. +/// +/// This builds a tree structure of elements defined in each module, so they can be looked up in a +/// 2nd step to ensure all used types are actually available and correct. +fn visit_module_tree<'a>(name: &'a str, path: &'_ str, defs: &'a [Definition<'_>]) -> Module<'a> { + let mut module = Module { + name, + path: format!("{path}::{name}"), + types: Vec::new(), + modules: HashMap::new(), + definitions: defs, + }; + + for def in defs { + match def { + Definition::Module(m) => { + module.modules.insert( + m.name.get(), + visit_module_tree(m.name.get(), &module.path, &m.definitions), + ); + } + Definition::Struct(s) => module.types.push(Type { + kind: TypeKind::Struct { + generics: s.generics.0.len(), + }, + name: s.name.clone(), + }), + Definition::Enum(e) => module.types.push(Type { + kind: TypeKind::Enum { + generics: e.generics.0.len(), + }, + name: e.name.clone(), + }), + Definition::Const(c) => module.types.push(Type { + kind: TypeKind::Const, + name: c.name.clone(), + }), + _ => {} + } + } + + module +} + +fn visit_externals(value: &DataType<'_>, visit: &mut F) -> Result<(), E> +where + F: FnMut(&ExternalType<'_>) -> Result<(), E>, +{ + match value { + DataType::Bool + | DataType::U8 + | DataType::U16 + | DataType::U32 + | DataType::U64 + | DataType::U128 + | DataType::I8 + | DataType::I16 + | DataType::I32 + | DataType::I64 + | DataType::I128 + | DataType::F32 + | DataType::F64 + | DataType::String + | DataType::StringRef + | DataType::Bytes + | DataType::BytesRef + | DataType::NonZero(_) + | DataType::BoxString + | DataType::BoxBytes => {} + DataType::Vec(ty) + | DataType::HashSet(ty) + | DataType::Option(ty) + | DataType::Array(ty, _) => { + visit_externals(ty, visit)?; + } + DataType::HashMap(kv) => { + visit_externals(&kv.0, visit)?; + visit_externals(&kv.1, visit)?; + } + DataType::Tuple(types) => { + for ty in types { + visit_externals(ty, visit)?; + } + } + DataType::External(ty) => { + visit(ty)?; + + for ty in &ty.generics { + visit_externals(ty, visit)?; + } + } + } + + Ok(()) +} diff --git a/crates/stef-compiler/tests/compiler.rs b/crates/stef-compiler/tests/compiler.rs index 5e1f88d..43f8988 100644 --- a/crates/stef-compiler/tests/compiler.rs +++ b/crates/stef-compiler/tests/compiler.rs @@ -26,9 +26,10 @@ fn compile_invalid_schema() { .build(); glob!("inputs/invalid/*.stef", |path| { + let name = path.file_stem().unwrap().to_str().unwrap(); let input = fs::read_to_string(path).unwrap(); let schema = Schema::parse(input.as_str()).unwrap(); - let result = stef_compiler::validate_schema(&schema).unwrap_err(); + let result = stef_compiler::validate_schema(name, &schema).unwrap_err(); let report = Report::new(result).with_source_code(NamedSource::new( path.file_name().unwrap().to_string_lossy(), input, diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_gens_mismatch.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_gens_mismatch.stef new file mode 100644 index 0000000..36f7f6e --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_gens_mismatch.stef @@ -0,0 +1,5 @@ +struct Sample { + value: Other @1, +} + +struct Other(T @1) diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_kind_mismatch.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_kind_mismatch.stef new file mode 100644 index 0000000..d413abf --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_kind_mismatch.stef @@ -0,0 +1,5 @@ +struct Sample { + value: OTHER @1, +} + +const OTHER: u32 = 1; diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_root.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_root.stef new file mode 100644 index 0000000..c2a2ed8 --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_root.stef @@ -0,0 +1,3 @@ +struct Sample { + value: Other @1, +} diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_sub.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_sub.stef new file mode 100644 index 0000000..d9ef5af --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_sub.stef @@ -0,0 +1,5 @@ +struct Sample { + value: inner::Other @1, +} + +mod inner {} diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested.stef new file mode 100644 index 0000000..d3bebd1 --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested.stef @@ -0,0 +1,11 @@ +struct Sample { + value: inner::a::b::c::Other @1, +} + +mod inner { + mod a { + mod b { + mod c {} + } + } +} diff --git a/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested2.stef b/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested2.stef new file mode 100644 index 0000000..ef37e03 --- /dev/null +++ b/crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested2.stef @@ -0,0 +1,11 @@ +struct Sample { + value: inner::a::b::c::Other @1, +} + +mod inner { + mod a { + mod d { + mod c {} + } + } +} diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_gens_mismatch.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_gens_mismatch.stef.snap new file mode 100644 index 0000000..0fd246d --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_gens_mismatch.stef.snap @@ -0,0 +1,21 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_gens_mismatch.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ the definition has 1 generics but the use side has 0 + ╭─[resolve_gens_mismatch.stef:1:1] + 1 │ struct Sample { + 2 │ value: Other @1, + · ──┬── + · ╰── used here + 3 │ } + 4 │ + 5 │ struct Other(T @1) + · ──┬── + · ╰── declared here + ╰──── + help: the amount of generics must always match + diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_kind_mismatch.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_kind_mismatch.stef.snap new file mode 100644 index 0000000..db2bf46 --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_kind_mismatch.stef.snap @@ -0,0 +1,21 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_kind_mismatch.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ definition found, but a const can't be referenced + ╭─[resolve_kind_mismatch.stef:1:1] + 1 │ struct Sample { + 2 │ value: OTHER @1, + · ──┬── + · ╰── used here + 3 │ } + 4 │ + 5 │ const OTHER: u32 = 1; + · ──┬── + · ╰── declared here + ╰──── + help: only struct and enum definitions can be used + diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_root.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_root.stef.snap new file mode 100644 index 0000000..cb970f4 --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_root.stef.snap @@ -0,0 +1,16 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_root.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ definition Other not found in module ::resolve_root + ╭─[resolve_root.stef:1:1] + 1 │ struct Sample { + 2 │ value: Other @1, + · ──┬── + · ╰── used here + 3 │ } + ╰──── + diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub.stef.snap new file mode 100644 index 0000000..6a7566d --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub.stef.snap @@ -0,0 +1,18 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_sub.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ definition Other not found in module ::resolve_sub::inner + ╭─[resolve_sub.stef:1:1] + 1 │ struct Sample { + 2 │ value: inner::Other @1, + · ──┬── + · ╰── used here + 3 │ } + 4 │ + 5 │ mod inner {} + ╰──── + diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested.stef.snap new file mode 100644 index 0000000..c501a6d --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested.stef.snap @@ -0,0 +1,18 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ definition Other not found in module ::resolve_sub_nested::inner::a::b::c + ╭─[resolve_sub_nested.stef:1:1] + 1 │ struct Sample { + 2 │ value: inner::a::b::c::Other @1, + · ──┬── + · ╰── used here + 3 │ } + 4 │ + 5 │ mod inner { + ╰──── + diff --git a/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested2.stef.snap b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested2.stef.snap new file mode 100644 index 0000000..b2227fa --- /dev/null +++ b/crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested2.stef.snap @@ -0,0 +1,19 @@ +--- +source: crates/stef-compiler/tests/compiler.rs +expression: "stef_compiler :: validate_schema(& schema).unwrap_err()" +input_file: crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested2.stef +--- + × type resolution failed + ├─▶ failed resolving type in local modules + ╰─▶ module b not found + ╭─[resolve_sub_nested2.stef:1:1] + 1 │ struct Sample { + 2 │ value: inner::a::b::c::Other @1, + · ──┬── + · ╰── used here + 3 │ } + 4 │ + 5 │ mod inner { + ╰──── + help: the resolution stopped at module path ::resolve_sub_nested2::inner::a +