Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor path module of bevy_reflect #8887

Merged
merged 11 commits into from
Jul 30, 2023

Conversation

nicopap
Copy link
Contributor

@nicopap nicopap commented Jun 18, 2023

Objective

  • The path module was getting fairly large.
  • The code in AccessRef::read_element and mut equivalent was very complex and difficult to understand.
  • The ReflectPathError had a lot of variants, and was difficult to read.

Solution

  • Split the file in two, access now has its own module
  • Rewrite the read_element methods, they were ~200 lines long, they are now ~70 lines long — I didn't change any of the logic. It's really just the same code, but error handling is separated.
  • Split the ReflectPathError error
  • Merge AccessRef and Access
  • A few other changes that aim to reduce code complexity

Fully detailed change list

  • Display impl of ParsedPath now includes prefix dots — this allows simplifying its implementation, and IMO .path.to.field is a better way to express a "path" than path.to.field which could suggest we are reading the to field of a variable named path
  • Add a test to check that dot prefixes and other are correctly parsed — Until now, no test contained a prefixing dot
  • Merge Access and AccessRef, using a Cow<'a, str>. Generated code seems to agree with this decision (ParsedPath::parse sheds 5% of instructions)
    • Remove Access::as_ref since there is no such thing as an AccessRef anymore.
    • Rename AccessRef::to_owned into AccessRef::into_owned() since it takes ownership of self now.
    • Add a parse_static that doesn't allocate new strings for named fields!
  • Add a section about path reflection in the bevy_reflect crate root doc — I saw a few people that weren't aware of path reflection, so I thought it was pertinent to add it to the root doc
  • a lot of nits
    • rename index to offset when it refers to offset in the path string — There is no more confusion with the other kind of indices in this context, also it's a common naming convention for parsing.
    • Make a dedicated enum for parsing errors
    • rename the read_element methods to element — shorter, but also read_element_mut was a fairly poor name
    • The error values now not only contain the expected type but also the actual type.
  • Remove lifetimes that could be inferred from the GetPath trait methods.

Change log

  • Added the ParsedPath::parse_static method, avoids allocating when parsing &'static str.

Migration Guide

If you were matching on the Err(ReflectPathError) value returned by GetPath and ParsedPath methods, now only the parse-related errors and the offset are publicly accessible. You can always use the fmt::Display to get a clear error message, but if you need programmatic access to the error types, please open an issue.

@nicopap nicopap added C-Code-Quality A section of code that is hard to understand or change A-Reflection Runtime information about types C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide labels Jun 18, 2023
@alice-i-cecile
Copy link
Member

I like the split a lot, and think this is an improvement in terms of code organization.

alice-i-cecile pushed a commit that referenced this pull request Jun 19, 2023
# Objective

`ParsedPath` does not need to be mut to access a field of a `Reflect`.
Be that access mutable or not. Yet `element_mut` requires a mutable
borrow on `self`.

## Solution

- Make `element_mut` take a `&self` over a `&mut self`.

#8887 fixes this, but this is a major limitation in the API and I'd
rather see it merged before 0.11.

---

## Changelog

- `ParsedPath::element_mut` and `ParsedPath::reflect_element_mut` now
accept a non-mutable `ParsedPath` (only the accessed `Reflect` needs to
be mutable)
@nicopap
Copy link
Contributor Author

nicopap commented Jun 19, 2023

@alice-i-cecile Those types are only public because they are part of ReflectPathError.

I created a wrapper type so that Type and AccessRef stay private.

I don't want to document them, it's difficult to come up with a good vocabulary, and it would require renaming every types in the module. And all of this because of fields of an error value.

@alice-i-cecile
Copy link
Member

Sounds good, I think that's a sensible solution <3

james7132 pushed a commit to james7132/bevy that referenced this pull request Jun 19, 2023
# Objective

`ParsedPath` does not need to be mut to access a field of a `Reflect`.
Be that access mutable or not. Yet `element_mut` requires a mutable
borrow on `self`.

## Solution

- Make `element_mut` take a `&self` over a `&mut self`.

bevyengine#8887 fixes this, but this is a major limitation in the API and I'd
rather see it merged before 0.11.

---

## Changelog

- `ParsedPath::element_mut` and `ParsedPath::reflect_element_mut` now
accept a non-mutable `ParsedPath` (only the accessed `Reflect` needs to
be mutable)
Copy link
Member

@MrGVSV MrGVSV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job! This is much cleaner!

Just some nits, but overall looks good!

crates/bevy_reflect/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 43 to 53
pub(super) enum Type {
Struct,
TupleStruct,
Tuple,
List,
Array,
Map,
Enum,
Value,
Unit,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty similar to this code from #7075 and is something I think deserves its own small PR due to how much it seems to want to crop up in these code de-duplications.

/// A simple enumeration of [kinds](ReflectKind) of type, without any associated object.
///
/// All types implementing [`Reflect`] are categorized into "kinds". They help to group types that
/// behave similarly and provide methods specific to its kind. These kinds directly correspond to
/// the traits [`Struct`], [`TupleStruct`], [`Tuple`], [`List`], [`Array`], [`Map`] and [`Enum`];
/// which means that a type implementing any one of the above traits will be of the corresponding
/// kind. All the remaining types will be `ReflectKind::Value`.
///
/// A `ReflectKind` is obtained via [`Reflect::reflect_kind`].
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ReflectKind {
Struct,
TupleStruct,
Tuple,
List,
Array,
Map,
Enum,
Value,
}

The only difference is that your code here includes a Unit variant (not sure how important it is to keep or not).

And I bring this up more as a note rather than something this PR needs to be blocked on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'd really like a TypeShape of sort. I think I copied the ReflectRef definition and removed the values from the variants to make the Type enum. I added Unit later to account for mismatched enum variants (Unit is the variant without value).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah and actually I wonder if TypeShape is a clearer terminology. We'll probably explore this more in a future PR.

crates/bevy_reflect/src/path/access.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/path/access.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/path/access.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/path/access.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/path/access.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/path/access.rs Show resolved Hide resolved
crates/bevy_reflect/src/path/mod.rs Outdated Show resolved Hide resolved
nicopap and others added 8 commits July 5, 2023 09:18
```
field[0].foo.bar
```

vs

```
.field[0].foo.bar
```

- Both are valid as input for parsing
- The one with the leading dot is more consistant,
  as in the current implementation, the hatch will be present
  if the leading access is a field index.
- IMO having the leading dot demonstrates better we are accessing the
  field `field`, rather than index 0 of variable `field`.
- The logic for displaying with the leading dot is simpler,
  we don't need to discriminate the first element.

Also implement Display for Access.
This removes 8 bytes to the size of `Access`.
The root doc for bevy_reflect doesn't mention `GetPath`. It's a fairly
useful featuree and I've seen people be surprised to learn it exists.
This commit:

- Splits the `path.rs` module of `bevy_reflect` in two. the `Access`
  structs are now defined in their own submodules
- Revamps error handling
    - Separate Parse error from access errors
    - rename "index" to offset. "offset" is more commonly used to
      designate parsing position, and it can't be confused with the
      other kind of indexing done in this module
    - Instead of having one error variant per possible mismatching type,
      create a single error with a "expected" and "actual" type.
    - The error values now contain more details about the nature of the
      error.
- Refactor the `read_element{_mut}` methods
    - They are now about 70 lines, while previously they were 188 lines
    - They are much more readable now.
    - Rename them to `element{_mut}`, since the  "read" is a bit
      misleading.
    - They accept a `self` rather than `&self`, since AccessRef is Copy
      now.
- We also rewrite Display for Access in term of write! rather than
  sequential write.fmt()
- Rename `to_ref` to `as_ref`
- Make AccessRef Copy: because it's a very small type. The downside is
  that it doesn't benefit from niching in the error type anymore.
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
nicopap and others added 2 commits July 5, 2023 09:36
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
@nicopap
Copy link
Contributor Author

nicopap commented Jul 5, 2023

@MrGVSV I found a nice way to merge Access and AccessRef. According to cargo asm, it is also a bit faster, and we also get a bonus parse_static method that removes all but one allocation when building ParsedPath!

Also thank you for the review, it helped improve the code a lot I think.

Copy link
Member

@MrGVSV MrGVSV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MrGVSV I found a nice way to merge Access and AccessRef. According to cargo asm, it is also a bit faster, and we also get a bonus parse_static method that removes all but one allocation when building ParsedPath!

Also thank you for the review, it helped improve the code a lot I think.

Awesome! Glad it worked out and great job cleaning this up! 😄

)]
Access { ty: TypeShape, access: Access<'a> },

#[error("invalid type shape: expected {expected} but found a reflect {actual}")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replying to this comment:

yeah, but Type here is not really a "type" but a "type shape", The error message would read as follow: "expected struct but found tuple". I don't think there is such a thing as a "struct-like" it's either a struct or not a struct.

Well I think it would be more useful for TypeShape::Tuple and TypeShape::List, since those also work for tuple structs and arrays, respectively. It's not required we use -like haha, I just think it would be clearer to users that we're not expecting them to pass only Struct, Tuple, and List types.

@nicopap nicopap added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Jul 9, 2023
@nicopap nicopap added this to the 0.12 milestone Jul 18, 2023
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jul 30, 2023
Merged via the queue into bevyengine:main with commit 08ea1d1 Jul 30, 2023
21 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Aug 5, 2023
# Objective

- Follow up to #8887
- The parsing code in `bevy_reflect/src/path/mod.rs` could also do with
some cleanup

## Solution

- Create the `parse.rs` module, move all parsing code to this module
- The parsing errors also now keep track of the whole parsed string, and
are much more fine-grained

### Detailed changes

- Move `PathParser` to `parse.rs` submodule
- Rename `token_to_access` to `access_following` (yep, goes from 132
lines to 16)
- Move parsing tests into the `parse.rs` file
@nicopap nicopap deleted the better-path branch August 30, 2023 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Code-Quality A section of code that is hard to understand or change S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants