askama icon indicating copy to clipboard operation
askama copied to clipboard

Iterating over elements of an iterator inside a macro moves the inner collection

Open msrd0 opened this issue 3 years ago • 6 comments

For some reason, askama won't use references of the inner collection when using macros, resulting in a compiler error

error[E0507]: cannot move out of `foo.list` which is behind a shared reference
   --> src/main.rs:
    |
280 |     #[derive(Template)]
    |              ^^^^^^^^
    |              |
    |              `foo.list` moved due to this method call
    |              move occurs because `foo.list` has type `BTreeSet<std::string::String>`, which does not implement the `Copy` trait
    |
note: this function takes ownership of the receiver `self`, which moves `foo.list`
   --> /usr/lib/rustlib/src/rust/library/core/src/iter/traits/collect.rs:261:18
    |
261 |     fn into_iter(self) -> Self::IntoIter;
    |                  ^^^^
    = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0507]: cannot move out of `foo.list` which is behind a shared reference
   --> src/main.rs:
    |
280 |     #[derive(Template)]
    |              ^^^^^^^^
    |              |
    |              `foo.list` moved due to this method call
    |              move occurs because `foo.list` has type `BTreeSet<std::string::String>`, which does not implement the `Copy` trait
    |
    = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

When taking the code from the macro body and placing it at the call side, it works as expected.

This is a minimal template to reproduce the problem:

{%- macro list(list) -%}
<ul>
	{%- for item in list %}
	<li>{{item}}</li>
	{%- endfor %}
</ul>
<ol>
	{%- for item in list %}
	<li>{{item}}</li>
	{%- endfor %}
</ol>
{%- endmacro -%}

{%- for foo in foos %}
{% call list(foo.list) %}
{%- endfor %}
#[derive(Template)]
#[template(path = "list.j2", print = "code")]
struct List {
	foos: Vec<Foo>
}

struct Foo {
	list: BTreeSet<String>
}
Generated code by askama

impl ::askama::Template for List {
    fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ::askama::Result<()> {
        include_bytes! ("/path/to/list.j2") ;
        {
            let mut _did_loop = false;
            let _iter = (&self.foos).into_iter();
            for (foo, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) {
                _did_loop = true;
                writer.write_str("\n")?;
                {
                    writer.write_str("<ul>")?;
                    {
                        let mut _did_loop = false;
                        let _iter = (foo.list).into_iter();
                        for (item, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) {
                            _did_loop = true;
                            ::std::write!(
                                writer,
                                "\n\t<li>{expr0}</li>",
                                expr0 = &::askama::MarkupDisplay::new_unsafe(&(item), ::askama::Html),
                            )?;
                        }
                        if !_did_loop {
                        }
                    }
                    writer.write_str("\n</ul>\n<ol>")?;
                    {
                        let mut _did_loop = false;
                        let _iter = (foo.list).into_iter();
                        for (item, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) {
                            _did_loop = true;
                            ::std::write!(
                                writer,
                                "\n\t<li>{expr1}</li>",
                                expr1 = &::askama::MarkupDisplay::new_unsafe(&(item), ::askama::Html),
                            )?;
                        }
                        if !_did_loop {
                        }
                    }
                    writer.write_str("\n</ol>")?;
                }
            }
            if !_did_loop {
            }
        }
        ::askama::Result::Ok(())
    }
    const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> =
    Some("j2")
    ;
    const SIZE_HINT: ::std::primitive::usize =
    0
    ;
    const MIME_TYPE: &'static ::std::primitive::str =
    "application/octet-stream"
    ;
}
impl ::std::fmt::Display for List {
    #[inline]
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
        ::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
    }
}

I'm using askama version 0.11.1 from crates.io.

msrd0 avatar Jul 26 '22 18:07 msrd0

Yeah, I guess we should maybe take a reference of whatever we're passing to the macro? Would you be able to submit a PR?

djc avatar Jul 28 '22 10:07 djc

I can try to do a PR but I need some more details on how you think this should be addressed. It sounds like the problem is with the macro somehow having a different logic to decide if the iterator needs to be taken over a reference or not than the template outside the macro? Can you tell me how this works in askama and point me to the relevant code?

msrd0 avatar Jul 31 '22 16:07 msrd0

By looking briefly at your loop code, it looks like askama "forgets" that self is involved and therefore doesn't take a reference. The behaviour can be replicated like this:

{%- for foo in foos %}
{%- set list = foo.list %}
<ul>
	{%- for item in list %}
	<li>{{item}}</li>
	{%- endfor %}
</ul>
<ol>
	{%- for item in list %}
	<li>{{item}}</li>
	{%- endfor %}
</ol>
{%- endfor %}

This produces the exact same error message. Maybe looping over a variable should always loop over that variable's reference?

msrd0 avatar Jul 31 '22 16:07 msrd0

I think this is the relevant PR: https://github.com/djc/askama/commit/230263711e7edb0110e1679f6d31353ba7cdc919. We may want to tweak that logic? Navigating whether we want to pass references or ownership is often tricky. I think it could make sense that, when referencing a self field, we want to pass a reference only to the macro.

djc avatar Aug 01 '22 09:08 djc

https://github.com/djc/askama/commit/c7697cbd406dce0962618e99a6b78116b14fdb1c contains similar changes for the macro calling code, I think.

djc avatar Aug 01 '22 09:08 djc

I tried to implement your solution, but either I didn't get it quite right or just always taking a reference of macro parameters doesn't work.

msrd0 avatar Aug 06 '22 13:08 msrd0