Skip to content

Commit

Permalink
Improve SID generation
Browse files Browse the repository at this point in the history
It's now OS-independent and orders by type name automatically.
  • Loading branch information
oscartbeaumont committed Sep 4, 2023
1 parent d59fae0 commit 953f9b7
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 122 deletions.
8 changes: 5 additions & 3 deletions macros/src/type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result<proc_macro::TokenSt
None => quote!(None),
};

let sid = quote!(#crate_ref::internal::construct::sid(#name, concat!("::", module_path!(), ":", line!(), ":", column!())));
let impl_location = quote!(#crate_ref::internal::construct::impl_location(concat!(file!(), ":", line!(), ":", column!())));

Ok(quote! {
const _: () = {
// We do this so `sid!()` is only called once, as it does a hashing operation.
const SID: #crate_ref::SpectaID = #crate_ref::sid!(@with_specta_path; #name; #crate_ref);
const IMPL_LOCATION: #crate_ref::ImplLocation = #crate_ref::impl_location!(@with_specta_path; #crate_ref);
const SID: #crate_ref::SpectaID = #sid;
const IMPL_LOCATION: #crate_ref::ImplLocation = #impl_location;

#[automatically_derived]
#type_impl_heading {
Expand Down
31 changes: 31 additions & 0 deletions src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,35 @@ pub mod construct {
pub const fn tuple(fields: Vec<DataType>) -> TupleType {
TupleType { fields }
}

pub const fn impl_location(loc: &'static str) -> ImplLocation {
ImplLocation(loc)
}

/// Compute an SID hash for a given type.
/// This will produce a type hash from the arguments.
/// This hashing function was derived from https://stackoverflow.com/a/71464396
pub const fn sid(type_name: &'static str, type_identifier: &'static str) -> SpectaID {
let mut hash = 0xcbf29ce484222325;
let prime = 0x00000100000001B3;

let mut bytes = type_name.as_bytes();
let mut i = 0;

while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}

bytes = type_identifier.as_bytes();
i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}

SpectaID { type_name, hash }
}
}
106 changes: 2 additions & 104 deletions src/type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use crate::*;
mod macros;
mod impls;
mod post_process;
mod specta_id;

pub use post_process::*;
pub use specta_id::*;

use self::reference::Reference;

Expand Down Expand Up @@ -125,107 +127,3 @@ pub mod reference {

/// A marker trait for compile-time validation of which types can be flattened.
pub trait Flatten: Type {}

/// The unique Specta ID for the type.
///
/// Be aware type aliases don't exist as far as Specta is concerned as they are flattened into their inner type by Rust's trait system.
/// The Specta Type ID holds for the given properties:
/// - `T::SID == T::SID`
/// - `T::SID != S::SID`
/// - `Type<T>::SID == Type<S>::SID` (unlike std::any::TypeId)
/// - `Box<T> == Arc<T> == Rc<T>` (unlike std::any::TypeId)
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[doc(hidden)]
pub struct SpectaID(u64);

/// Compute an SID hash for a given type.
/// This hash function comes from https://stackoverflow.com/a/71464396
/// You should NOT use this directly. Rely on `sid!();` instead.
#[doc(hidden)]
pub const fn internal_sid_hash(
module_path: &'static str,
file: &'static str,
// This is required for a unique hash because all impls generated by a `macro_rules!` will have an identical `module_path` and `file` value.
type_name: &'static str,
) -> SpectaID {
let mut hash = 0xcbf29ce484222325;
let prime = 0x00000100000001B3;

let mut bytes = module_path.as_bytes();
let mut i = 0;

while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}

bytes = file.as_bytes();
i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}

bytes = type_name.as_bytes();
i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}

SpectaID(hash)
}

/// Compute an SID hash for a given type.
#[macro_export]
#[doc(hidden)]
macro_rules! sid {
($name:expr) => {
$crate::sid!($name, $crate::impl_location!().as_str())
};
// Using `$crate_path:path` here does not work because: https://github.com/rust-lang/rust/issues/48067
(@with_specta_path; $name:expr; $first:ident$(::$rest:ident)*) => {{
use $first$(::$rest)*::{internal_sid_hash, impl_location};

internal_sid_hash(
module_path!(),
impl_location!().as_str(),
$name,
)
}};
}

/// The location of the impl block for a given type. This is used for error reporting.
/// The content of it is transparent and should be generated by the `impl_location!` macro.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[doc(hidden)]
pub struct ImplLocation(&'static str);

impl ImplLocation {
#[doc(hidden)]
pub const fn internal_new(s: &'static str) -> Self {
Self(s)
}

/// Get the location as a string
pub const fn as_str(&self) -> &'static str {
self.0
}
}

/// Compute the location for an impl block
#[macro_export]
#[doc(hidden)]
macro_rules! impl_location {
() => {
$crate::ImplLocation::internal_new(concat!(file!(), ":", line!(), ":", column!()))
};
// Using `$crate_path:path` here does not work because: https://github.com/rust-lang/rust/issues/48067
(@with_specta_path; $first:ident$(::$rest:ident)*) => {
$first$(::$rest)*::ImplLocation::internal_new(concat!(file!(), ":", line!(), ":", column!()))
};
}
55 changes: 55 additions & 0 deletions src/type/specta_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::cmp::Ordering;

/// The unique Specta ID for the type.
///
/// Be aware type aliases don't exist as far as Specta is concerned as they are flattened into their inner type by Rust's trait system.
/// The Specta Type ID holds for the given properties:
/// - `T::SID == T::SID`
/// - `T::SID != S::SID`
/// - `Type<T>::SID == Type<S>::SID` (unlike std::any::TypeId)
/// - `&'a T::SID == &'b T::SID` (unlike std::any::TypeId which forces a static lifetime)
/// - `Box<T> == Arc<T> == Rc<T>` (unlike std::any::TypeId)
///
#[derive(Debug, Clone, Copy)]
pub struct SpectaID {
pub(crate) type_name: &'static str,
pub(crate) hash: u64,
}

// We do custom impls so the order prefers type_name over hash.
impl Ord for SpectaID {
fn cmp(&self, other: &Self) -> Ordering {
self.type_name
.cmp(other.type_name)
.then(self.hash.cmp(&other.hash))
}
}

// We do custom impls so the order prefers type_name over hash.
impl PartialOrd<Self> for SpectaID {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

// We do custom impls so equals is by SID exclusively.
impl Eq for SpectaID {}

// We do custom impls so equals is by SID exclusively.
impl PartialEq<Self> for SpectaID {
fn eq(&self, other: &Self) -> bool {
self.hash.eq(&other.hash)
}
}

/// The location of the impl block for a given type. This is used for error reporting.
/// The content of it is transparent and is generated by the macros.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImplLocation(pub(crate) &'static str);

impl ImplLocation {
/// Get the location as a string
pub const fn as_str(&self) -> &'static str {
self.0
}
}
13 changes: 8 additions & 5 deletions tests/duplicate_ty_name.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use specta::{
ts::{export, TsExportError},
ImplLocation, Type,
Type,
};

mod one {
Expand Down Expand Up @@ -33,17 +33,20 @@ pub struct Demo {

#[test]
fn test_duplicate_ty_name() {
// DO NOT COPY THIS. This is a hack to construct the impl locations but IS NOT STABLE.
use specta::internal::construct::impl_location;

#[cfg(not(target_os = "windows"))]
let err = Err(TsExportError::DuplicateTypeName(
"One".into(),
ImplLocation::internal_new("tests/duplicate_ty_name.rs:19:14"),
ImplLocation::internal_new("tests/duplicate_ty_name.rs:9:14"),
impl_location("tests/duplicate_ty_name.rs:19:14"),
impl_location("tests/duplicate_ty_name.rs:9:14"),
));
#[cfg(target_os = "windows")]
let err = Err(TsExportError::DuplicateTypeName(
"One".into(),
ImplLocation::internal_new("tests\\duplicate_ty_name.rs:9:14"),
ImplLocation::internal_new("tests\\duplicate_ty_name.rs:19:14"),
impl_location("tests\\duplicate_ty_name.rs:9:14"),
impl_location("tests\\duplicate_ty_name.rs:19:14"),
));

assert_eq!(export::<Demo>(&Default::default()), err);
Expand Down
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod rename;
mod reserved_keywords;
mod selection;
mod serde;
mod sid;
mod transparent;
pub mod ts;
mod ts_rs;
Expand Down
20 changes: 10 additions & 10 deletions tests/macro/compile_error.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ error[E0277]: the trait bound `UnitExternal: specta::Flatten` is not satisfied
toml::map::Map<K, V>
FlattenExternal
toml_datetime::datetime::Datetime
toml_datetime::datetime::Date
FlattenUntagged
toml_datetime::datetime::Date
toml_datetime::datetime::Time
toml_datetime::datetime::Offset
FlattenInternal
toml_datetime::datetime::Offset
and $N others
note: required by a bound in `_::<impl specta::Type for FlattenExternal>::inline::validate_flatten`
--> tests/macro/compile_error.rs:29:10
Expand All @@ -79,11 +79,11 @@ error[E0277]: the trait bound `UnnamedMultiExternal: specta::Flatten` is not sat
toml::map::Map<K, V>
FlattenExternal
toml_datetime::datetime::Datetime
toml_datetime::datetime::Date
FlattenUntagged
toml_datetime::datetime::Date
toml_datetime::datetime::Time
toml_datetime::datetime::Offset
FlattenInternal
toml_datetime::datetime::Offset
and $N others
note: required by a bound in `_::<impl specta::Type for FlattenExternal>::inline::validate_flatten`
--> tests/macro/compile_error.rs:29:10
Expand All @@ -102,11 +102,11 @@ error[E0277]: the trait bound `UnnamedUntagged: specta::Flatten` is not satisfie
toml::map::Map<K, V>
FlattenExternal
toml_datetime::datetime::Datetime
toml_datetime::datetime::Date
FlattenUntagged
toml_datetime::datetime::Date
toml_datetime::datetime::Time
toml_datetime::datetime::Offset
FlattenInternal
toml_datetime::datetime::Offset
and $N others
note: required by a bound in `_::<impl specta::Type for FlattenUntagged>::inline::validate_flatten`
--> tests/macro/compile_error.rs:49:10
Expand All @@ -125,11 +125,11 @@ error[E0277]: the trait bound `UnnamedMultiUntagged: specta::Flatten` is not sat
toml::map::Map<K, V>
FlattenExternal
toml_datetime::datetime::Datetime
toml_datetime::datetime::Date
FlattenUntagged
toml_datetime::datetime::Date
toml_datetime::datetime::Time
toml_datetime::datetime::Offset
FlattenInternal
toml_datetime::datetime::Offset
and $N others
note: required by a bound in `_::<impl specta::Type for FlattenUntagged>::inline::validate_flatten`
--> tests/macro/compile_error.rs:49:10
Expand All @@ -148,11 +148,11 @@ error[E0277]: the trait bound `UnnamedInternal: specta::Flatten` is not satisfie
toml::map::Map<K, V>
FlattenExternal
toml_datetime::datetime::Datetime
toml_datetime::datetime::Date
FlattenUntagged
toml_datetime::datetime::Date
toml_datetime::datetime::Time
toml_datetime::datetime::Offset
FlattenInternal
toml_datetime::datetime::Offset
and $N others
note: required by a bound in `_::<impl specta::Type for FlattenInternal>::inline::validate_flatten`
--> tests/macro/compile_error.rs:67:10
Expand Down
47 changes: 47 additions & 0 deletions tests/sid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use specta::{DefOpts, Type, TypeMap};

#[derive(Type)]
#[specta(export = false)]
pub struct A {}

#[derive(Type)]
#[specta(export = false)]
pub struct B {}

#[derive(Type)]
#[specta(export = false)]
pub struct C {}

#[derive(Type)]
#[specta(export = false)]
pub struct Z {}

#[derive(Type)]
#[specta(export = false)]
pub struct BagOfTypes {
// Fields are outta order intentionally so we don't fluke the test
a: A,
z: Z,
b: B,
c: C,
}

#[test]
fn test_sid() {
// TODO: This is so hard for an end-user to work with. Add some convenience API's!!!
let mut type_map = TypeMap::default();
// We are calling this for it's side-effects
BagOfTypes::definition(DefOpts {
parent_inline: false,
type_map: &mut type_map,
});

// `TypeMap` is a `BTreeMap` so it's sorted by SID. It should be sorted alphabetically by name
assert_eq!(
type_map
.into_iter()
.filter_map(|(_, t)| t.map(|t| t.name().clone()))
.collect::<Vec<_>>(),
["A", "B", "C", "Z"]
);
}

0 comments on commit 953f9b7

Please sign in to comment.