-
-
Notifications
You must be signed in to change notification settings - Fork 219
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
Reworked implicit borrowing to be more relaxed #396
Conversation
I've sent you a message on Twitter the other day, in case you haven't noticed. |
I think I like this direction but in order to be able to review/understand it well I feel like I need more guidance, and perhaps I'd also want to aim for splitting up the initial commit here into smaller changes. Let's maybe start by leaving new tests out of this PR for now so I can focus on reviewing the logic changes and the changes to existing tests. Also, please squash intermediate commits like First, how do you see this affecting backwards compatibility for existing templates? Second, what is the goal of the change from |
The actual change to the generator isn't that crazy. It's overall just:
But tell me how I can help or feel free to comment on all the lines you feel necessary, and I'll comment. (Maybe wait until I've rebased.)
In short, consider the following template: {% macro bar(y) %}
{{ y }}
{% if y == "bar" %}
{% endif %}
{% endmacro %}
{% macro foo(x) %}
{{ x }}
{% endmacro %}
{% call foo(self.item) %} Then before it would essentially generate: (Which would result in a compile error for the if) let x = &self.item;
let y = &x;
write(&y);
if &y == "bar" {} Whereas now it essentially generates: write(&self.item);
if self.item == "bar" {} All in all, now you're able to do So in short, to be able to know which variable a local variable points to, that information is stored in the value part of the So since that information needed to be part of every variable in every scope, then I thought the simplest way would be to change
Technically, it's a breaking change. However, only 4 of the existing test cases were affected, 1 of which I introduced 11 days ago (#393). The affected test cases, are functions that had a borrowed literal as a parameter (e.g. So realistically, I don't think it will be an issue, and if someone did encounter issues, then adding Though, maybe the previous non-breaking changes should be released as a patch (v0.10.6), and then after we can bump the minor version (v0.11.0) and include this change.
Maybe it will be possible to split the
👍
Sure, I wanted to rewrite those into the existing test cases, in a more approachable manner anyways. As it is indeed a bit much, I just wanted to test every case I could think of, to ensure they wouldn't break later. So, do I just drop the commit for the new test cases? (I'll keep a local copy, to rework into the other test cases for later then) |
Ok, that all sounds good. Definitely split off the new test cases for a later PR. In fact, if it's not too hard to extract from the other changes, I'd prefer to focus this PR only on the change around the |
07b056f
to
5202327
Compare
Yes, that is correct. I've updated the PR. Now it only includes the In short:
I've updated the PR description to reflect the changes. |
askama_shared/src/generator.rs
Outdated
fn insert(&mut self, val: T) { | ||
self.scopes.last_mut().unwrap().insert(val); | ||
fn insert(&mut self, key: K, val: V) { | ||
let old_val = self.scopes.last_mut().unwrap().insert(key, val); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment documenting why the asserted invariant should hold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, we could also remove the assert
because Rust itself will error when attempting to compile the generated template. Basically, the cases are e.g. {% macro f(a, a) %}
and {% let (a, a) = ... %}
which both results in a compile error along the lines of "identifier a
used more than once" .
I'll remove it and add a note to insert
. Because if we want to add "identifier {:?} used more than once"
to the assert, then insert
requires where K: Debug + Clone/Copy
.
5202327
to
d34ea74
Compare
When this is merged, I'll do "let shadow" and then after that "copy literals". |
d34ea74
to
e689b4d
Compare
e689b4d
to
70afa33
Compare
I tried changing the |
Looks good! In the future, please consider the following in terms of ordering:
In the new |
Hmm,
So in relation to that, you referring to swapping
I assume, you're just mentioning this so I know for the future. Because a quick glance, and I don't see anywhere that I switched it around. To be fair, I didn't really think about that, because my personal preference for ordering, is different than that. 😅 However, I'll try to remember that for the future. |
Yeah, sorry, the
Yup.
What is your preference for ordering, and why? |
I'll try to remember this in the future. But otherwise, just tell me to reorder if I forget.
Actually, "preference" might not be the correct word. Now that I'm thinking about it, it's probably more correct to say "habit" and "what I'm used too". Generally, I order stuff by visibility, i.e.
I'd actually write use declarations in the opposite order (in e.g. generator.rs), i.e. However, ultimately I always attempt to follow the style of the repo I'm making a PR for. :) |
That order is actually pretty close to what I'm aiming for, but I do like to order functions (and items in general) such that depending items come before depended upon items. Usually the former are more complex and need more changes and are more likely to be called on by "external" callers. |
Of course, and honestly that actually makes a lot of sense in terms of readability. Say My only annoyance would be that in the documentation, I'd prefer |
I'm not sure if this is the right place, but I cannot figure out how to make this run
it would generate for (i, _loop_item) in ::askama::helpers::TemplateLoop::new((&self.v.iter().skip(&self.offset).take(&{
self.size.clone()}
)).into_iter()) I think it should not borrow when the expression is an function call |
Have you tried with latest main? |
Currently This is related to "copy literals" (issue #404), and a possible solution and workaround should be available soon. If you use the latest commit on main, then there is a rather simple workaround, by using a method on your template. impl SomeTemplate {
fn iter_skip_take(&self) -> impl Iterator<Item = &...> {
self.v.iter().skip(self.offset).take(self.size)
}
} {% for i in self.iter_skip_take() %}
{% endfor %} [dependencies]
askama = { git = "https://github.com/djc/askama", rev = "b880799" } |
Fixes #376 (can't compare
&str
withstr
, whileimport
/include
remains the same by design)This PR changes
SetChain
intoMapChain
, where the value represents an optional variable which the local variable "points to". This improves the generated Rust code, as existing variables are no longer continuously generated for every macro call. This means that variables are no longer continuously borrowed, e.g. making it possible tos == "foo"
in a macro.No test cases were modified, and this is now a non-breaking change.
Old Explanation:
To be able to reuse variables in nested macro calls, I ended up reworking
SetChain
intoMapChain
. Such that the each local can have an optional variable (or expression) which it points to. Then I addedresolve_var
to be able to follow and resolve a chain if variables, e.g."param"
->"a"
->"b"
->"self.c"
.This avoid that previously when
let (..) = (..)
would be defined in the local scope, it would either (a) cause values to be moved if non-copyable or (b) force all values to be referenced. This would cause every level of nested macro call, to increase the amount of indirection (i.e. 3 depth macro call ==&&&T
). This would makeb it harder to use.clone()
, but now it does not exceed&T
so using.clone()
is always possible.Given that
HashSet<T>
internally uses aHashMap<T, ()>
, then aSetChain
can still be created using, e.g.type SetChain<'a, T> = MapChain<'a, T, ()>
. Which implies that they are still identical. I have not changed "SetChain
"'s functionality, I've only addedget
andget_skip
as well asresolve_var
which I implemented specifically forMapChain<'_, &str, Option<String>>
.Additionally, now empty
let () = ();
would be generated more frequently, since variables are forwarded. So these are also culled from the output.