diff --git a/Cargo.lock b/Cargo.lock index ddd2d6b..4806204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "atty" version = "0.2.14" @@ -698,6 +704,7 @@ dependencies = [ "proc-macro2", "quote", "stef", + "stef-compiler", "stef-parser", "syn", "thiserror", @@ -719,6 +726,7 @@ name = "stef-compiler" version = "0.1.0" dependencies = [ "stef-parser", + "thiserror", ] [[package]] @@ -746,6 +754,7 @@ dependencies = [ name = "stef-playground" version = "0.1.0" dependencies = [ + "anyhow", "stef", "stef-build", ] @@ -838,18 +847,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8f6c80e..97d87f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ mimalloc = "0.1.39" proc-macro2 = { version = "1.0.69", default-features = false } quote = { version = "1.0.33", default-features = false } syn = "2.0.38" -thiserror = "1.0.49" +thiserror = "1.0.50" [profile.release] lto = true diff --git a/crates/stef-build/Cargo.toml b/crates/stef-build/Cargo.toml index d804589..8baa2ef 100644 --- a/crates/stef-build/Cargo.toml +++ b/crates/stef-build/Cargo.toml @@ -15,6 +15,7 @@ miette.workspace = true prettyplease = "0.2.15" proc-macro2.workspace = true quote.workspace = true +stef-compiler = { path = "../stef-compiler" } stef-parser = { path = "../stef-parser" } syn.workspace = true thiserror.workspace = true diff --git a/crates/stef-build/src/lib.rs b/crates/stef-build/src/lib.rs index 034b244..01a01ee 100644 --- a/crates/stef-build/src/lib.rs +++ b/crates/stef-build/src/lib.rs @@ -38,27 +38,39 @@ pub enum Error { }, #[error("failed parsing schema from {file:?}: {message}")] Parse { message: String, file: PathBuf }, + #[error("failed compiling schema from {file:?}")] + Compile { + #[source] + source: stef_compiler::Error, + file: PathBuf, + }, } pub fn compile(schemas: &[impl AsRef], _includes: &[impl AsRef]) -> Result<()> { let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); for schema in schemas.iter().map(AsRef::as_ref) { - for schema in glob::glob(schema).map_err(|e| Error::Pattern { - source: e, + for schema in glob::glob(schema).map_err(|source| Error::Pattern { + source, glob: schema.to_owned(), })? { let path = schema.map_err(|e| Error::Glob { source: e })?; - let input = std::fs::read_to_string(&path).map_err(|e| Error::Read { - source: e, + let input = std::fs::read_to_string(&path).map_err(|source| Error::Read { + source, file: path.clone(), })?; let schema = Schema::parse(&input).map_err(|e| Error::Parse { - message: e.to_string(), + message: format!("{e:?}"), file: path.clone(), })?; + + stef_compiler::validate_schema(&schema).map_err(|source| Error::Compile { + source, + file: path.clone(), + })?; + let code = definition::compile_schema(&schema); let code = prettyplease::unparse(&syn::parse2(code).unwrap()); diff --git a/crates/stef-compiler/Cargo.toml b/crates/stef-compiler/Cargo.toml index ba3843f..25bbde6 100644 --- a/crates/stef-compiler/Cargo.toml +++ b/crates/stef-compiler/Cargo.toml @@ -11,3 +11,4 @@ license.workspace = true [dependencies] stef-parser = { path = "../stef-parser" } +thiserror.workspace = true diff --git a/crates/stef-compiler/src/ids.rs b/crates/stef-compiler/src/ids.rs new file mode 100644 index 0000000..fafde4d --- /dev/null +++ b/crates/stef-compiler/src/ids.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use stef_parser::{Enum, Fields, Id, Struct}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DuplicateId { + #[error("duplicate ID in an enum variant")] + EnumVariant(#[from] DuplicateVariantId), + #[error("duplicate ID in a field")] + Field(#[from] DuplicateFieldId), +} + +#[derive(Debug, Error)] +#[error("duplicate ID {} in enum variant `{name}`, already used in `{other_name}`", id.0)] +pub struct DuplicateVariantId { + pub id: Id, + pub name: String, + pub other_name: String, +} + +#[derive(Debug, Error)] +pub enum DuplicateFieldId { + #[error("duplicate ID {} in field `{name}`, already used in `{other_name}`", id.0)] + Named { + id: Id, + name: String, + other_name: String, + }, + #[error("duplicate ID {} in field {position}, already used in {other_position}", id.0)] + Unnamed { + id: Id, + position: usize, + other_position: usize, + }, +} + +/// Ensure all IDs inside a struct are unique (which are the field IDs). +pub(crate) fn validate_struct_ids(value: &Struct<'_>) -> Result<(), DuplicateFieldId> { + validate_field_ids(&value.fields) +} + +/// Ensure all IDs inside an enum are unique, which means all variants have a unique ID, plus all +/// potential fields in a variant are unique (within that variant). +pub(crate) fn validate_enum_ids(value: &Enum<'_>) -> Result<(), DuplicateId> { + let mut visited = HashMap::with_capacity(value.variants.len()); + value + .variants + .iter() + .find_map(|variant| { + visited + .insert(variant.id, variant.name) + .map(|other_name| { + DuplicateVariantId { + id: variant.id, + name: variant.name.to_owned(), + other_name: other_name.to_owned(), + } + .into() + }) + .or_else(|| { + validate_field_ids(&variant.fields) + .err() + .map(DuplicateId::from) + }) + }) + .map_or(Ok(()), Err) +} + +/// Ensure all field IDs of a struct or enum are unique. +fn validate_field_ids(value: &Fields) -> Result<(), DuplicateFieldId> { + match value { + Fields::Named(named) => { + let mut visited = HashMap::with_capacity(named.len()); + named + .iter() + .find_map(|field| { + visited.insert(field.id, field.name).map(|other_field| { + DuplicateFieldId::Named { + id: field.id, + name: field.name.to_owned(), + other_name: other_field.to_owned(), + } + }) + }) + .map_or(Ok(()), Err)?; + } + Fields::Unnamed(unnamed) => { + let mut visited = HashMap::with_capacity(unnamed.len()); + unnamed + .iter() + .enumerate() + .find_map(|(pos, field)| { + visited + .insert(field.id, pos) + .map(|other_position| DuplicateFieldId::Unnamed { + id: field.id, + position: pos, + other_position, + }) + }) + .map_or(Ok(()), Err)?; + } + Fields::Unit => {} + } + + Ok(()) +} diff --git a/crates/stef-compiler/src/lib.rs b/crates/stef-compiler/src/lib.rs index 8b13789..4c6c23c 100644 --- a/crates/stef-compiler/src/lib.rs +++ b/crates/stef-compiler/src/lib.rs @@ -1 +1,30 @@ +pub use ids::{DuplicateFieldId, DuplicateId, DuplicateVariantId}; +use stef_parser::{Definition, Schema}; +use thiserror::Error; +mod ids; + +#[derive(Debug, Error)] +pub enum Error { + #[error("duplicate ID found")] + DuplicateId(#[from] DuplicateId), +} + +impl From for Error { + fn from(v: DuplicateFieldId) -> Self { + Self::DuplicateId(v.into()) + } +} + +pub fn validate_schema(value: &Schema<'_>) -> Result<(), Error> { + value.definitions.iter().try_for_each(validate_definition) +} + +fn validate_definition(value: &Definition<'_>) -> Result<(), Error> { + match value { + Definition::Module(m) => m.definitions.iter().try_for_each(validate_definition), + Definition::Struct(s) => ids::validate_struct_ids(s).map_err(Into::into), + Definition::Enum(e) => ids::validate_enum_ids(e).map_err(Into::into), + Definition::TypeAlias(_) | Definition::Const(_) | Definition::Import(_) => Ok(()), + } +} diff --git a/crates/stef-parser/Cargo.toml b/crates/stef-parser/Cargo.toml index 03d264d..f03ef29 100644 --- a/crates/stef-parser/Cargo.toml +++ b/crates/stef-parser/Cargo.toml @@ -16,7 +16,7 @@ rustc-args = ["--cfg", "docsrs"] miette.workspace = true owo-colors = { version = "3.5.0", features = ["supports-colors"] } stef-derive = { path = "../stef-derive" } -winnow = "0.5.16" +winnow = "0.5.17" [dev-dependencies] indoc.workspace = true diff --git a/crates/stef-parser/src/lib.rs b/crates/stef-parser/src/lib.rs index 86abfc5..3ebde34 100644 --- a/crates/stef-parser/src/lib.rs +++ b/crates/stef-parser/src/lib.rs @@ -763,7 +763,7 @@ impl<'a> Display for Generics<'a> { /// ```txt /// @1 /// ``` -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Id(pub u32); impl Display for Id { diff --git a/crates/stef-playground/Cargo.toml b/crates/stef-playground/Cargo.toml index 0dbc0f6..2bfb7e5 100644 --- a/crates/stef-playground/Cargo.toml +++ b/crates/stef-playground/Cargo.toml @@ -14,4 +14,5 @@ publish = false stef = { path = "../stef" } [build-dependencies] +anyhow = "1.0.75" stef-build = { path = "../stef-build" } diff --git a/crates/stef-playground/build.rs b/crates/stef-playground/build.rs index e8e4c26..d6c27f9 100644 --- a/crates/stef-playground/build.rs +++ b/crates/stef-playground/build.rs @@ -1,4 +1,5 @@ -fn main() { - stef_build::compile(&["src/sample.stef"], &["src/"]).unwrap(); - stef_build::compile(&["schemas/*.stef"], &["schemas/"]).unwrap(); +fn main() -> anyhow::Result<()> { + stef_build::compile(&["src/sample.stef"], &["src/"])?; + stef_build::compile(&["schemas/*.stef"], &["schemas/"])?; + Ok(()) } diff --git a/crates/stef/src/buf/decode.rs b/crates/stef/src/buf/decode.rs index dc921e4..bc5096f 100644 --- a/crates/stef/src/buf/decode.rs +++ b/crates/stef/src/buf/decode.rs @@ -14,7 +14,7 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { - InsufficentData, + InsufficientData, DecodeInt(varint::DecodeIntError), NonUtf8(std::string::FromUtf8Error), MissingField { id: u32, name: Option<&'static str> }, @@ -39,7 +39,7 @@ pub const END_MARKER: u32 = 0; macro_rules! ensure_size { ($r:ident, $size:expr) => { if $r.remaining() < $size { - return Err(Error::InsufficentData); + return Err(Error::InsufficientData); } }; } @@ -155,7 +155,7 @@ where { let len = decode_u64(r)?; if (len as usize) < N { - return Err(Error::InsufficentData); + return Err(Error::InsufficientData); } let buf = (0..N).map(|_| decode(r)).collect::>>()?;