Skip to content

Commit d4e5e9f

Browse files
feat: while, break/continue/return, and preamble statements in html! (#4124)
also note: Our MSRV is 1.85+ but users might stay on edition 2021 for various reasons This change wraps break/continue in Into::into(...) triggered the never-type fallback in editions before 2024, where unconstrained ! falls back to (), producing "(): Into<VNode> is not satisfied" on edition 2021 users. Emit the keyword directly as a statement instead so the generated code is edition-agnostic.
1 parent 7085ad3 commit d4e5e9f

21 files changed

Lines changed: 2618 additions & 189 deletions

File tree

Lines changed: 10 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
1-
use proc_macro2::{Ident, TokenStream};
1+
use proc_macro2::TokenStream;
22
use quote::{ToTokens, quote};
33
use syn::buffer::Cursor;
44
use syn::parse::{Parse, ParseStream};
5-
use syn::spanned::Spanned;
65
use syn::token::{For, In};
7-
use syn::{Expr, Local, Pat, Stmt, Token, braced};
6+
use syn::{Expr, Pat, Stmt, braced};
87

9-
use super::{HtmlChildrenTree, ToNodeIterator};
8+
use super::HtmlChildrenTree;
9+
use super::html_loop::{emit_loop, parse_loop_body};
1010
use crate::PeekValue;
11-
use crate::html_tree::HtmlTree;
12-
13-
/// Determines if an expression is guaranteed to always return the same value anywhere.
14-
fn is_contextless_pure(expr: &Expr) -> bool {
15-
match expr {
16-
Expr::Lit(_) => true,
17-
Expr::Path(path) => path.path.get_ident().is_none(),
18-
_ => false,
19-
}
20-
}
2111

2212
pub struct HtmlFor {
2313
pat: Pat,
2414
iter: Expr,
25-
let_stmts: Vec<Local>,
15+
stmts: Vec<Stmt>,
2616
body: HtmlChildrenTree,
2717
deprecations: TokenStream,
2818
}
@@ -44,39 +34,12 @@ impl Parse for HtmlFor {
4434
let body_stream;
4535
braced!(body_stream in input);
4636

47-
let mut let_stmts = Vec::new();
48-
while body_stream.peek(Token![let]) {
49-
let stmt: Stmt = body_stream.parse()?;
50-
match stmt {
51-
Stmt::Local(local) => let_stmts.push(local),
52-
_ => unreachable!("peeked Token![let] but parsed non-local statement"),
53-
}
54-
}
55-
56-
let body = HtmlChildrenTree::parse_delimited_with_nodes(&body_stream)?;
57-
let deprecations = super::check_unnecessary_fragment(&body);
58-
// TODO: more concise code by using if-let guards (MSRV 1.95)
59-
for child in body.0.iter() {
60-
let HtmlTree::Element(element) = child else {
61-
continue;
62-
};
63-
64-
let Some(key) = &element.props.special.key else {
65-
continue;
66-
};
37+
let (stmts, body, deprecations) = parse_loop_body(&body_stream, "for")?;
6738

68-
if is_contextless_pure(&key.value) {
69-
return Err(syn::Error::new(
70-
key.value.span(),
71-
"duplicate key for a node in a `for`-loop\nthis will create elements with \
72-
duplicate keys if the loop iterates more than once",
73-
));
74-
}
75-
}
7639
Ok(Self {
7740
pat,
7841
iter,
79-
let_stmts,
42+
stmts,
8043
body,
8144
deprecations,
8245
})
@@ -88,57 +51,11 @@ impl ToTokens for HtmlFor {
8851
let Self {
8952
pat,
9053
iter,
91-
let_stmts,
54+
stmts,
9255
body,
9356
deprecations,
9457
} = self;
95-
let acc = Ident::new("__yew_v", iter.span());
96-
97-
let alloc_opt = body
98-
.size_hint()
99-
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
100-
.map(|size| quote!( #acc.reserve(#size) ));
101-
102-
let vlist_gen = match body.fully_keyed() {
103-
Some(true) => quote! {
104-
::yew::virtual_dom::VList::__macro_new(
105-
#acc,
106-
::std::option::Option::None,
107-
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
108-
)
109-
},
110-
Some(false) => quote! {
111-
::yew::virtual_dom::VList::__macro_new(
112-
#acc,
113-
::std::option::Option::None,
114-
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
115-
)
116-
},
117-
None => quote! {
118-
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
119-
},
120-
};
121-
122-
let body = body
123-
.0
124-
.iter()
125-
.map(|child| match child.to_node_iterator_stream() {
126-
Some(child) => {
127-
quote!( #acc.extend(#child) )
128-
}
129-
_ => {
130-
quote!( #acc.push(::std::convert::Into::into(#child)) )
131-
}
132-
});
133-
134-
tokens.extend(quote!({
135-
#deprecations
136-
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
137-
::std::iter::Iterator::for_each(
138-
::std::iter::IntoIterator::into_iter(#iter),
139-
|#pat| { #(#let_stmts)* #alloc_opt; #(#body);* }
140-
);
141-
#vlist_gen
142-
}))
58+
let header = quote!(for #pat in #iter);
59+
tokens.extend(emit_loop(header, stmts, body, deprecations));
14360
}
14461
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use proc_macro2::{Ident, Span, TokenStream};
2+
use quote::quote;
3+
use syn::parse::ParseStream;
4+
use syn::spanned::Spanned;
5+
use syn::{Expr, Stmt};
6+
7+
use super::{
8+
HtmlChildrenTree, HtmlTree, ToNodeIterator, parse_preamble_stmts, stmts_have_divergent,
9+
};
10+
11+
/// Determines if an expression is guaranteed to always return the same value anywhere.
12+
pub(super) fn is_contextless_pure(expr: &Expr) -> bool {
13+
match expr {
14+
Expr::Lit(_) => true,
15+
Expr::Path(path) => path.path.get_ident().is_none(),
16+
_ => false,
17+
}
18+
}
19+
20+
/// Parse leading Rust statements from a loop body, then the remaining children.
21+
/// Also runs duplicate-key detection keyed to `loop_kind` (e.g. "for", "while").
22+
pub(super) fn parse_loop_body(
23+
body_stream: ParseStream,
24+
loop_kind: &str,
25+
) -> syn::Result<(Vec<Stmt>, HtmlChildrenTree, TokenStream)> {
26+
let stmts = parse_preamble_stmts(body_stream)?;
27+
28+
let body = HtmlChildrenTree::parse_delimited_with_nodes(body_stream)?;
29+
let deprecations = super::check_unnecessary_fragment(&body);
30+
// TODO: more concise code by using if-let guards (MSRV 1.95)
31+
for child in body.0.iter() {
32+
let HtmlTree::Element(element) = child else {
33+
continue;
34+
};
35+
36+
let Some(key) = &element.props.special.key else {
37+
continue;
38+
};
39+
40+
if is_contextless_pure(&key.value) {
41+
return Err(syn::Error::new(
42+
key.value.span(),
43+
format!(
44+
"duplicate key for a node in a `{loop_kind}`-loop\nthis will create elements \
45+
with duplicate keys if the loop iterates more than once"
46+
),
47+
));
48+
}
49+
}
50+
51+
Ok((stmts, body, deprecations))
52+
}
53+
54+
/// Emit a loop that accumulates its body children into a `VList`.
55+
///
56+
/// `loop_header` is the native Rust loop syntax without its body, e.g.
57+
/// `for #pat in #iter` or `while #cond`.
58+
pub(super) fn emit_loop(
59+
loop_header: TokenStream,
60+
stmts: &[Stmt],
61+
body: &HtmlChildrenTree,
62+
deprecations: &TokenStream,
63+
) -> TokenStream {
64+
let acc = Ident::new("__yew_v", Span::mixed_site());
65+
66+
let alloc_opt = body
67+
.size_hint()
68+
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
69+
.map(|size| quote!( #acc.reserve(#size) ));
70+
71+
let vlist_gen = match body.fully_keyed() {
72+
Some(true) => quote! {
73+
::yew::virtual_dom::VList::__macro_new(
74+
#acc,
75+
::std::option::Option::None,
76+
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
77+
)
78+
},
79+
Some(false) => quote! {
80+
::yew::virtual_dom::VList::__macro_new(
81+
#acc,
82+
::std::option::Option::None,
83+
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
84+
)
85+
},
86+
None => quote! {
87+
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
88+
},
89+
};
90+
91+
let body_streams = body.0.iter().map(|child| match child {
92+
HtmlTree::Break(_) | HtmlTree::Continue(_) | HtmlTree::Return(_) => quote!( #child ),
93+
_ => match child.to_node_iterator_stream() {
94+
Some(stream) => quote!( #acc.extend(#stream) ),
95+
_ => quote!( #acc.push(::std::convert::Into::into(#child)) ),
96+
},
97+
});
98+
99+
let has_top_level_divergent = body.0.iter().any(|c| {
100+
matches!(
101+
c,
102+
HtmlTree::Break(_) | HtmlTree::Continue(_) | HtmlTree::Return(_)
103+
)
104+
}) || stmts_have_divergent(stmts);
105+
106+
// Nest in an inner block when divergent, so `#![allow(unreachable_code)]`
107+
// lands in an inner expression block (accepted everywhere) rather than in
108+
// an if-branch or match-arm position where it would be rejected.
109+
if has_top_level_divergent {
110+
quote!({
111+
#deprecations
112+
{
113+
#![allow(unreachable_code)]
114+
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
115+
#loop_header {
116+
#(#stmts)* #alloc_opt; #(#body_streams);*
117+
}
118+
#vlist_gen
119+
}
120+
})
121+
} else {
122+
quote!({
123+
#deprecations
124+
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
125+
#loop_header {
126+
#(#stmts)* #alloc_opt; #(#body_streams);*
127+
}
128+
#vlist_gen
129+
})
130+
}
131+
}

0 commit comments

Comments
 (0)