From 2b518ab70920b86ac8a5e9e6fa8aea5579fdd1aa Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Sat, 28 Oct 2023 22:05:50 +0900 Subject: [PATCH] feat: resolve all local types in schemas Check that all external types defined in any struct or enum definition exist within the bounds of the schema and give nice error messages in case a type can't be located. --- crates/stef-benches/benches/compiler.rs | 4 +- crates/stef-build/src/lib.rs | 10 +- crates/stef-compiler/src/lib.rs | 14 +- crates/stef-compiler/src/resolve.rs | 285 ++++++++++++++++++ crates/stef-compiler/tests/compiler.rs | 3 +- .../inputs/invalid/resolve_gens_mismatch.stef | 5 + .../inputs/invalid/resolve_kind_mismatch.stef | 5 + .../tests/inputs/invalid/resolve_root.stef | 3 + .../tests/inputs/invalid/resolve_sub.stef | 5 + .../inputs/invalid/resolve_sub_nested.stef | 11 + .../inputs/invalid/resolve_sub_nested2.stef | 11 + ...ler__error@resolve_gens_mismatch.stef.snap | 21 ++ ...ler__error@resolve_kind_mismatch.stef.snap | 21 ++ .../compiler__error@resolve_root.stef.snap | 16 + .../compiler__error@resolve_sub.stef.snap | 18 ++ ...mpiler__error@resolve_sub_nested.stef.snap | 18 ++ ...piler__error@resolve_sub_nested2.stef.snap | 19 ++ 17 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 crates/stef-compiler/src/resolve.rs create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_gens_mismatch.stef create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_kind_mismatch.stef create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_root.stef create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_sub.stef create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested.stef create mode 100644 crates/stef-compiler/tests/inputs/invalid/resolve_sub_nested2.stef create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_gens_mismatch.stef.snap create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_kind_mismatch.stef.snap create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_root.stef.snap create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub.stef.snap create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested.stef.snap create mode 100644 crates/stef-compiler/tests/snapshots/compiler__error@resolve_sub_nested2.stef.snap 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 +