Skip to content

feat: auto-detect imperative for/while/loop in preamble#4146

Open
Madoshakalaka wants to merge 2 commits intomasterfrom
auto-imperative-preamble
Open

feat: auto-detect imperative for/while/loop in preamble#4146
Madoshakalaka wants to merge 2 commits intomasterfrom
auto-imperative-preamble

Conversation

@Madoshakalaka
Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka commented Apr 30, 2026

Description

First off, the final diagnostics patch in b9fe62a in #4124 seems unnecessary. Either let _ = for {...} or a trailing comma (for {...};) seems to do the same job and the latter should have definitely be recommended. I mistakenly thought let _ = was the only workaround in some edge cases, tried various patterns today and can't really reproduce.

This PR should finally remove the need for all these temporary workarounds though.

This PR now auto-detects imperative for/while/loop blocks in html! preamble position so users no longer need a trailing ; or a let _ = binding to make them parse as Rust statements.

Details

Before, this was an error:

html! {
    for item in items.iter() {
        let mut by_han = BTreeMap::new();
        for src in &item.sources {
            by_han.entry(src.han_nom.clone()).or_default().push(src);
        }
        <div>{render(by_han)}</div>
    }
}

The inner for parses as Stmt::Expr(ForLoop, None) and used to be rejected with a help message pointing at let _ = ...;. The reason the parser was conservative: a bare imperative loop in preamble looks identical to html-for-control-flow with a body that has no html children. The two interpretations are indistinguishable on syntax alone.

But they are not indistinguishable in practice. If syn can fully parse the loop as a Rust Stmt, the entire expression contains no html elements anywhere inside, and an html-for over an html-less body would just emit empty VLists per iteration which is rarely what anyone wants. So we accept it as a Rust statement instead.

When the body does contain html, syn fails to parse it as Stmt and we fall through to the html-control-flow parser exactly as before. if/match/bare-{...} are unchanged.

The trailing-; and let _ = ...; forms still work as regression patterns.

Checklist

  • I have reviewed my own code
  • I have added tests
    • packages/yew-macro/tests/html_macro/imperative-preamble-pass.rs (new): mirrors the deleted imperative-preamble-fail.rs, plus labeled-loop and back-to-back loop coverage.
    • packages/yew/tests/html_for.rs: three new wasm tests asserting the inner-loop side effects run and that the surrounding html-for emits one node per outer iteration (for_imperative_inner_for_runs_as_preamble, ..._while_..., ..._loop_...).
    • packages/yew-macro/tests/html_macro/for-pass.rs: regression cases proving the trailing-; and let _ = ...; forms still parse.
    • Deleted imperative-preamble-fail.{rs,stderr} since the cases now compile cleanly.

When a fork-parsed `Stmt::Expr(For/While/Loop, None)` succeeds, the
entire expression is Rust-parseable and contains no html elements, so
it cannot be a misread of html-control-flow. Accept it as a preamble
statement instead of erroring with the `let _ = ...;` hint.

Users no longer need a trailing `;` or a `let _ =` binding to use
imperative side-effect loops inside an html! body.
@Madoshakalaka Madoshakalaka added the A-yew-macro Area: The yew-macro crate label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Visit the preview URL for this PR (updated for commit e611fb0):

https://yew-rs--pr4146-auto-imperative-prea-6fnzvy2o.web.app

(expires Thu, 07 May 2026 08:14:06 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.748 ns      │ 2.986 ns      │ 2.75 ns       │ 2.756 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.777 ns      │ 4.037 ns      │ 2.78 ns       │ 2.82 ns       │ 100     │ 1000000000

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
actix_ssr_router 609.500 609.500 0 0.000%
async_clock 99.919 99.919 0 0.000%
axum_ssr_router 609.505 609.505 0 0.000%
boids 163.660 163.660 0 0.000%
communication_child_to_parent 93.377 93.377 0 0.000%
communication_grandchild_with_grandparent 105.390 105.390 0 0.000%
communication_grandparent_to_grandchild 101.745 101.745 0 0.000%
communication_parent_to_child 90.806 90.806 0 0.000%
contexts 105.662 105.662 0 0.000%
counter 85.636 85.636 0 0.000%
counter_functional 87.655 87.655 0 0.000%
dyn_create_destroy_apps 89.521 89.521 0 0.000%
file_upload 98.662 98.662 0 0.000%
function_delayed_input 94.246 94.246 0 0.000%
function_memory_game 169.229 169.229 0 0.000%
function_router 398.322 398.322 0 0.000%
function_todomvc 163.973 163.973 0 0.000%
futures 234.340 234.340 0 0.000%
game_of_life 100.308 100.308 0 0.000%
immutable 258.517 258.517 0 0.000%
inner_html 80.506 80.506 0 0.000%
js_callback 109.485 109.485 0 0.000%
keyed_list 175.788 175.788 0 0.000%
mount_point 83.860 83.860 0 0.000%
nested_list 112.597 112.597 0 0.000%
node_refs 91.393 91.393 0 0.000%
password_strength 1719.030 1719.030 0 0.000%
portals 93.036 93.036 0 0.000%
router 365.084 365.084 0 0.000%
suspense 113.380 113.380 0 0.000%
timer 88.211 88.211 0 0.000%
timer_functional 99.052 99.052 0 0.000%
todomvc 141.029 141.029 0 0.000%
two_apps 85.831 85.831 0 0.000%
web_worker_fib 136.153 136.153 0 0.000%
web_worker_prime 184.490 184.490 0 0.000%
webgl 82.649 82.649 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 268.956 282.374 270.357 4.223
Hello World 10 378.328 381.752 380.539 1.118
Function Router 10 25880.837 26097.183 26028.647 63.309
Concurrent Task 10 1004.231 1006.530 1005.962 0.699
Many Providers 10 796.734 809.100 802.776 3.943

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 268.953 270.130 269.180 0.349
Hello World 10 376.156 415.828 382.832 11.703
Function Router 10 26056.289 26117.900 26094.026 17.996
Concurrent Task 10 1005.236 1006.643 1006.029 0.456
Many Providers 10 814.056 834.683 823.776 6.208

@Madoshakalaka
Copy link
Copy Markdown
Member Author

e611fb0 adds caution and workarounds on the website for this slightly relevant footgun pattern:

html! {
    if condition {
        for _ in 0..10 {
            {my_foo}
        }
    }
}

When a user writes this. They probally want to render my_foo conditionally 10 times, while this code actually fails to compile because html! treats the for loop as a preamble statement which can't return anything in Rust.

@Madoshakalaka Madoshakalaka marked this pull request as ready for review April 30, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew-macro Area: The yew-macro crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant