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

Implemented Revamped Object Model #148

Merged
merged 16 commits into from Nov 19, 2022
Merged

Implemented Revamped Object Model #148

merged 16 commits into from Nov 19, 2022

Conversation

mitsuhiko
Copy link
Owner

@mitsuhiko mitsuhiko commented Nov 16, 2022

This changes the object interface to support both structs and sequences with the possibility to add extra behavior (eg: arbitrary key maps) at a future point. There are some challenges with this design however.

Issues:

These are probably something I need to accept:

  • impl<'a, T: ArgType<'a, Output = T>> ArgType<'a> for Vec<T> cannot be implemented to support these new dynamic sequences because the lifetime constraints cannot be enabled. Restrict?

Things to resolve:

This is stuff that can be fixed:

  • Similarly the slice syntax x[:5] first converts the entire value before slicing down.
  • Decide on the names.
  • The Value::as_slice API cannot return borrowed values for dynamic slices. Internally a as_cow_slice has been added. Potential solution is to retire as_slice entirely for SeqObject.
  • Iteration over dynamic slices first makes a real slice out of it. This is mostly done because of lifetime issues that are annoying to work around.
  • Consider adding SeqObject::iter

Refs #147

@mitsuhiko
Copy link
Owner Author

Other potential names I made up:

  • Object::behavior -> Object::as_container
  • ObjectBehavior -> AsContainer
  • AsSeq -> AsSeqContainer
  • AsStruct -> AsStructContainer

@SergioBenitez
Copy link
Contributor

SergioBenitez commented Nov 18, 2022

Why not just ObjectKind? Object::kind() -> ObjectKind, StructObject, SeqObject, as_seq_object, as_struct_object.

Will look into making use of this later on today and report back.

@SergioBenitez
Copy link
Contributor

SergioBenitez commented Nov 18, 2022

A few notes from making use of this:

  • The naming of the get() methods introduced conflicts with {BTree/Hash}Map::get(). Perhaps get_field()?
  • The name of len() in AsSeq and AsStruct is also cause for concern as it may conflict with slice/vec methods. Maybe num_fields()?
  • Implementing both AsStruct or AsSeq and Object feels really redundant. One thing that could help resolve this are Value::from_struct_object and Value::from_seq_object methods so I don't need to implement Object when it provides no value. Or perhaps a macro that generates the impl.
  • AsStruct::len() and AsStruct::is_empty() feel like they shouldn't be there. I'm mostly curious why they're there when they weren't there before and if I should try to optimize them. Edit: I see why they're there now, but I'm wondering where they're used and if it's worthwhile to try to optimize them.

@SergioBenitez
Copy link
Contributor

SergioBenitez commented Nov 18, 2022

Also, I haven't investigated further, but I believe there's a bug somewhere. I'm getting an undefined value error where I don't expect one. Specifically, I have the equivalent of the following:

19 >       foo.bar[0].baz
i                 ^^^^^^^ undefined value

From the call to bar[0], I return:

Object {
    "baz": String("..."),
},

And yet I still get the error.

Edit: Here's the bug: #148 (review).

Copy link
Contributor

@SergioBenitez SergioBenitez left a comment

Choose a reason for hiding this comment

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

General feedback.

minijinja/src/filters.rs Outdated Show resolved Hide resolved
minijinja/src/value/mod.rs Outdated Show resolved Hide resolved
minijinja/src/value/mod.rs Outdated Show resolved Hide resolved
minijinja/src/value/mod.rs Show resolved Hide resolved
minijinja/src/value/mod.rs Outdated Show resolved Hide resolved
minijinja/src/value/mod.rs Outdated Show resolved Hide resolved
minijinja/src/value/ops.rs Outdated Show resolved Hide resolved
minijinja/src/value/mod.rs Outdated Show resolved Hide resolved
@SergioBenitez
Copy link
Contributor

SergioBenitez commented Nov 18, 2022

Also, it would be simple to add a (implemented by default) iter() -> Iter<'_> method to AsSeq to iterate over any AsSeq. The struct would of course hold a reference to &self and internally call get().

@SergioBenitez
Copy link
Contributor

Performance wise, I'm seeing about a ~5% speedup as a result of making use of this. Not as wild as using Object, but still a nice and easy win.

@mitsuhiko
Copy link
Owner Author

I agree that having to implement Object and AsSeq is a bit odd, but it's not entirely terrible. The slice situation is more annoying. Potentially the solution is to really say the engine at least internally starts using the AsSeq trait in most places so at least the code duplication goes away, but I'm not sure if it's really so appealing as a user to have to use a custom sequence trait but it's unclear.

In any case as_slice as it exists today cannot be satisfied with dynamic slices and either has to live with that restriction or go away entirely.

@mitsuhiko
Copy link
Owner Author

mitsuhiko commented Nov 18, 2022

Still not particularly happy with this but it's going somewhere. as_slice is internal now but it has to hang around for borrowing of filter arguments. Unfortunately I cannot find a way to make these work with dynamic sequences since they do not have values that can be borrowed. I'm really not sure what could be done about that.

The APIs of SeqObject and StructObject are also quite ugly now with the new names (get_item, seq_len and get_field and struct_size) but I do agree that having the names clash with common other names is highly impractical.

I now also have a weird impl impl<'a> SeqObject for &'a [Value] which doesn't make too much sense. I would love to have one for [Value] instead but Rust doesn't allow that today. It only exists for a single case to implement the include logic. Wanna get rid of that.

@SergioBenitez
Copy link
Contributor

SergioBenitez commented Nov 19, 2022

I agree that having to implement Object and AsSeq is a bit odd, but it's not entirely terrible.

Yeah, it's absolutely not terrible. But, are you opposed to Value::from_seq_object() and Value::from_struct_object() constructor methods? Internally they would simply wrap in a T: Object. Their presence would mean you don't need to implement Object unless you need to dynamically decide between being a struct or seq.

Copy link
Contributor

@SergioBenitez SergioBenitez left a comment

Choose a reason for hiding this comment

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

Some more general commentary. This looks awesome.

minijinja/src/filters.rs Show resolved Hide resolved
Comment on lines 114 to 116
/// Similarly it's not possible to borrow dynamic slices
/// ([`SeqObject`](crate::value::SeqObject)) into `&[Value]`.
///
Copy link
Contributor

Choose a reason for hiding this comment

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

Since as_slice() is gone, what is this referring to?

Copy link
Owner Author

Choose a reason for hiding this comment

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

The underlying ArgTypes cannot be implemented still. See the remaining internal uses of as_slice. I'm unsure how this can be resolved due to limitations with how lifetimes are entangled with value. Basically you cannot have a filter taking foo(items: &[Value]) or foo(items: Vec<i32>) when the items value was a dynamic sequence.

}

/// If the value is a struct, return it as [`StructObject`].
pub fn as_struct(&self) -> Option<&dyn StructObject> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also return Some() when we have a ValueRepr::Map? That would make its functionality similar to as_seq(). And if so, maybe StructObject should be called MapObject.

Copy link
Owner Author

Choose a reason for hiding this comment

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

At a later point I might introduce a MapObject which takes arbitrary keys. However that opens up some uncomfortable questions about how to represent keys which I don't want to go into yet.

minijinja/src/value/object.rs Outdated Show resolved Hide resolved
minijinja/src/value/object.rs Outdated Show resolved Hide resolved
minijinja/src/value/object.rs Outdated Show resolved Hide resolved
@mitsuhiko
Copy link
Owner Author

I ended up adding Value::from_seq_object and Value::from_struct_object. For people who want to just impersonate basic struct type maps and sequences they also come with a matching default implementation for debug and stringification that look exactly like the built-in sequences. That however means that the Arc for these is hidden and that downcasting does not work since the internal wrappers are not accessible.

@mitsuhiko mitsuhiko changed the title Refactor objects to support sequences Implemented Revamped Object Model Nov 19, 2022
@mitsuhiko mitsuhiko merged commit f6cc6a4 into main Nov 19, 2022
@mitsuhiko mitsuhiko deleted the feature/seq-objs branch November 19, 2022 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants