diff --git a/OVERVIEW.md b/OVERVIEW.md index b77cd98..bfc1678 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -106,6 +106,9 @@ A reference is a Bauble object which may not be present in the current module. A Bauble object's value may use a reference to avoid code duplication in the local file (referencing a previous object to use its values), or to reference the value of a Bauble object from a different file (module). References are specified using a bauble `Path`. +Bauble does expose the builtin type for references, [`Ref`], which can be used for convenience to represent references from Bauble in Rust. +It is not required to use this type to represent references, custom types which are capable of parsing reference values are equal to the builtin [`Ref`] type, it is just a convenience. + # Sub-objects A single Bauble object may contain sub-objects. diff --git a/bauble/src/builtin.rs b/bauble/src/builtin.rs new file mode 100644 index 0000000..da2d321 --- /dev/null +++ b/bauble/src/builtin.rs @@ -0,0 +1,88 @@ +use crate::{ + Bauble, BaubleAllocator, ToRustErrorKind, Val, Value, + path::TypePath, + types::{Type, TypeRegistry}, +}; +use std::{cell::UnsafeCell, fmt::Debug, marker::PhantomData}; + +/// The builtin reference type. +/// +/// This corresponds to the type used internally by Bauble for objects which are +/// references. That is to say, if you were to write +/// ```ignore +/// own: MyType = MyType { ... } +/// ref = $own +/// ``` +/// then `ref` has the type `Ref` and the value `own`. +/// +/// This type is not required for parsing or using references. Custom types like +/// `MyRefType` can still be made to be used to parse various references and be +/// used in Bauble. This is a convenience type and is capable of handling various +/// references without needing to be registered to Bauble manually. Besides +/// convenience this type offers no advantage, and if special behaviour is requred +/// for parsing and handling references, prefer to use a custom type from Rust. +/// +/// `S` is the inner representation used for `TypePath`. +pub struct Ref { + /// The path to the referenced asset. + pub path: TypePath, + /// Invariant over `T`. + _mark: PhantomData>, +} + +impl Ref { + /// Create a reference from the specified path. The path must not be valid. + pub fn from_path(path: TypePath) -> Self { + Self { + path, + _mark: PhantomData, + } + } +} + +impl PartialEq for Ref { + fn eq(&self, other: &Self) -> bool { + self.path == other.path + } +} +impl Eq for Ref {} + +impl Debug for Ref { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.path, f) + } +} + +impl<'b, A: BaubleAllocator<'b>, T: Bauble<'b, A>> Bauble<'b, A> for Ref { + fn builtin(registry: &mut TypeRegistry) -> Option { + let inner = registry.get_or_register_type::(); + Some(registry.get_or_register_asset_ref(inner)) + } + + fn construct_type(_registry: &mut TypeRegistry) -> Type { + unreachable!("This is a builtin and should never be constructed."); + } + + fn from_bauble( + val: Val, + allocator: &A, + ) -> Result<>::Out, crate::ToRustError> { + match val.value.value { + Value::Ref(r) => Ok({ + let path = allocator.wrap_type_path(r); + let value = Self { + // SAFETY: path was derived from `allocator`. + path: unsafe { allocator.validate(path)? }, + _mark: PhantomData, + }; + + // SAFETY: allocator has wrapped the type path. + unsafe { allocator.wrap(value) } + }), + _ => Err(Self::error( + val.value.span, + ToRustErrorKind::WrongType { found: val.ty }, + )), + } + } +} diff --git a/bauble/src/context.rs b/bauble/src/context.rs index 64bce40..cb391f5 100644 --- a/bauble/src/context.rs +++ b/bauble/src/context.rs @@ -1,10 +1,9 @@ -use indexmap::IndexMap; - use crate::{ Bauble, BaubleAllocator, BaubleErrors, path::{TypePath, TypePathElem}, types::{BaubleTrait, TypeId, TypeRegistry}, }; +use indexmap::IndexMap; #[allow(missing_docs)] pub type Source = ariadne::Source; @@ -503,6 +502,7 @@ impl BaubleContext { /// Returns ID of internal Ref type for `ty`. pub fn register_asset(&mut self, path: TypePath<&str>, ty: TypeId) -> TypeId { let ref_ty = self.registry.get_or_register_asset_ref(ty); + self.root_node.build_type(ref_ty, &self.registry); self.root_node.build_asset(path, ref_ty); ref_ty } diff --git a/bauble/src/lib.rs b/bauble/src/lib.rs index 81122b8..e9ffda4 100644 --- a/bauble/src/lib.rs +++ b/bauble/src/lib.rs @@ -2,6 +2,7 @@ #![feature(iterator_try_collect, let_chains, ptr_metadata)] #![warn(missing_docs)] +mod builtin; mod context; mod error; mod parse; @@ -13,6 +14,7 @@ pub mod types; pub use bauble_macros::Bauble; +pub use builtin::Ref; pub use context::{BaubleContext, BaubleContextBuilder, FileId, PathReference, Source}; pub use error::{BaubleError, BaubleErrors, CustomError, Level, print_errors}; pub use spanned::{Span, SpanExt, Spanned}; @@ -39,9 +41,12 @@ pub mod private { #[macro_export] macro_rules! bauble_test { ( [$($ty:ty),* $(,)?] $source:literal [$($test_value:expr),* $(,)?]) => { - $crate::bauble_test!(__TEST_CTX [$($ty),*] $source [$($test_value),*]) + { $crate::bauble_test!(__TEST_CTX [$($ty),*] [$source] [$($test_value),*]); } }; - ($ctx_static:ident [$($ty:ty),* $(,)?] $source:literal [$($test_value:expr),* $(,)?]) => { + ( [$($ty:ty),* $(,)?] [$($source:literal),* $(,)?] [$($test_value:expr),* $(,)?]) => { + { $crate::bauble_test!(__TEST_CTX [$($ty),*] [$($source),*] [$($test_value),*]); } + }; + ($ctx_static:ident [$($ty:ty),* $(,)?] [$($source:literal),* $(,)?] [$($test_value:expr),* $(,)?]) => { static $ctx_static: std::sync::OnceLock> = std::sync::OnceLock::new(); { let file_path = $crate::path::TypePath::new("test").unwrap(); @@ -52,7 +57,9 @@ macro_rules! bauble_test { let mut ctx = ctx.build(); ctx.type_registry().validate(true).expect("Invalid type registry"); - ctx.register_file(file_path, format!("\n{}\n", $source)); + $( + ctx.register_file(file_path, format!("\n{}\n", $source)); + )* std::sync::RwLock::new(ctx) }); diff --git a/bauble/src/traits.rs b/bauble/src/traits.rs index 3280629..b134bf7 100644 --- a/bauble/src/traits.rs +++ b/bauble/src/traits.rs @@ -243,12 +243,18 @@ pub trait BaubleAllocator<'a> { where Self: 'a; + /// The inner representation of a type path created by this allocator. + type TypePathInner: 'static; + /// # Safety /// Allocations in `value` have to be allocated with this allocator unsafe fn wrap(&self, value: T) -> Self::Out; /// # Safety /// If validated an item must be placed within the same allocator. unsafe fn validate(&self, value: Self::Out) -> Result; + + /// Create a type path from this allocator. + fn wrap_type_path(&self, path: TypePath) -> Self::Out>; } /// The standard Rust allocator when used by Bauble. @@ -256,6 +262,7 @@ pub struct DefaultAllocator; impl BaubleAllocator<'_> for DefaultAllocator { type Out = T; + type TypePathInner = String; unsafe fn wrap(&self, value: T) -> Self::Out { value @@ -264,10 +271,22 @@ impl BaubleAllocator<'_> for DefaultAllocator { unsafe fn validate(&self, value: Self::Out) -> Result { Ok(value) } + + fn wrap_type_path(&self, path: TypePath) -> TypePath { + path + } } /// The trait used by types usable by Bauble. Any type that can be parsed by bauble should implement this trait. pub trait Bauble<'a, A: BaubleAllocator<'a> = DefaultAllocator>: Sized + 'static { + /// # DON'T CALL THIS, call `TypeRegistry::get_or_register_type` instead. + /// + /// Constructs a builtin type. A builtin type is a type which is not constructed by Bauble during context + /// construction and might warrant additional rules. + fn builtin(#[expect(unused)] registry: &mut types::TypeRegistry) -> Option { + None + } + /// # DON'T CALL THIS, call `TypeRegistry::get_or_register_type` instead. /// /// Constructs a reflection type that bauble uses to parse and resolve types. diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 430f35a..32bdbe6 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -420,13 +420,14 @@ impl TypeRegistry { ) } + /// If `inner` is a reference, then this function should return the ID of `inner`. pub(crate) fn get_or_register_asset_ref(&mut self, inner: TypeId) -> TypeId { if let Some(id) = self.asset_refs.get(&inner) { *id } else { self.register_type(|this, id| { this.asset_refs.insert(inner, id); - let mut ty = Type { + let ty = Type { meta: TypeMeta { path: TypePath::new(format!("Ref<{}>", this.key_type(inner).meta.path)) .expect("Invariant"), @@ -436,14 +437,14 @@ impl TypeRegistry { kind: TypeKind::Ref(inner), }; - this.on_register_type(id, &mut ty); + this.on_register_type(id, &ty); ty }) } } - fn on_register_type(&mut self, id: TypeId, ty: &mut Type) { + fn on_register_type(&mut self, id: TypeId, ty: &Type) { for tr in ty.meta.traits.iter() { let TypeKind::Trait(types) = &mut self.types[tr.0.0].kind else { panic!("Invariant") @@ -527,9 +528,9 @@ impl TypeRegistry { /// Makes it possible to register a type which is not represented in Rust. #[must_use] - pub fn register_dummy_type(&mut self, mut ty: Type) -> TypeId { + pub fn register_dummy_type(&mut self, ty: Type) -> TypeId { self.register_type(|this, id| { - this.on_register_type(id, &mut ty); + this.on_register_type(id, &ty); ty }) @@ -567,9 +568,9 @@ impl TypeRegistry { continue; } - let object_name = - TypePathElem::new(format!("{}_{i}", ty.meta.path.get_end().unwrap().1)) - .unwrap(); + let path_end = ty.meta.path.get_end().unwrap().1; + let object_path = path_end.strip_generic().append(&format!("_{i}")).unwrap(); + let object_name = object_path.get_end().unwrap().1; let object_path = file.join(&object_name); @@ -666,12 +667,16 @@ impl TypeRegistry { /// Register `T` if it is not registerted already, then get the type ID for `T`. pub fn get_or_register_type<'a, T: Bauble<'a, A>, A: BaubleAllocator<'a>>(&mut self) -> TypeId { + if let Some(id) = T::builtin(self) { + return id; + } + let id = self.type_id::(); match id { Some(id) if self.to_be_assigned.remove(&id) => { - let mut ty = T::construct_type(self); - self.on_register_type(id, &mut ty); + let ty = T::construct_type(self); + self.on_register_type(id, &ty); self.types[id.0] = ty; id @@ -679,8 +684,8 @@ impl TypeRegistry { Some(id) => id, None => self.register_type(|this, id| { this.type_from_rust.insert(std::any::TypeId::of::(), id); - let mut ty = T::construct_type(this); - this.on_register_type(id, &mut ty); + let ty = T::construct_type(this); + this.on_register_type(id, &ty); ty }), @@ -873,7 +878,6 @@ impl TypeRegistry { (TypeKind::Trait(types), _) => types.contains(input_id), (TypeKind::Ref(a), TypeKind::Ref(b)) => self.can_infer_from(*a, *b), (TypeKind::Ref(t), _) => self.can_infer_from(*t, input_id), - (TypeKind::Primitive(a), TypeKind::Primitive(b)) => a == b, (_, TypeKind::Generic(types)) => types.contains(output_id), diff --git a/bauble/src/types/path.rs b/bauble/src/types/path.rs index 8b3382a..b5e5ef2 100644 --- a/bauble/src/types/path.rs +++ b/bauble/src/types/path.rs @@ -415,6 +415,10 @@ impl> TypePath { == Some(PATH_SEPERATOR)) } + pub fn is_generic(&self) -> bool { + self.0.as_ref().ends_with('>') + } + /// Returns true if this type path, referencing a type, is something that is /// allowed to be parsed by Bauble. /// @@ -487,6 +491,42 @@ impl> TypePath { pub fn is_subobject(&self) -> bool { self.iter().any(|part| part.as_str().contains('@')) } + + /// Appends `end` onto `self`. + /// + /// Appending is different from `join` in that it will not insert a separator, + /// and when dealing with generic, `append` will insert `end` before the + /// generic argument but onto the last identifier before `<`. + /// + /// Performs path validation on the returned value. + pub fn append(&self, end: &str) -> Result> { + if self.is_generic() { + let index = self + .as_str() + .find('<') + .expect("generic should always have '<'"); + let mut string = self.to_string(); + string.insert_str(index, end); + TypePath::new(string) + } else { + TypePath::new(format!("{self}{end}")) + } + } + + /// Returns a variant of `self` with no generics at the end. + pub fn strip_generic(self) -> TypePath { + if self.is_generic() { + let index = self + .0 + .as_ref() + .find('<') + .expect("generic should always have '<'"); + // No need to check since nothing is added onto the path. + TypePath::new_unchecked(self.0.as_ref().split_at(index).0.to_string()) + } else { + TypePath::new_unchecked(self.as_str().to_string()) + } + } } impl<'a> TypePath<&'a str> { diff --git a/bauble/src/value/convert.rs b/bauble/src/value/convert.rs index e7e0af1..34f5102 100644 --- a/bauble/src/value/convert.rs +++ b/bauble/src/value/convert.rs @@ -1072,7 +1072,7 @@ where Value::Or(variants) } - // A ref can be from a ref value. + // A ref can convert from a ref value. (types::TypeKind::Ref(_), Value::Ref(r)) => { Value::Ref(Self::get_asset(r, meta.symbols)?) } diff --git a/bauble/src/value/mod.rs b/bauble/src/value/mod.rs index 2f81af4..8efcd21 100644 --- a/bauble/src/value/mod.rs +++ b/bauble/src/value/mod.rs @@ -665,6 +665,8 @@ impl PathKind { pub struct DelayedRegister { pub asset: Spanned, pub reference: Spanned, + /// The type we want a potential reference to resolve into. + pub expected_ty_path: Option>, } pub(crate) fn resolve_delayed( @@ -672,6 +674,7 @@ pub(crate) fn resolve_delayed( ctx: &mut crate::context::BaubleContext, ) -> std::result::Result<(), Vec>> { loop { + let mut errors = Vec::new(); let old_len = delayed.len(); // Try to register delayed registers, and remove them as they succeed. @@ -685,6 +688,54 @@ pub(crate) fn resolve_delayed( } } && let Some((ty, _)) = &r.asset { + // TODO: for now, it is assumed all references which explicitly + // specify their inner type should have that inner type resolved + // by this point. If that is not the case, this should be a + // nested reference, and in the case it is a nested reference + // this code can optionally work or not work, depending on the + // order the references appear in Bauble. This is unpredictable + // and weird, it is likely best to simply error in general if it + // is noticed that nested references are explictly being written + // in bauble at all, and they should instead prefer having their + // types implicitly solved. + if let Some(desired_ty) = &d.expected_ty_path { + let span = desired_ty.span; + let path = &desired_ty.value; + let Some(desired_ty) = (match path { + PathKind::Direct(path) => ctx.get_ref(path.borrow()), + PathKind::Indirect(path, ident) => { + ctx.ref_with_ident(path.borrow(), ident.borrow()) + } + }) else { + errors.push( + ConversionError::Custom(crate::CustomError::new(format!( + "Invalid explicit reference path '{path}'" + ))) + .spanned(span), + ); + return false; + }; + + let Some(desired_ty) = desired_ty.ty else { + errors.push( + ConversionError::Custom(crate::CustomError::new(format!( + "Expected path to refer to type '{path}'", + ))) + .spanned(span), + ); + return false; + }; + + if desired_ty != *ty { + errors.push( + ConversionError::ExpectedExactType { + expected: desired_ty, + got: Some(*ty), + } + .spanned(span), + ); + } + } ctx.register_asset(d.asset.value.borrow(), *ty); false } else { @@ -692,6 +743,10 @@ pub(crate) fn resolve_delayed( } }); + if !errors.is_empty() { + return Err(errors); + } + if delayed.is_empty() { return Ok(()); } @@ -711,7 +766,6 @@ pub(crate) fn resolve_delayed( } } - let mut errors = Vec::new(); for scc in petgraph::algo::tarjan_scc(&graph) { if scc.len() == 1 { if map[&scc[0]].could_be(scc[0].borrow()) { @@ -788,7 +842,17 @@ pub(crate) fn register_assets( let symbols = Symbols { ctx: &*ctx, uses }; // To register an asset we need to determine its type. - let ty = if let Some(ty) = &binding.type_path { + let ty = if let Some(ty) = &binding.type_path + // If the value is a reference, then an explicit type should + // still be delayed. The reason why it should be delayed is + // because the type of the reference `Ref`, where `T` is + // the type of the referenced object, may not exist at this + // point, and as such trying to resolve the type may cause + // issues because the type is not known and depends on the + // order the reference was created compared to the + // referenced object, which is undesired behaviour. + && !matches!(binding.value.value.value, Value::Ref(_)) + { symbols.resolve_type(ty) } else { let res = value_type(&binding.value, &symbols) @@ -815,7 +879,20 @@ pub(crate) fn register_assets( && let Value::Ref(reference) = &*binding.value.value && let Ok(reference) = symbols.resolve_path(reference) { + let expected_ty_path = if let Some(expected_ty_path) = &binding.type_path { + match symbols.resolve_path(expected_ty_path) { + Ok(s) => Some(s), + Err(e) => { + errors.push(e); + None + } + } + } else { + None + }; + delayed.push(DelayedRegister { + expected_ty_path, asset: path.spanned(span), reference, }); @@ -1101,7 +1178,6 @@ fn convert_object( mut meta: ConvertMeta, ) -> Result { let value = value.convert(meta.reborrow(), expected_type, no_attr())?; - create_object(path, meta.object_name, value, meta.symbols) } diff --git a/bauble/tests/integration.rs b/bauble/tests/integration.rs index 7ceb0ab..f1f0e11 100644 --- a/bauble/tests/integration.rs +++ b/bauble/tests/integration.rs @@ -2,6 +2,7 @@ use bauble::Bauble; use bauble::BaubleContext; use bauble::Object; +use bauble::Ref; use bauble::path::{TypePath, TypePathElem}; #[derive(Bauble, PartialEq, Debug)] @@ -377,3 +378,122 @@ fn reference_with_use() { &[a, b], ); } + +#[test] +pub fn ref_implicit_type() { + bauble::bauble_test!( + [Test] + "test = integration::Test{ x: -5, y: 5 }\n\ + r = $test" + [ + Test { x: -5, y: 5 }, + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + ] + ); + + bauble::bauble_test!( + [Test] + "r = $test::test\n\ + test = integration::Test{ x: -5, y: 5 }" + [ + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + Test { x: -5, y: 5 }, + ] + ); +} + +#[test] +pub fn ref_explicit_type() { + bauble::bauble_test!( + [Test] + "test = integration::Test{ x: -2, y: 2 }\n\ + r: Ref = $test" + [ + Test { x: -2, y: 2 }, + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + ] + ); + + bauble::bauble_test!( + [Test] + "r: Ref = $test::test\n\ + test = integration::Test{ x: -2, y: 2 }" + [ + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + Test { x: -2, y: 2 }, + ] + ); +} + +#[test] +pub fn ref_explicit_type_multiple_files() { + bauble::bauble_test!( + [Test] + [ + "test = integration::Test{ x: -5, y: 5 }", + "r: Ref = $test::test" + ] + [ + Test { x: -5, y: 5 }, + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + ] + ); + + bauble::bauble_test!( + [Test] + [ + "r: Ref = $test::test", + "test = integration::Test{ x: -5, y: 5 }" + ] + [ + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + Test { x: -5, y: 5 }, + ] + ); +} + +#[test] +pub fn ref_implicit_type_multiple_files() { + bauble::bauble_test!( + [Test] + [ + "test = integration::Test{ x: -5, y: 5 }", + "r = $test::test" + ] + [ + Test { x: -5, y: 5 }, + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + ] + ); + + bauble::bauble_test!( + [Test] + [ + "r = $test::test", + "test = integration::Test{ x: -5, y: 5 }" + ] + [ + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + Test { x: -5, y: 5 }, + ] + ); +} + +#[test] +#[should_panic] +pub fn ref_explicit_type_incorrect() { + #[derive(Bauble, PartialEq, Eq, Debug)] + struct Incorrect(u32); + + bauble::bauble_test!( + [Test, Incorrect] + "i: Incorrect = Incorrect(0)\n\ + r: Ref = $test::test\n\ + test = integration::Test{ x: -2, y: 2 }" + [ + Incorrect(0), + Ref::::from_path(TypePath::new_unchecked("test::test").to_owned()), + Test { x: -2, y: 2 }, + ] + ); +} diff --git a/bauble_macros/tests/derive.rs b/bauble_macros/tests/derive.rs index 9ac960e..dc86044 100644 --- a/bauble_macros/tests/derive.rs +++ b/bauble_macros/tests/derive.rs @@ -145,39 +145,6 @@ fn test_std_types() { ); } -#[test] -fn test_complex_flatten() { - #[derive(Bauble, PartialEq, Debug)] - #[bauble(flatten)] - struct Inner( - u32, - #[bauble(attribute = a, default)] u32, - #[bauble(attribute = b)] u32, - ); - - #[derive(Bauble, PartialEq, Debug)] - #[bauble(flatten)] - struct Transparent(Inner, #[bauble(attribute = a)] u32); - - bauble_test!( - [Transparent] - r#" - a: derive::Transparent = #[a = 1, b = 2] 3 - - copy t = #[a = 2] 1 - - // Since `b` isn't an attribute on `Transparent` that gets passed to `Inner`. And - // since we already have `a` defined here, the `a` attribute on `copy t` gets - // assigned to `Inner.1`. - b: derive::Transparent = #[a = 4, b = 3] $t - "# - [ - Transparent(Inner(3, 0, 2), 1), - Transparent(Inner(1, 2, 3), 4), - ] - ); -} - #[test] fn test_from() { #[derive(Bauble, PartialEq, Debug)] @@ -368,13 +335,13 @@ fn test_trait() { bauble_test!( TEST_CTX [Trans, Foo, Bar] - r#" + [r#" use derive::{Trans, Foo, Bar}; a: Trans = Foo(32) b: Trans = "meow" - "# + "#] [Trans(Box::new(Foo(32))), Trans(Box::new(Bar("meow".to_string())))] ); }