From 6cab55a0e0216f72cb21f9b1f3d4d3d6129eb56f Mon Sep 17 00:00:00 2001 From: Imbris Date: Mon, 1 Jun 2026 11:35:48 -0400 Subject: [PATCH 1/8] Add DefaultMaker to enforce type ID assignment when creating default values Other changes: - Fix a bug in type validation causing all types to be skipped since the default object_trait_dependency is the any trait which is not explicitly listed in the traits list for each type. - Fix a bug where transparent types could have an infinite loop in `instantiate` if they are wrapping a trait type and implement the trait themselves. - Fix a bug in the logic that determines whether an explicit type needs to be written out for a binding. This was skipping the explicit type for `Option`, but there are cases where the value does not hint at the type (i.e. `Option::None` contains no indication of what the generic type parameter should be). - Document that `instantiate` will always produce a value with a type ID that is for a `TypeKind::instanciable` type. --- bauble/src/traits.rs | 10 +++--- bauble/src/types.rs | 61 +++++++++++++++++++++++++++++++++--- bauble/src/value/convert.rs | 2 +- bauble/src/value/display.rs | 22 +++++++------ bauble_macro_util/src/lib.rs | 2 +- 5 files changed, 77 insertions(+), 20 deletions(-) diff --git a/bauble/src/traits.rs b/bauble/src/traits.rs index 028fd01..ae7eb58 100644 --- a/bauble/src/traits.rs +++ b/bauble/src/traits.rs @@ -7,7 +7,7 @@ use crate::{ object_path::ObjectPath, path::{TypePath, TypePathElem}, spanned::{Span, Spanned}, - types::{self, Extra, FieldType, TypeId}, + types::{self, DefaultMaker, Extra, FieldType, TypeId}, value::{Ident, PrimitiveValue}, }; @@ -312,7 +312,9 @@ impl<'a, A: BaubleAllocator<'a>> Bauble<'a, A> for Val { meta: types::TypeMeta { path: TypePath::new("bauble::Val").unwrap().to_owned(), attributes: types::NamedFields::any(), - default: Some(|_, _, _| UnspannedVal::new(Value::default())), + default: Some(DefaultMaker::new(|_, _, _| { + UnspannedVal::new(Value::default()) + })), ..Default::default() }, kind: types::TypeKind::Primitive(types::Primitive::Any), @@ -650,7 +652,7 @@ impl<'a, A: BaubleAllocator<'a>, T: Bauble<'a, A>> Bauble<'a, A> for Option { )) .unwrap(), generic_base_type: Some(generic), - default: Some(|a, registry, ty| { + default: Some(DefaultMaker::new(|a, registry, ty| { let none = match ®istry.key_type(ty).kind { types::TypeKind::Enum { variants } => variants .get("None") @@ -667,7 +669,7 @@ impl<'a, A: BaubleAllocator<'a>, T: Bauble<'a, A>> Bauble<'a, A> for Option { .expect("We should be able to instantiate unit fields"), ), )) - }), + })), ..Default::default() }, kind: types::TypeKind::Enum { variants }, diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 4e9dc4f..3af16d4 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -403,7 +403,7 @@ impl TypeRegistry { attributes: variant.attributes, extra: variant.extra, extra_validation: variant.extra_validation, - default: variant.default, + default: variant.default.map(DefaultMaker::new), ..Default::default() }, kind: match variant.kind { @@ -574,7 +574,6 @@ impl TypeRegistry { // If the path is not writable then it cannot be validated // as it cannot be written out as Bauble source. || !ty.meta.path.is_representable_type() - || !ty.meta.traits.contains(&self.object_trait_dependency) { continue; } @@ -594,6 +593,15 @@ impl TypeRegistry { }); }; + // Value can't be deserialized to object if it doesn't impl the object trait, so + // skip converting these to the bauble text format and back. + // + // We check this after `instantiate`, so we can test the instantiation code for + // more types even if a full serialization roundtrip is not tested). + if !self.impls_object_trait(value.ty) { + continue; + } + for (path, value) in additonal.into_objects() { objects.push(crate::Object { object_path: path, @@ -905,6 +913,11 @@ impl TypeRegistry { } /// Create the default value of this type. + /// + /// The returned value will be assigned a `TypeId` that is for a type where + /// `TypeKind::instanciable` is `true`. In most cases this matches the `ty_id` provided to + /// `instantiate`. For trait types this is instead the `ty_id` of the type instiantiated that + /// implements the trait. pub fn instantiate( &self, ty_id: TypeId, @@ -913,7 +926,7 @@ impl TypeRegistry { let ty = self.key_type(ty_id); if let Some(default) = &ty.meta.default { - return Some(default(additional_objects, self, ty_id).with_type(ty_id)); + return Some(default.make_value(additional_objects, self, ty_id)); } let construct_unnamed = @@ -1002,7 +1015,19 @@ impl TypeRegistry { TypeKind::Transparent(ty) => { let inner = self.key_type(*ty); crate::Value::Transparent(Box::new(if let TypeKind::Trait(tr) = &inner.kind { - self.iter_type_set(tr) + let mut v: Vec<_> = self + .iter_type_set(tr) + // The transparent type may itself implement the trait, so we need to skip + // it to avoid an infinite loop. + .filter(|ty| *ty != ty_id) + .collect(); + v.sort_unstable_by(|a, b| { + self.key_type(*a) + .meta + .path + .cmp(&self.key_type(*b).meta.path) + }); + v.into_iter() .find_map(|ty| self.instantiate(ty, additional_objects))? } else { self.instantiate(*ty, additional_objects)? @@ -1051,6 +1076,32 @@ pub type ValidationFunction = pub type DefaultFunction = fn(&mut AdditionalUnspannedObjects, &TypeRegistry, TypeId) -> UnspannedVal; +/// Wrapper around `DefaultFunction` that ensures the returned value is properly assigned a +/// concrete type ID. +#[derive(Clone, Copy, Debug)] +pub struct DefaultMaker(DefaultFunction); + +impl DefaultMaker { + /// Creates a new `DefaultMaker` wrapping the provided `DefaultFunction`. + pub fn new(f: DefaultFunction) -> Self { + Self(f) + } + + /// See [`DefaultFunction`] docs. + pub fn make_value( + &self, + additional: &mut AdditionalUnspannedObjects, + types: &TypeRegistry, + ty: TypeId, + ) -> UnspannedVal { + // Default values not allowed for non-instanciable types. + debug_assert!(types.key_type(ty).kind.instanciable()); + let mut value = (self.0)(additional, types, ty); + value.ty = ty; + value + } +} + /// Meta information on a type registered within a Bauble context. #[derive(Default, Clone, Debug)] pub struct TypeMeta { @@ -1061,7 +1112,7 @@ pub struct TypeMeta { /// The traits implemented by the type. pub traits: Vec, /// Optional function to create a default value of the type. - pub default: Option, + pub default: Option, /// What attributes the type expects. pub attributes: NamedFields, /// If this type has any extra invariants that need to be checked. diff --git a/bauble/src/value/convert.rs b/bauble/src/value/convert.rs index ea0f349..a619342 100644 --- a/bauble/src/value/convert.rs +++ b/bauble/src/value/convert.rs @@ -835,7 +835,7 @@ where |additional| { ty.meta .default - .map(|v| v(additional, types, *ty_id).into_spanned(span)) + .map(|d| d.make_value(additional, types, *ty_id).into_spanned(span)) .expect("We checked that this is some in the match") }, )?; diff --git a/bauble/src/value/display.rs b/bauble/src/value/display.rs index ac63701..0754d78 100644 --- a/bauble/src/value/display.rs +++ b/bauble/src/value/display.rs @@ -589,20 +589,24 @@ impl, V: IndentedDisplay + ValueTrait> IndentedDisplay true, - TypeKind::Enum { variants } => { + TypeKind::Enum { variants } if !is_generic_instance => { if let Value::Enum(variant, _) = self.value.value() && let Some(variant_ty) = variants.get(variant.borrow()) + && let TypeKind::EnumVariant { .. } = registry.key_type(variant_ty).kind { - matches!( - registry.key_type(variant_ty).kind, - TypeKind::EnumVariant { .. } - ) + true } else { false } @@ -611,7 +615,7 @@ impl, V: IndentedDisplay + ValueTrait> IndentedDisplay false, }; - if !can_skip_type && let Some(path) = registry.get_representable_path(ty) { + if !can_skip_type && let Some(path) = registry.get_representable_path(ty_id) { let path = path.to_owned(); w.write(": "); w.write(path.as_str()); diff --git a/bauble_macro_util/src/lib.rs b/bauble_macro_util/src/lib.rs index dfad27d..e752592 100644 --- a/bauble_macro_util/src/lib.rs +++ b/bauble_macro_util/src/lib.rs @@ -1258,7 +1258,7 @@ pub fn derive_bauble_derive_input( .unwrap_or(quote!(::core::option::Option::None)); let default = match ty_attrs.value_default { - Some(e) => quote! { ::core::option::Option::Some(#e) }, + Some(e) => quote! { ::core::option::Option::Some(::bauble::types::DefaultMaker::new(#e)) }, None => quote! { ::core::option::Option::None }, }; From 30deddb332e25b9e14290f05e595ca715aee519b Mon Sep 17 00:00:00 2001 From: Imbris Date: Wed, 10 Jun 2026 11:13:12 -0400 Subject: [PATCH 2/8] Add note that there may be other cases of infinite loops with instantiation involving traits --- bauble/src/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 3af16d4..188574a 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -1019,6 +1019,8 @@ impl TypeRegistry { .iter_type_set(tr) // The transparent type may itself implement the trait, so we need to skip // it to avoid an infinite loop. + // TODO: are there cases for other type kinds where this may happen? E.g. a + // random struct with a trait (or reference to a trait) as a field? .filter(|ty| *ty != ty_id) .collect(); v.sort_unstable_by(|a, b| { From 21593a86db7caf1013c89d0b86259fbcb2e9abac Mon Sep 17 00:00:00 2001 From: Imbris Date: Mon, 8 Jun 2026 12:59:04 -0400 Subject: [PATCH 3/8] bump nightly version --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d9d3e79..e0fb084 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2025-02-25" +channel = "nightly-2026-06-05" From c33f3a0f23878a2723fa1044e85fd013f6f03dd0 Mon Sep 17 00:00:00 2001 From: Imbris Date: Mon, 8 Jun 2026 12:59:43 -0400 Subject: [PATCH 4/8] cargo fix --- bauble/src/parse/value.rs | 2 +- bauble/src/traits.rs | 8 ++++---- bauble/src/types.rs | 2 +- bauble/src/types/path.rs | 2 +- bauble/src/value/convert.rs | 2 +- bauble/src/value/display.rs | 10 +++++----- bauble/src/value/mod.rs | 12 ++++++------ bauble_macro_util/src/lib.rs | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bauble/src/parse/value.rs b/bauble/src/parse/value.rs index 873ec2a..5088dd6 100644 --- a/bauble/src/parse/value.rs +++ b/bauble/src/parse/value.rs @@ -147,7 +147,7 @@ impl ValueTrait for ParseVal { &self.value } - fn to_any(&self) -> AnyVal { + fn to_any(&self) -> AnyVal<'_> { AnyVal::Parse(self) } } diff --git a/bauble/src/traits.rs b/bauble/src/traits.rs index ae7eb58..45388a8 100644 --- a/bauble/src/traits.rs +++ b/bauble/src/traits.rs @@ -471,7 +471,7 @@ impl Bauble<'_> for String { fn from_bauble( val: Val, _: &DefaultAllocator, - ) -> Result<::Out, ToRustError> { + ) -> Result<>::Out, ToRustError> { if let Value::Primitive(PrimitiveValue::Str(str)) = val.value.value { Ok(str) } else { @@ -740,7 +740,7 @@ impl<'a, T: Bauble<'a>> Bauble<'a> for Vec { fn from_bauble( val: Val, allocator: &DefaultAllocator, - ) -> Result<::Out, ToRustError> { + ) -> Result<>::Out, ToRustError> { if let Value::Array(items) = val.value.value { items .into_iter() @@ -775,7 +775,7 @@ impl<'a, T: Bauble<'a>> Bauble<'a> for Box { fn from_bauble( val: Val, allocator: &DefaultAllocator, - ) -> Result<::Out, ToRustError> { + ) -> Result<>::Out, ToRustError> { T::from_bauble(val, allocator).map(Box::new) } } @@ -809,7 +809,7 @@ macro_rules! impl_map { fn from_bauble( val: Val, allocator: &DefaultAllocator, - ) -> Result<::Out, ToRustError> { + ) -> Result<>::Out, ToRustError> { if let Value::Map(map) = val.value.value { map.into_iter() .map(|(k, v)| { diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 188574a..2a63456 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -551,7 +551,7 @@ impl TypeRegistry { /// Currently this checks: /// - Are there any unassigned registered types? /// - If `assert_instanciable` is true then if all `instanciable` types have valid bauble representations. - pub fn validate(&self, assert_instanciable: bool) -> Result<(), TypeSystemError> { + pub fn validate(&self, assert_instanciable: bool) -> Result<(), TypeSystemError<'_>> { if !self.to_be_assigned.is_empty() { return Err(TypeSystemError::ToBeAssigned( self.to_be_assigned diff --git a/bauble/src/types/path.rs b/bauble/src/types/path.rs index 9c6a52c..28efde0 100644 --- a/bauble/src/types/path.rs +++ b/bauble/src/types/path.rs @@ -394,7 +394,7 @@ impl> TypePath { } /// Create an iterator for iterating the segments of `self`. - pub fn iter(&self) -> PathIter { + pub fn iter(&self) -> PathIter<'_> { PathIter { path: self.borrow(), } diff --git a/bauble/src/value/convert.rs b/bauble/src/value/convert.rs index a619342..16881c0 100644 --- a/bauble/src/value/convert.rs +++ b/bauble/src/value/convert.rs @@ -244,7 +244,7 @@ pub struct ConvertMeta<'a> { } impl ConvertMeta<'_> { - pub fn reborrow(&mut self) -> ConvertMeta { + pub fn reborrow(&mut self) -> ConvertMeta<'_> { ConvertMeta { symbols: self.symbols, additional_objects: self.additional_objects, diff --git a/bauble/src/value/display.rs b/bauble/src/value/display.rs index 0754d78..75cfffe 100644 --- a/bauble/src/value/display.rs +++ b/bauble/src/value/display.rs @@ -95,7 +95,7 @@ mod formatter { write!(&mut self.0.string, "{v:?}").expect("Shouldn't fail"); } - pub fn reborrow(&mut self) -> LineWriter { + pub fn reborrow(&mut self) -> LineWriter<'_, CTX> { LineWriter(self.0.reborrow()) } @@ -122,11 +122,11 @@ mod formatter { LineWriter(self.0.with_ctx(ctx)) } - pub fn with_typed_context(&mut self) -> LineWriter { + pub fn with_typed_context(&mut self) -> LineWriter<'_, CTX> { LineWriter(self.0.with_typed_context()) } - pub fn with_untyped_context(&mut self) -> LineWriter { + pub fn with_untyped_context(&mut self) -> LineWriter<'_, CTX> { LineWriter(self.0.with_untyped_context()) } } @@ -146,7 +146,7 @@ mod formatter { typed_context: true, } } - fn reborrow(&mut self) -> Formatter { + fn reborrow(&mut self) -> Formatter<'_, CTX> { Formatter { config: self.config, string: self.string, @@ -156,7 +156,7 @@ mod formatter { typed_context: self.typed_context, } } - fn bump_indent(&mut self) -> Formatter { + fn bump_indent(&mut self) -> Formatter<'_, CTX> { let mut r = self.reborrow(); r.indent += 1; r diff --git a/bauble/src/value/mod.rs b/bauble/src/value/mod.rs index d53096c..1bac9f6 100644 --- a/bauble/src/value/mod.rs +++ b/bauble/src/value/mod.rs @@ -42,7 +42,7 @@ pub trait ValueTrait: Clone + std::fmt::Debug { fn value(&self) -> &Value; - fn to_any(&self) -> AnyVal; + fn to_any(&self) -> AnyVal<'_>; } /// A helper trait for extracting the spans out of a Bauble value. @@ -77,7 +77,7 @@ pub trait ValueContainer: Clone + std::fmt::Debug { fn container_ty(&self) -> TypeId; - fn container_to_any(&self) -> AnyVal; + fn container_to_any(&self) -> AnyVal<'_>; } impl ValueContainer for V { @@ -91,7 +91,7 @@ impl ValueContainer for V { ValueTrait::ty(self) } - fn container_to_any(&self) -> AnyVal { + fn container_to_any(&self) -> AnyVal<'_> { self.to_any() } } @@ -115,7 +115,7 @@ impl ValueContainer for AnyVal<'_> { } } - fn container_to_any(&self) -> AnyVal { + fn container_to_any(&self) -> AnyVal<'_> { *self } } @@ -245,7 +245,7 @@ impl ValueTrait for Val { &self.value } - fn to_any(&self) -> AnyVal { + fn to_any(&self) -> AnyVal<'_> { AnyVal::Complete(self) } } @@ -345,7 +345,7 @@ impl ValueTrait for UnspannedVal { &self.value } - fn to_any(&self) -> AnyVal { + fn to_any(&self) -> AnyVal<'_> { AnyVal::Unspanned(self) } } diff --git a/bauble_macro_util/src/lib.rs b/bauble_macro_util/src/lib.rs index e752592..dc88e8b 100644 --- a/bauble_macro_util/src/lib.rs +++ b/bauble_macro_util/src/lib.rs @@ -337,7 +337,7 @@ fn parse_fields( tuple: bool, has_from: bool, construct_default: fn(TokenStream) -> TokenStream, -) -> syn::Result { +) -> syn::Result> { let mut val_count = 0; let kind = match fields { // Named fields in a type with the `tuple` attribute are treated as a tuple From d7b94c1cbab1e204476b5980320d3280e4988691 Mon Sep 17 00:00:00 2001 From: Imbris Date: Tue, 9 Jun 2026 11:59:22 -0400 Subject: [PATCH 5/8] address new lints --- bauble/src/lib.rs | 2 +- bauble/src/parse/parser.rs | 20 -------------------- bauble/src/types.rs | 23 +++++++++++++++++++---- bauble/src/value/convert.rs | 14 +++++++------- bauble/src/value/mod.rs | 6 +++--- bauble_macros/tests/derive.rs | 2 +- 6 files changed, 31 insertions(+), 36 deletions(-) diff --git a/bauble/src/lib.rs b/bauble/src/lib.rs index 8cab17b..877461c 100644 --- a/bauble/src/lib.rs +++ b/bauble/src/lib.rs @@ -1,5 +1,5 @@ #![doc = include_str!("../../OVERVIEW.md")] -#![feature(iterator_try_collect, let_chains, ptr_metadata)] +#![feature(iterator_try_collect, ptr_metadata)] #![warn(missing_docs)] mod builtin; diff --git a/bauble/src/parse/parser.rs b/bauble/src/parse/parser.rs index 9e352c4..4aaee74 100644 --- a/bauble/src/parse/parser.rs +++ b/bauble/src/parse/parser.rs @@ -139,26 +139,6 @@ impl<'a, T> From> for chumsky::error::RichPattern<'a, T> { } } -#[derive(Clone, Copy)] -struct State(T); - -impl<'src, T: Copy, I: Input<'src>> chumsky::inspector::Inspector<'src, I> for State { - type Checkpoint = State; - #[inline(always)] - fn on_token(&mut self, _: &>::Token) {} - #[inline(always)] - fn on_save<'parse>(&self, _: &chumsky::input::Cursor<'src, 'parse, I>) -> Self::Checkpoint { - *self - } - #[inline(always)] - fn on_rewind<'parse>( - &mut self, - c: &chumsky::input::Checkpoint<'src, 'parse, I, Self::Checkpoint>, - ) { - *self = *c.inspector(); - } -} - // TODO Re-add error recovery pub fn parser<'a>() -> impl Parser<'a, ParserSource<'a>, ParseValues, Extra<'a>> { let line_comment_end = just::<_, ParserSource<'a>, Extra<'a>>('\n').or(end().map(|_| '\0')); diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 2a63456..247f81f 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -193,6 +193,15 @@ impl Variant { } } +/// Error details for when the instantiated value of a type is not identical after roundtripping +/// through the Bauble format. +#[derive(Debug)] +pub struct ConstructInequalityError { + src: String, + original: UnspannedVal, + new: UnspannedVal, +} + /// An error that occured within the Bauble context's type-system during [`TypeRegister::validate`]. #[allow(missing_docs)] #[derive(Debug)] @@ -203,7 +212,8 @@ pub enum TypeSystemError<'a> { ty: TypePath<&'a str>, }, InstantiableErrors, - ConstructInequality(String, UnspannedVal, UnspannedVal), + // This variant is large, so we box it. + ConstructInequality(Box), MissingObjects { instantiated_missing: Vec, loaded_unknown: Vec, @@ -232,11 +242,12 @@ impl Display for TypeSystemError<'_> { f, "Errors while trying to read default instantiated objects" ), - TypeSystemError::ConstructInequality(s, a, b) => { + TypeSystemError::ConstructInequality(e) => { + let ConstructInequalityError { src, original, new } = &**e; write!( f, "The constructed instantiated type, and the value read from the instantiated \ - value formatted as text are not equal.\nBauble:\n{s}\n\nInstantiated: {a:#?}\nRead: {b:#?}" + value formatted as text are not equal.\nBauble:\n{src}\n\nInstantiated: {original:#?}\nRead: {new:#?}" ) } TypeSystemError::MissingObjects { @@ -660,7 +671,11 @@ impl TypeRegistry { .skip(span.start) .take(span.end - span.start) .collect(); - TypeSystemError::ConstructInequality(src, original, new) + TypeSystemError::ConstructInequality(Box::new(ConstructInequalityError { + src, + original, + new, + })) } else { TypeSystemError::MissingObjects { instantiated_missing: missing diff --git a/bauble/src/value/convert.rs b/bauble/src/value/convert.rs index 16881c0..5a40eb3 100644 --- a/bauble/src/value/convert.rs +++ b/bauble/src/value/convert.rs @@ -78,14 +78,14 @@ fn resolve_type( } }; - if let Some(val_type) = val_type { - if !types.can_infer_from(expected_type, val_type.value) { - return Err(ConversionError::ExpectedExactType { - expected: expected_type, - got: Some(val_type.value), - } - .spanned(span)); + if let Some(val_type) = val_type + && !types.can_infer_from(expected_type, val_type.value) + { + return Err(ConversionError::ExpectedExactType { + expected: expected_type, + got: Some(val_type.value), } + .spanned(span)); } Ok(ty) diff --git a/bauble/src/value/mod.rs b/bauble/src/value/mod.rs index 1bac9f6..c09479f 100644 --- a/bauble/src/value/mod.rs +++ b/bauble/src/value/mod.rs @@ -1056,8 +1056,8 @@ fn compare_objects( loaded: &UnspannedVal, orig_map: &HashMap, loaded_map: &HashMap, -) -> std::result::Result<(), (UnspannedVal, UnspannedVal)> { - let inquality_err = || (original.clone(), loaded.clone()); +) -> std::result::Result<(), Box<(UnspannedVal, UnspannedVal)>> { + let inquality_err = || Box::new((original.clone(), loaded.clone())); original .attributes @@ -1199,7 +1199,7 @@ pub fn compare_object_sets( if !matches!(k, ObjectPath::Inline(_)) { if let Some((span, b)) = loaded_object_map.get(k) { if let Err((original, new)) = - compare_objects(a, b, &original_object_map, &loaded_object_map) + compare_objects(a, b, &original_object_map, &loaded_object_map).map_err(|e| *e) { mismatched.push((k.to_owned(), *span, original, new)); } diff --git a/bauble_macros/tests/derive.rs b/bauble_macros/tests/derive.rs index acc6191..6cd8180 100644 --- a/bauble_macros/tests/derive.rs +++ b/bauble_macros/tests/derive.rs @@ -285,7 +285,7 @@ fn test_trait() { val: bauble::Val, allocator: &bauble::DefaultAllocator, ) -> Result< - ::Out, + >::Out, bauble::ToRustError, > { let s = val.span(); From 0b423a62d4948f3e479a3edaffef3913f676faa9 Mon Sep 17 00:00:00 2001 From: Imbris Date: Tue, 26 May 2026 21:09:13 -0400 Subject: [PATCH 6/8] Improve PathError docs, readability of path_len, and add more test cases for path errors --- bauble/src/types/path.rs | 51 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/bauble/src/types/path.rs b/bauble/src/types/path.rs index 28efde0..ddd7772 100644 --- a/bauble/src/types/path.rs +++ b/bauble/src/types/path.rs @@ -167,10 +167,17 @@ impl> std::fmt::Display for TypePath { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PathError { /// There was an empty path element, for example `foo::::bar`. + /// + /// Contains byte index where path element was expected to start, just after the starting '::' + /// for that path element. This index may be right after the end of the string. EmptyElem(usize), - /// A start delimiter, i.e `<`, `(`, `[`, is missing its end equivalent. + /// A start delimiter, i.e `<`, `(`, `[`, `{`, is missing its end equivalent. + /// + /// Contains byte index of start delimiter character. MissingDelimiterEnd(usize), - /// An end delimiter, i.e `>`, `)`, `]`, is missing its start equivalent. + /// An end delimiter, i.e `>`, `)`, `]`, `}`, is missing its start equivalent. + /// + /// Contains byte index of end delimiter character. MissingDelimiterStart(usize), /// Too many path elements. /// @@ -219,23 +226,25 @@ fn path_len(path: &str) -> Result { return Ok(0); } - let mut count = 1; - let mut current_path_delim = PATH_SEPERATOR.chars(); + let mut count = 0; + // When this iterator is empty, we expect to an encounter a path element, and not + // the start of PATH_SEPERATOR. A path must start with a path element, so this starts as an + // empty iterator. + let mut current_path_delim = "".chars(); let mut path_iter = path.char_indices(); - let mut is_empty = true; while let Some((i, c)) = path_iter.next() { match current_path_delim.next() { Some(expected) => { if c == expected { continue; - } else { - is_empty = false; } } None => { + // `c` is the start of a new path element. count += 1; - if PATH_SEPERATOR.starts_with(c) || is_empty { + // If `c` matches the start of PATH_SEPERATOR, this path element was empty. + if PATH_SEPERATOR.starts_with(c) { return Err(PathError::EmptyElem(i)); } } @@ -244,6 +253,7 @@ fn path_len(path: &str) -> Result { skip_delims(&mut path_iter, c, i)?; } + // The path ended with PATH_SEPERATOR and no following element. if current_path_delim.next().is_none() { return Err(PathError::EmptyElem(path.len())); } @@ -722,8 +732,29 @@ fn test_path_seperating() { ), ); - let error_path = TypePath::new("root(test<)::test::el]em"); - assert!(error_path.is_err()); + let error_paths = [ + "root(test<)::test::elem", + "root(test<)::test::el]em", + "root(test)::test::el]em", + "::", + ":::", + "::test", + ":test", + ":", + "root:::elem", + "root::::elem", + "root:::::elem", + "root::test::", + "root::test:::", + "root::test::::", + // Note, these are currently allowed: + // "test:ing", + // "test:in:g", + // "root::test:", + ]; + for path in error_paths { + assert!(TypePath::new(path).is_err(), "{}", path); + } let empty_path = TypePath::new("").unwrap(); From 8ac0af3e32f4fa0c0495a40d2d23772600376b93 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 11 Jun 2026 13:57:45 -0400 Subject: [PATCH 7/8] Document what Extra is --- bauble/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauble/src/types.rs b/bauble/src/types.rs index 247f81f..ae4ab7d 100644 --- a/bauble/src/types.rs +++ b/bauble/src/types.rs @@ -23,7 +23,7 @@ use crate::{ value::UnspannedVal, }; -#[allow(missing_docs)] +/// Type associated data that can be given to a registered type, variant, or field. pub type Extra = IndexMap; /// A trait that can be represented within a bauble context. From e79d84367c79da4b75aa3e8a4e098f196eb96c20 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 11 Jun 2026 13:59:34 -0400 Subject: [PATCH 8/8] Adjust nightly version in CI config --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 782ddf9..c2b3c20 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2025-02-25 + toolchain: nightly-2026-06-05 override: true components: rustfmt,clippy