Skip to content

Commit

Permalink
feat: resolve all local types in schemas
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dnaka91 committed Oct 28, 2023
1 parent 2acd7b7 commit 2b518ab
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 12 deletions.
4 changes: 2 additions & 2 deletions crates/stef-benches/benches/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn main() {
fn large_schema<const N: usize>(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)))
}
10 changes: 3 additions & 7 deletions crates/stef-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub fn compile(schemas: &[impl AsRef<str>], _includes: &[impl AsRef<Path>]) -> 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,
Expand All @@ -85,7 +86,7 @@ pub fn compile(schemas: &[impl AsRef<str>], _includes: &[impl AsRef<Path>]) -> 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(),
Expand All @@ -94,12 +95,7 @@ pub fn compile(schemas: &[impl AsRef<str>], _includes: &[impl AsRef<Path>]) -> 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();
}
Expand Down
14 changes: 12 additions & 2 deletions crates/stef-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<DuplicateFieldId> for Error {
Expand All @@ -42,9 +47,14 @@ impl From<DuplicateFieldName> 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> {
Expand Down
285 changes: 285 additions & 0 deletions crates/stef-compiler/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -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<usize>,
},
#[error("definition {name} not found in module {path}")]
MissingDefinition {
name: String,
path: String,
#[label("used here")]
used: Range<usize>,
},
#[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<usize>,
#[label("used here")]
used: Range<usize>,
},
#[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<usize>,
#[label("used here")]
used: Range<usize>,
},
}

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<Type<'a>>,
/// 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<E, F>(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(())
}
3 changes: 2 additions & 1 deletion crates/stef-compiler/tests/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
struct Sample {
value: Other @1,
}

struct Other<T>(T @1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
struct Sample {
value: OTHER @1,
}

const OTHER: u32 = 1;
3 changes: 3 additions & 0 deletions crates/stef-compiler/tests/inputs/invalid/resolve_root.stef
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
struct Sample {
value: Other @1,
}
Loading

0 comments on commit 2b518ab

Please sign in to comment.