diff --git a/tests/tests/pointer.rs b/tests/tests/pointer.rs new file mode 100644 index 0000000..94b30f1 --- /dev/null +++ b/tests/tests/pointer.rs @@ -0,0 +1,79 @@ +use valuable::{pointer, Valuable, Value, Visit}; + +#[derive(Valuable)] +struct Struct1 { + x: String, + y: Struct2, +} + +#[derive(Valuable)] +struct Struct2 { + z: String, +} + +#[derive(Default)] +struct CollectValues(Vec); + +impl Visit for CollectValues { + fn visit_value(&mut self, value: Value<'_>) { + self.0.push(format!("{:?}", value)); + } +} + +#[test] +fn basic() { + let value = Struct1 { + x: "a".to_owned(), + y: Struct2 { z: "b".to_owned() }, + }; + + let mut visitor = CollectValues::default(); + value.visit_pointer( + pointer::Pointer::new(&[pointer::Segment::Field("x")]), + &mut visitor, + ); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#""a""#); + + let mut visitor = CollectValues::default(); + value.visit_pointer( + pointer::Pointer::new(&[pointer::Segment::Field("y")]), + &mut visitor, + ); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#"Struct2 { z: "b" }"#); + + let mut visitor = CollectValues::default(); + value.visit_pointer( + pointer::Pointer::new(&[pointer::Segment::Field("y"), pointer::Segment::Field("z")]), + &mut visitor, + ); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#""b""#); +} + +#[cfg(feature = "derive")] +#[test] +fn visit_pointer_macro() { + use valuable::visit_pointer; + + let value = Struct1 { + x: "a".to_owned(), + y: Struct2 { z: "b".to_owned() }, + }; + + let mut visitor = CollectValues::default(); + visit_pointer!(value.x, visitor); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#""a""#); + + let mut visitor = CollectValues::default(); + visit_pointer!(value.y, visitor); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#"Struct2 { z: "b" }"#); + + let mut visitor = CollectValues::default(); + visit_pointer!(value.y.z, visitor); + assert_eq!(visitor.0.len(), 1); + assert_eq!(visitor.0[0], r#""b""#); +} diff --git a/valuable-derive/Cargo.toml b/valuable-derive/Cargo.toml index 50fe784..82b1de0 100644 --- a/valuable-derive/Cargo.toml +++ b/valuable-derive/Cargo.toml @@ -22,7 +22,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.60" quote = "1.0" -syn = { version = "2.0", features = ["extra-traits"] } +syn = { version = "2.0", features = ["full", "extra-traits"] } [dev-dependencies] valuable = { path = "../valuable", features = ["derive"] } diff --git a/valuable-derive/src/expand.rs b/valuable-derive/src/derive.rs similarity index 82% rename from valuable-derive/src/expand.rs rename to valuable-derive/src/derive.rs index 53d78e6..decdbc3 100644 --- a/valuable-derive/src/expand.rs +++ b/valuable-derive/src/derive.rs @@ -16,6 +16,7 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea let name = &input.ident; let name_literal = name.to_string(); let visit_fields; + let visit_pointer; let struct_def; let mut named_fields_statics = None; @@ -33,13 +34,17 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea ) }; - let fields = data.fields.iter().map(|field| { - let f = field.ident.as_ref(); - let tokens = quote! { - &self.#f - }; - respan(tokens, &field.ty) - }); + let fields: Vec<_> = data + .fields + .iter() + .map(|field| { + let f = field.ident.as_ref(); + let tokens = quote! { + &self.#f + }; + respan(tokens, &field.ty) + }) + .collect(); visit_fields = quote! { visitor.visit_named_fields(&::valuable::NamedValues::new( #named_fields_static_name, @@ -47,7 +52,21 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea #(::valuable::Valuable::as_value(#fields),)* ], )); - } + }; + let field_name_literals = data + .fields + .iter() + .map(|field| field.ident.as_ref().unwrap().to_string()); + visit_pointer = quote! { + if let ::valuable::pointer::Segment::Field(f) = pointer.path()[0] { + match f { + #(#field_name_literals => { + ::valuable::Valuable::visit_pointer(#fields, pointer.step(), visit) + })* + _ => {} + } + } + }; } syn::Fields::Unnamed(_) | syn::Fields::Unit => { let len = data.fields.len(); @@ -58,13 +77,18 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea ) }; - let indices = data.fields.iter().enumerate().map(|(i, field)| { - let index = syn::Index::from(i); - let tokens = quote! { - &self.#index - }; - respan(tokens, &field.ty) - }); + let indices: Vec<_> = data + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let index = syn::Index::from(i); + let tokens = quote! { + &self.#index + }; + respan(tokens, &field.ty) + }) + .collect(); visit_fields = quote! { visitor.visit_unnamed_fields( &[ @@ -72,6 +96,21 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea ], ); }; + let field_indices = data + .fields + .iter() + .enumerate() + .map(|(i, _)| syn::Index::from(i)); + visit_pointer = quote! { + if let ::valuable::pointer::Segment::TupleIndex(i) = pointer.path()[0] { + match i { + #(#field_indices => { + ::valuable::Valuable::visit_pointer(#indices, pointer.step(), visit) + })* + _ => {} + } + } + }; } } @@ -94,6 +133,18 @@ fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> TokenStrea fn visit(&self, visitor: &mut dyn ::valuable::Visit) { #visit_fields } + + fn visit_pointer( + &self, + pointer: ::valuable::pointer::Pointer<'_>, + visit: &mut dyn ::valuable::Visit, + ) { + if pointer.path().is_empty() { + visit.visit_value(::valuable::Valuable::as_value(self)); + return; + } + #visit_pointer + } } }; @@ -309,13 +360,14 @@ fn named_fields_static(name: &Ident, fields: &syn::Fields) -> TokenStream { fn allowed_lints() -> TokenStream { quote! { #[allow(non_upper_case_globals)] + #[allow(clippy::collapsible_match)] #[allow(clippy::unknown_clippy_lints)] #[allow(clippy::used_underscore_binding)] #[allow(clippy::indexing_slicing)] } } -fn respan(tokens: TokenStream, span: &impl ToTokens) -> TokenStream { +pub(crate) fn respan(tokens: TokenStream, span: &impl ToTokens) -> TokenStream { let mut iter = span.to_token_stream().into_iter(); // `Span` on stable Rust has a limitation that only points to the first // token, not the whole tokens. We can work around this limitation by diff --git a/valuable-derive/src/lib.rs b/valuable-derive/src/lib.rs index 6021cb6..b593886 100644 --- a/valuable-derive/src/lib.rs +++ b/valuable-derive/src/lib.rs @@ -1,9 +1,10 @@ -extern crate proc_macro; +#![warn(rust_2018_idioms, unreachable_pub)] -mod expand; +mod derive; +mod pointer; use proc_macro::TokenStream; -use syn::parse_macro_input; +use syn::{parse_macro_input, Error}; /// Derive a `Valuable` implementation for a struct or enum. /// @@ -26,5 +27,12 @@ use syn::parse_macro_input; #[proc_macro_derive(Valuable, attributes(valuable))] pub fn derive_valuable(input: TokenStream) -> TokenStream { let mut input = parse_macro_input!(input as syn::DeriveInput); - expand::derive_valuable(&mut input).into() + derive::derive_valuable(&mut input).into() +} + +#[proc_macro] +pub fn visit_pointer(input: TokenStream) -> TokenStream { + pointer::visit_pointer(input.into()) + .unwrap_or_else(Error::into_compile_error) + .into() } diff --git a/valuable-derive/src/pointer.rs b/valuable-derive/src/pointer.rs new file mode 100644 index 0000000..68ece19 --- /dev/null +++ b/valuable-derive/src/pointer.rs @@ -0,0 +1,94 @@ +use std::collections::VecDeque; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Result, Token}; + +use crate::derive::respan; + +pub(crate) fn visit_pointer(input: TokenStream) -> Result { + let Input { + expr, + segments, + visit, + } = syn::parse2(input)?; + + let segments = segments.iter().map(|segment| match segment { + Segment::Member(syn::Member::Named(ident)) => { + let literal = ident.to_string(); + quote! { + ::valuable::pointer::Segment::Field(#literal), + } + } + Segment::Member(syn::Member::Unnamed(index)) => { + quote! { + ::valuable::pointer::Segment::TupleIndex(#index), + } + } + Segment::Index(expr) => { + let expr = respan(quote! { &#expr }, expr); + quote! { + ::valuable::pointer::Segment::Index( + ::valuable::Valuable::as_value(#expr) + ), + } + } + }); + + let visit_pointer = respan(quote! { ::valuable::Valuable::visit_pointer }, &expr); + Ok(quote! { + #visit_pointer( + &#expr, + ::valuable::pointer::Pointer::new(&[ + #(#segments)* + ]), + &mut #visit, + ) + }) +} + +struct Input { + expr: Expr, + segments: VecDeque, + visit: Expr, +} + +enum Segment { + Member(syn::Member), + Index(Box), +} + +impl Parse for Input { + fn parse(input: ParseStream<'_>) -> Result { + let mut chain = input.parse()?; + let _: Token![,] = input.parse()?; + let visit = input.parse()?; + let _: Option = input.parse()?; + + let mut segments = VecDeque::new(); + let expr; + loop { + match chain { + Expr::Field(e) => { + chain = *e.base; + segments.push_front(Segment::Member(e.member)) + } + Expr::Index(e) => { + chain = *e.expr; + segments.push_front(Segment::Index(e.index)) + } + e => { + expr = e; + break; + } + } + } + + Ok(Self { + expr, + segments, + visit, + }) + } +} diff --git a/valuable/src/lib.rs b/valuable/src/lib.rs index 255ee5a..74c9e4f 100644 --- a/valuable/src/lib.rs +++ b/valuable/src/lib.rs @@ -135,6 +135,8 @@ pub use mappable::Mappable; mod named_values; pub use named_values::NamedValues; +pub mod pointer; + mod slice; pub use slice::Slice; @@ -154,4 +156,4 @@ mod visit; pub use visit::{visit, Visit}; #[cfg(feature = "derive")] -pub use valuable_derive::Valuable; +pub use valuable_derive::{visit_pointer, Valuable}; diff --git a/valuable/src/listable.rs b/valuable/src/listable.rs index b79317f..ea7901a 100644 --- a/valuable/src/listable.rs +++ b/valuable/src/listable.rs @@ -147,6 +147,20 @@ macro_rules! slice { fn visit(&self, visit: &mut dyn Visit) { T::visit_slice(self, visit); } + + fn visit_pointer(&self, pointer: pointer::Pointer<'_>, visit: &mut dyn Visit) { + if pointer.path().is_empty() { + visit.visit_value(self.as_value()); + return; + } + if let pointer::Segment::Index(i) = pointer.path()[0] { + if let Some(i) = i.as_usize() { + if let Some(value) = self.get(i) { + value.visit_pointer(pointer.step(), visit); + } + } + } + } } $(#[$attrs])* @@ -226,6 +240,20 @@ impl Valuable for alloc::collections::VecDeque { T::visit_slice(first, visit); T::visit_slice(second, visit); } + + fn visit_pointer(&self, pointer: pointer::Pointer<'_>, visit: &mut dyn Visit) { + if pointer.path().is_empty() { + visit.visit_value(self.as_value()); + return; + } + if let pointer::Segment::Index(i) = pointer.path()[0] { + if let Some(i) = i.as_usize() { + if let Some(value) = self.get(i) { + value.visit_pointer(pointer.step(), visit); + } + } + } + } } #[cfg(feature = "alloc")] diff --git a/valuable/src/pointer.rs b/valuable/src/pointer.rs new file mode 100644 index 0000000..c254e36 --- /dev/null +++ b/valuable/src/pointer.rs @@ -0,0 +1,204 @@ +//! References to fields of [`Valuable`] values. +//! +//! A [`Pointer`] stores a path traversal to a particular value in a nested +//! [`Valuable`] structure. For example, a [`Pointer`] might refer to the field `z` +//! of the field `y` of the value `x`. [`Pointer`] paths can also include indices into +//! [`Listable`] and [`Tuplable`] values, in order to represent expressions like +//! `x.y[3].0`. +//! +//! # Examples +//! +//! ```rust +//! use valuable::*; +//! +//! #[derive(Valuable)] +//! struct Struct1 { +//! x: String, +//! y: Struct2, +//! } +//! +//! #[derive(Valuable)] +//! struct Struct2 { +//! z: String, +//! } +//! +//! struct Visitor; +//! +//! impl Visit for Visitor { +//! fn visit_value(&mut self, value: Value<'_>) { +//! println!("{:?}", value); +//! } +//! } +//! +//! let value = Struct1 { +//! x: "a".to_owned(), +//! y: Struct2 { +//! z: "b".to_owned(), +//! }, +//! }; +//! +//! let mut visitor = Visitor; +//! +//! visit_pointer!(value.x, visitor); // "a" +//! visit_pointer!(value.y, visitor); // Struct2 { field: "b" } +//! visit_pointer!(value.y.z, visitor); // "b" +//! ``` +//! +//! [`Listable`]: crate::Listable +//! [`Tuplable`]: crate::Tuplable + +use crate::{NamedValues, Slice, Valuable, Value, Visit}; + +/// A pointer to the value. +#[derive(Debug, Clone, Copy)] +pub struct Pointer<'a> { + path: &'a [Segment<'a>], +} + +impl<'a> Pointer<'a> { + /// Creates a new `Pointer`. + pub const fn new(path: &'a [Segment<'a>]) -> Self { + Self { path } + } + + /// Returns a path pointed by this pointer. + /// + /// If this pointer points to the current value, this method returns an empty slice. + pub fn path(self) -> &'a [Segment<'a>] { + self.path + } + + #[doc(hidden)] + #[must_use] + pub fn step(self) -> Self { + Self::new(&self.path[1..]) + } +} + +impl<'a> From<&'a [Segment<'a>]> for Pointer<'a> { + fn from(path: &'a [Segment<'a>]) -> Self { + Self::new(path) + } +} + +/// A segment of a path. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum Segment<'a> { + /// Access of a named struct field. + Field(&'a str), + /// Access of an unnamed struct or a tuple field. + TupleIndex(usize), + /// Indexing of a list or a map. + Index(Value<'a>), +} + +pub(crate) fn visit_pointer(value: &V, pointer: Pointer<'_>, visit: &mut dyn Visit) +where + V: ?Sized + Valuable, +{ + let value = value.as_value(); + if pointer.path.is_empty() { + visit.visit_value(value); + return; + } + + let visitor = &mut Visitor { + pointer, + visit, + index: 0, + }; + match (value, pointer.path[0]) { + (Value::Listable(l), Segment::Index(..)) => { + l.visit(visitor); + } + (Value::Mappable(m), Segment::Index(..)) => { + m.visit(visitor); + } + (Value::Tuplable(t), Segment::TupleIndex(..)) => { + t.visit(visitor); + } + (Value::Structable(s), Segment::TupleIndex(..)) if s.definition().fields().is_unnamed() => { + s.visit(visitor); + } + (Value::Structable(s), Segment::Field(..)) if s.definition().fields().is_named() => { + s.visit(visitor); + } + (_, p) => { + panic!("invalid pointer: {:?},", p) + } + } +} + +struct Visitor<'a> { + pointer: Pointer<'a>, + visit: &'a mut dyn Visit, + index: usize, +} + +impl Visit for Visitor<'_> { + fn visit_value(&mut self, value: Value<'_>) { + if let Segment::Index(index) = self.pointer.path[0] { + if index.as_usize() == Some(self.index) { + value.visit_pointer(self.pointer.step(), self.visit); + } + } + self.index += 1; + } + + fn visit_named_fields(&mut self, named_values: &NamedValues<'_>) { + if let Segment::Field(name) = self.pointer.path[0] { + if let Some(value) = named_values.get_by_name(name) { + value.visit_pointer(self.pointer.step(), self.visit); + } + } + } + + fn visit_unnamed_fields(&mut self, values: &[Value<'_>]) { + if let Segment::TupleIndex(index) = self.pointer.path[0] { + if let Some(value) = values.get(index - self.index) { + value.visit_pointer(self.pointer.step(), self.visit); + } + } + } + + fn visit_primitive_slice(&mut self, slice: Slice<'_>) { + if let Segment::Index(index) = self.pointer.path[0] { + if let Some(index) = index.as_usize() { + if let Some(value) = slice.get(index - self.index) { + value.visit_pointer(self.pointer.step(), self.visit); + } + } + } + self.index += slice.len(); + } + + fn visit_entry(&mut self, key: Value<'_>, value: Value<'_>) { + if let Segment::Index(index) = self.pointer.path[0] { + let matched = match key { + Value::Bool(k) => index.as_bool() == Some(k), + Value::Char(k) => index.as_char() == Some(k), + Value::I8(k) => index.as_i8() == Some(k), + Value::I16(k) => index.as_i16() == Some(k), + Value::I32(k) => index.as_i32() == Some(k), + Value::I64(k) => index.as_i64() == Some(k), + Value::I128(k) => index.as_i128() == Some(k), + Value::Isize(k) => index.as_isize() == Some(k), + Value::U8(k) => index.as_u8() == Some(k), + Value::U16(k) => index.as_u16() == Some(k), + Value::U32(k) => index.as_u32() == Some(k), + Value::U64(k) => index.as_u64() == Some(k), + Value::U128(k) => index.as_u128() == Some(k), + Value::Usize(k) => index.as_usize() == Some(k), + Value::String(k) => index.as_str() == Some(k), + #[cfg(feature = "std")] + Value::Path(k) => index.as_path() == Some(k), + // f32 and f64 are not included because they do not implement `Eq`. + _ => false, + }; + if matched { + value.visit_pointer(self.pointer.step(), self.visit); + } + } + } +} diff --git a/valuable/src/slice.rs b/valuable/src/slice.rs index a6ada6e..0cf75e2 100644 --- a/valuable/src/slice.rs +++ b/valuable/src/slice.rs @@ -75,7 +75,6 @@ macro_rules! slice { } } - /// Returns `true` if the slice is not empty. /// /// # Examples @@ -95,6 +94,17 @@ macro_rules! slice { self.len() == 0 } + /// Returns a reference to an element of index. + pub fn get(&self, index: usize) -> Option> { + #[allow(unused_doc_comments)] + match self { + $( + $(#[$attrs])* + Slice::$variant(s) => s.get(index).map(Valuable::as_value), + )* + } + } + /// Returns an iterator over the slice. /// /// # Examples diff --git a/valuable/src/valuable.rs b/valuable/src/valuable.rs index e8fa5dd..292a0eb 100644 --- a/valuable/src/valuable.rs +++ b/valuable/src/valuable.rs @@ -1,3 +1,4 @@ +use crate::pointer::{self, Pointer}; use crate::{Slice, Value, Visit}; use core::fmt; @@ -70,6 +71,11 @@ pub trait Valuable { visit.visit_value(item.as_value()); } } + + /// Visit the value pointed by `pointer`. + fn visit_pointer(&self, pointer: Pointer<'_>, visit: &mut dyn Visit) { + pointer::visit_pointer(self, pointer, visit); + } } macro_rules! deref {