Skip to main content

cheers_ast/
control.rs

1use std::hash::{Hash, Hasher};
2
3use base64::Engine;
4use proc_macro2::{Ident, Span, TokenStream, TokenTree};
5use quote::{ToTokens, quote};
6use rustc_hash::FxHasher;
7use syn::{
8    Expr, LitStr, Local, Pat, Stmt, Token, braced,
9    parse::{Parse, ParseStream},
10    spanned::Spanned as _,
11    token::Brace,
12};
13
14use super::{AnyBlock, Generate, Generator, Node, Nodes};
15use crate::{
16    Attribute, AttributeKind, AttributeName, AttributeValueNode, Context, ElementNode,
17    SyntaxStatic, basics::Literal,
18};
19
20fn tokens_contain_ident(tokens: &TokenStream, needle: &str) -> bool {
21    tokens.clone().into_iter().any(|token| match token {
22        TokenTree::Ident(ident) => ident == needle,
23        TokenTree::Group(group) => tokens_contain_ident(&group.stream(), needle),
24        TokenTree::Punct(_) | TokenTree::Literal(_) => false,
25    })
26}
27
28fn tokens_contain_any_ident(tokens: &TokenStream, needles: &[Ident]) -> bool {
29    tokens.clone().into_iter().any(|token| match token {
30        TokenTree::Ident(ident) => needles.iter().any(|needle| needle == &ident),
31        TokenTree::Group(group) => tokens_contain_any_ident(&group.stream(), needles),
32        TokenTree::Punct(_) | TokenTree::Literal(_) => false,
33    })
34}
35
36fn collect_pat_bindings(pat: &Pat, bindings: &mut Vec<(TokenStream, Ident)>) {
37    match pat {
38        Pat::Ident(pat) => {
39            let mut binding = TokenStream::new();
40            pat.mutability.to_tokens(&mut binding);
41            pat.ident.to_tokens(&mut binding);
42
43            if let Some(existing) = bindings.iter_mut().find(|(_, ident)| ident == &pat.ident) {
44                *existing = (binding, pat.ident.clone());
45            } else {
46                bindings.push((binding, pat.ident.clone()));
47            }
48        }
49        Pat::Or(pat) => {
50            for case in &pat.cases {
51                collect_pat_bindings(case, bindings);
52            }
53        }
54        Pat::Paren(pat) => collect_pat_bindings(&pat.pat, bindings),
55        Pat::Reference(pat) => collect_pat_bindings(&pat.pat, bindings),
56        Pat::Slice(pat) => {
57            for elem in &pat.elems {
58                collect_pat_bindings(elem, bindings);
59            }
60        }
61        Pat::Struct(pat) => {
62            for field in &pat.fields {
63                collect_pat_bindings(&field.pat, bindings);
64            }
65        }
66        Pat::Tuple(pat) => {
67            for elem in &pat.elems {
68                collect_pat_bindings(elem, bindings);
69            }
70        }
71        Pat::TupleStruct(pat) => {
72            for elem in &pat.elems {
73                collect_pat_bindings(elem, bindings);
74            }
75        }
76        Pat::Type(pat) => collect_pat_bindings(&pat.pat, bindings),
77        Pat::Const(_)
78        | Pat::Lit(_)
79        | Pat::Macro(_)
80        | Pat::Path(_)
81        | Pat::Range(_)
82        | Pat::Rest(_)
83        | Pat::Verbatim(_)
84        | Pat::Wild(_) => {}
85        _ => {}
86    }
87}
88
89fn leading_let_bindings(
90    nodes: &[ElementNode],
91    leading_let_count: usize,
92) -> Vec<(TokenStream, Ident)> {
93    let mut bindings = Vec::new();
94
95    for node in nodes.iter().take(leading_let_count) {
96        if let ElementNode::Control(Control {
97            kind: ControlKind::Let(Let(local)),
98            ..
99        }) = node
100        {
101            collect_pat_bindings(&local.pat, &mut bindings);
102        }
103    }
104
105    bindings
106}
107
108fn leading_lets_can_be_moved_into_hot_args(
109    nodes: &[ElementNode],
110    leading_let_count: usize,
111) -> bool {
112    let mut prior_bindings = Vec::new();
113
114    for node in nodes.iter().take(leading_let_count) {
115        let ElementNode::Control(Control {
116            kind: ControlKind::Let(Let(local)),
117            ..
118        }) = node
119        else {
120            continue;
121        };
122
123        let Some(init) = &local.init else {
124            return false;
125        };
126
127        if tokens_contain_any_ident(&init.expr.to_token_stream(), &prior_bindings) {
128            return false;
129        }
130
131        let mut bindings = Vec::new();
132        collect_pat_bindings(&local.pat, &mut bindings);
133        prior_bindings.extend(bindings.into_iter().map(|(_, ident)| ident));
134    }
135
136    true
137}
138
139fn source_offset(source: &str, line: usize, column: usize) -> Option<usize> {
140    let mut offset = 0;
141    for (index, segment) in source.split_inclusive('\n').enumerate() {
142        if index + 1 == line {
143            let mut column_offset = column.min(segment.len());
144            while column_offset > 0 && !segment.is_char_boundary(column_offset) {
145                column_offset -= 1;
146            }
147            return Some(offset + column_offset);
148        }
149        offset += segment.len();
150    }
151
152    None
153}
154
155fn skip_rust_block_comment(source: &str, mut offset: usize) -> Option<usize> {
156    let bytes = source.as_bytes();
157    let mut depth = 0usize;
158
159    while offset + 1 < bytes.len() {
160        if bytes[offset] == b'/' && bytes[offset + 1] == b'*' {
161            depth += 1;
162            offset += 2;
163        } else if bytes[offset] == b'*' && bytes[offset + 1] == b'/' {
164            depth = depth.checked_sub(1)?;
165            offset += 2;
166            if depth == 0 {
167                return Some(offset);
168            }
169        } else {
170            offset += 1;
171        }
172    }
173
174    None
175}
176
177fn skip_rust_trivia(source: &str, mut offset: usize) -> usize {
178    loop {
179        let rest = &source[offset..];
180        if let Some(ch) = rest.chars().next()
181            && ch.is_whitespace()
182        {
183            offset += ch.len_utf8();
184            continue;
185        }
186
187        if rest.starts_with("//") {
188            offset += rest.find('\n').map_or(rest.len(), |newline| newline + 1);
189            continue;
190        }
191
192        if rest.starts_with("/*") {
193            offset = skip_rust_block_comment(source, offset).unwrap_or(source.len());
194            continue;
195        }
196
197        return offset;
198    }
199}
200
201fn is_ident_continue(ch: char) -> bool {
202    ch == '_' || ch.is_alphanumeric()
203}
204
205fn starts_with_async_keyword(source: &str, offset: usize) -> bool {
206    let Some(rest) = source.get(offset..) else {
207        return false;
208    };
209    let Some(after_async) = rest.strip_prefix("async") else {
210        return false;
211    };
212
213    after_async
214        .chars()
215        .next()
216        .is_none_or(|ch| !is_ident_continue(ch))
217}
218
219fn async_marker_count(source: &str) -> usize {
220    let mut count = 0;
221    let mut offset = 0;
222
223    while let Some(at_offset) = source[offset..].find('@').map(|rel| offset + rel) {
224        let after_at = skip_rust_trivia(source, at_offset + '@'.len_utf8());
225        if starts_with_async_keyword(source, after_at) {
226            count += 1;
227        }
228        offset = at_offset + '@'.len_utf8();
229    }
230
231    count
232}
233
234fn async_source_ordinal(file: &str, line: usize, column: usize) -> Option<usize> {
235    let source = std::fs::read_to_string(file).ok()?;
236    let offset = source_offset(&source, line, column)?;
237    Some(async_marker_count(&source[..offset]))
238}
239
240fn element_body_contains_async(body: &crate::ElementBody) -> bool {
241    match body {
242        crate::ElementBody::Normal { children, .. } => element_nodes_contain_async(&children.0),
243        crate::ElementBody::Void { .. } => false,
244    }
245}
246
247fn element_nodes_contain_async(nodes: &[ElementNode]) -> bool {
248    nodes.iter().any(element_node_contains_async)
249}
250
251fn element_nodes_are_static(nodes: &[ElementNode]) -> bool {
252    nodes.iter().all(SyntaxStatic::is_static)
253}
254
255fn element_node_contains_async(node: &ElementNode) -> bool {
256    match node {
257        ElementNode::Element(element) => element_body_contains_async(&element.body),
258        ElementNode::Component(component) => element_body_contains_async(&component.body),
259        ElementNode::Control(Control { kind, .. }) => element_control_contains_async(kind),
260        ElementNode::Group(group) => element_nodes_contain_async(&group.nodes.0),
261        ElementNode::Literal(_) | ElementNode::Expr(_) => false,
262    }
263}
264
265fn element_control_contains_async(kind: &ControlKind<ElementNode>) -> bool {
266    match kind {
267        ControlKind::Let(_) => false,
268        ControlKind::If(if_) => {
269            control_block_contains_async(&if_.then_block)
270                || if_
271                    .else_branch
272                    .as_ref()
273                    .is_some_and(|(_, _, branch)| control_if_or_block_contains_async(branch))
274        }
275        ControlKind::For(for_) => control_block_contains_async(&for_.block),
276        ControlKind::While(while_) => control_block_contains_async(&while_.block),
277        ControlKind::Match(match_) => match_.arms.iter().any(|arm| match &arm.body {
278            MatchNodeArmBody::Block(block) => control_block_contains_async(block),
279            MatchNodeArmBody::Node(node) => element_node_contains_async(node),
280        }),
281        ControlKind::Async(_) => true,
282    }
283}
284
285fn control_block_contains_async(block: &ControlBlock<ElementNode>) -> bool {
286    element_nodes_contain_async(&block.nodes.0)
287}
288
289fn control_if_or_block_contains_async(branch: &ControlIfOrBlock<ElementNode>) -> bool {
290    match branch {
291        ControlIfOrBlock::If(if_) => {
292            control_block_contains_async(&if_.then_block)
293                || if_
294                    .else_branch
295                    .as_ref()
296                    .is_some_and(|(_, _, branch)| control_if_or_block_contains_async(branch))
297        }
298        ControlIfOrBlock::Block(block) => control_block_contains_async(block),
299    }
300}
301
302#[allow(clippy::large_enum_variant)]
303pub enum ControlKind<N: Node> {
304    Let(Let),
305    If(If<N>),
306    For(For<N>),
307    While(While<N>),
308    Match(Match<N>),
309    Async(Async),
310}
311
312pub struct Control<N: Node> {
313    pub at_token: Token![@],
314    pub kind: ControlKind<N>,
315}
316
317impl<N: Node> SyntaxStatic for Control<N> {
318    fn is_static(&self) -> bool {
319        false
320    }
321}
322
323impl<N: Node + Parse> Parse for Control<N> {
324    fn parse(input: ParseStream) -> syn::Result<Self> {
325        let at_token = input.parse::<Token![@]>()?;
326
327        let lookahead = input.lookahead1();
328
329        let kind = if lookahead.peek(Token![let]) {
330            input.parse().map(ControlKind::Let)
331        } else if lookahead.peek(Token![if]) {
332            input.parse().map(ControlKind::If)
333        } else if lookahead.peek(Token![for]) {
334            input.parse().map(ControlKind::For)
335        } else if lookahead.peek(Token![while]) {
336            input.parse().map(ControlKind::While)
337        } else if lookahead.peek(Token![match]) {
338            input.parse().map(ControlKind::Match)
339        } else if lookahead.peek(Token![async]) {
340            input.parse().map(ControlKind::Async)
341        } else {
342            Err(lookahead.error())
343        }?;
344
345        Ok(Self { at_token, kind })
346    }
347}
348
349impl<N: Node> Generate for Control<N> {
350    const CONTEXT: Context = N::CONTEXT;
351
352    fn generate(&mut self, g: &mut Generator<'_>) {
353        match &mut self.kind {
354            ControlKind::Let(let_) => g.push(let_),
355            ControlKind::If(if_) => g.push(if_),
356            ControlKind::For(for_) => g.push(for_),
357            ControlKind::While(while_) => g.push(while_),
358            ControlKind::Match(match_) => g.push(match_),
359            ControlKind::Async(suspense) => g.push(suspense),
360        }
361    }
362}
363
364pub struct Let(pub Local);
365
366impl Parse for Let {
367    fn parse(input: ParseStream) -> syn::Result<Self> {
368        let local = match input.parse()? {
369            Stmt::Local(local) => local,
370            stmt => return Err(syn::Error::new_spanned(stmt, "expected `let` statement")),
371        };
372
373        Ok(Self(local))
374    }
375}
376
377impl Generate for Let {
378    const CONTEXT: Context = Context::Element;
379
380    fn generate(&mut self, g: &mut Generator<'_>) {
381        g.push_stmt(&self.0);
382    }
383}
384
385pub struct ControlBlock<N: Node> {
386    pub brace_token: Brace,
387    pub nodes: Nodes<N>,
388}
389
390impl<N: Node> ControlBlock<N> {
391    fn block(&mut self, g: &mut Generator<'_>) -> AnyBlock {
392        self.nodes.block(g, self.brace_token)
393    }
394}
395
396impl<N: Node + Parse> Parse for ControlBlock<N> {
397    fn parse(input: ParseStream) -> syn::Result<Self> {
398        let content;
399
400        Ok(Self {
401            brace_token: braced!(content in input),
402            nodes: content.parse()?,
403        })
404    }
405}
406
407pub struct If<N: Node> {
408    if_token: Token![if],
409    pub cond: Expr,
410    pub then_block: ControlBlock<N>,
411    pub else_branch: Option<(Token![@], Token![else], Box<ControlIfOrBlock<N>>)>,
412}
413
414impl<N: Node> If<N> {
415    pub fn if_token(&self) -> &Token![if] {
416        &self.if_token
417    }
418}
419
420impl<N: Node + Parse> Parse for If<N> {
421    fn parse(input: ParseStream) -> syn::Result<Self> {
422        Ok(Self {
423            if_token: input.parse()?,
424            cond: input.call(Expr::parse_without_eager_brace)?,
425            then_block: input.parse()?,
426            else_branch: if input.peek(Token![@]) && input.peek2(Token![else]) {
427                Some((input.parse()?, input.parse()?, input.parse()?))
428            } else {
429                None
430            },
431        })
432    }
433}
434
435impl<N: Node> Generate for If<N> {
436    const CONTEXT: Context = N::CONTEXT;
437
438    fn generate(&mut self, g: &mut Generator<'_>) {
439        fn to_expr<N: Node>(if_: &mut If<N>, g: &mut Generator<'_>) -> TokenStream {
440            let if_token = if_.if_token;
441            let cond = &if_.cond;
442            let then_block = if_.then_block.block(g);
443            let else_branch = if_
444                .else_branch
445                .as_mut()
446                .map(|(_, else_token, if_or_block)| {
447                    let else_block = match &mut **if_or_block {
448                        ControlIfOrBlock::If(if_) => to_expr(if_, g),
449                        ControlIfOrBlock::Block(block) => block.block(g).to_token_stream(),
450                    };
451
452                    quote! {
453                        #else_token #else_block
454                    }
455                });
456
457            quote! {
458                #if_token #cond
459                    #then_block
460                #else_branch
461            }
462        }
463
464        let expr = to_expr(self, g);
465
466        g.push_stmt(expr);
467    }
468}
469
470#[allow(clippy::large_enum_variant)]
471pub enum ControlIfOrBlock<N: Node> {
472    If(If<N>),
473    Block(ControlBlock<N>),
474}
475
476impl<N: Node + Parse> Parse for ControlIfOrBlock<N> {
477    fn parse(input: ParseStream) -> syn::Result<Self> {
478        let lookahead = input.lookahead1();
479
480        if lookahead.peek(Token![if]) {
481            input.parse().map(Self::If)
482        } else if lookahead.peek(Brace) {
483            input.parse().map(Self::Block)
484        } else {
485            Err(lookahead.error())
486        }
487    }
488}
489
490pub struct For<N: Node> {
491    for_token: Token![for],
492    pub pat: Pat,
493    in_token: Token![in],
494    pub expr: Expr,
495    pub block: ControlBlock<N>,
496}
497
498impl<N: Node + Parse> Parse for For<N> {
499    fn parse(input: ParseStream) -> syn::Result<Self> {
500        Ok(Self {
501            for_token: input.parse()?,
502            pat: input.call(Pat::parse_multi_with_leading_vert)?,
503            in_token: input.parse()?,
504            expr: input.call(Expr::parse_without_eager_brace)?,
505            block: input.parse()?,
506        })
507    }
508}
509
510impl<N: Node> Generate for For<N> {
511    const CONTEXT: Context = N::CONTEXT;
512
513    fn generate(&mut self, g: &mut Generator<'_>) {
514        let for_token = self.for_token;
515        let pat = &self.pat;
516        let in_token = self.in_token;
517        let expr = &self.expr;
518        let block = self.block.block(g);
519
520        g.push_stmt(quote! {
521            #for_token #pat #in_token #expr
522                #block
523        });
524    }
525}
526
527pub struct While<N: Node> {
528    while_token: Token![while],
529    pub cond: Expr,
530    pub block: ControlBlock<N>,
531}
532
533impl<N: Node + Parse> Parse for While<N> {
534    fn parse(input: ParseStream) -> syn::Result<Self> {
535        Ok(Self {
536            while_token: input.parse()?,
537            cond: input.call(Expr::parse_without_eager_brace)?,
538            block: input.parse()?,
539        })
540    }
541}
542
543impl<N: Node> Generate for While<N> {
544    const CONTEXT: Context = N::CONTEXT;
545
546    fn generate(&mut self, g: &mut Generator<'_>) {
547        let while_token = self.while_token;
548        let cond = &self.cond;
549        let block = self.block.block(g);
550
551        g.push_stmt(quote! {
552            #while_token #cond
553                #block
554        });
555    }
556}
557
558pub struct Match<N: Node> {
559    match_token: Token![match],
560    pub expr: Expr,
561    pub brace_token: Brace,
562    pub arms: Vec<MatchNodeArm<N>>,
563}
564
565impl<N: Node + Parse> Parse for Match<N> {
566    fn parse(input: ParseStream) -> syn::Result<Self> {
567        let content;
568
569        Ok(Self {
570            match_token: input.parse()?,
571            expr: input.call(Expr::parse_without_eager_brace)?,
572            brace_token: braced!(content in input),
573            arms: {
574                let mut arms = Vec::new();
575
576                while !content.is_empty() {
577                    arms.push(content.parse()?);
578                }
579
580                arms
581            },
582        })
583    }
584}
585
586impl<N: Node> Generate for Match<N> {
587    const CONTEXT: Context = N::CONTEXT;
588
589    fn generate(&mut self, g: &mut Generator<'_>) {
590        let arms = self
591            .arms
592            .iter_mut()
593            .map(|arm| {
594                let pat = arm.pat.clone();
595                let guard = arm
596                    .guard
597                    .as_ref()
598                    .map(|(if_token, guard)| quote!(#if_token #guard));
599                let fat_arrow_token = arm.fat_arrow_token;
600                let block = match &mut arm.body {
601                    MatchNodeArmBody::Block(block) => block.block(g),
602                    MatchNodeArmBody::Node(node) => {
603                        g.block_with(Brace::default(), |g| g.push(node), true)
604                    }
605                };
606                let comma = arm.comma_token;
607
608                quote!(#pat #guard #fat_arrow_token #block #comma)
609            })
610            .collect::<TokenStream>();
611
612        let match_token = self.match_token;
613        let expr = &self.expr;
614
615        let mut stmt = quote!(#match_token #expr);
616
617        self.brace_token
618            .surround(&mut stmt, |tokens| tokens.extend(arms));
619
620        g.push_stmt(stmt);
621    }
622}
623
624pub struct MatchNodeArm<N: Node> {
625    pub pat: Pat,
626    pub guard: Option<(Token![if], Expr)>,
627    fat_arrow_token: Token![=>],
628    pub body: MatchNodeArmBody<N>,
629    comma_token: Option<Token![,]>,
630}
631
632impl<N: Node> MatchNodeArm<N> {
633    pub fn fat_arrow_span(&self) -> Span {
634        self.fat_arrow_token.span()
635    }
636}
637
638impl<N: Node + Parse> Parse for MatchNodeArm<N> {
639    fn parse(input: ParseStream) -> syn::Result<Self> {
640        Ok(Self {
641            pat: input.call(Pat::parse_multi_with_leading_vert)?,
642            guard: if input.peek(Token![if]) {
643                Some((input.parse()?, input.parse()?))
644            } else {
645                None
646            },
647            fat_arrow_token: input.parse()?,
648            body: input.parse()?,
649            comma_token: input.parse()?,
650        })
651    }
652}
653
654pub enum MatchNodeArmBody<N: Node> {
655    Block(ControlBlock<N>),
656    Node(N),
657}
658
659impl<N: Node + Parse> Parse for MatchNodeArmBody<N> {
660    fn parse(input: ParseStream) -> syn::Result<Self> {
661        if input.peek(Brace) {
662            input.parse().map(Self::Block)
663        } else {
664            input.parse().map(Self::Node)
665        }
666    }
667}
668
669pub struct Async {
670    pub async_token: Token![async],
671    pub async_block: ControlBlock<ElementNode>,
672    pub else_at_token: Token![@],
673    pub else_token: Token![else],
674    pub else_block: ControlBlock<ElementNode>,
675    else_block_first_elem_idx: usize,
676}
677
678impl Parse for Async {
679    fn parse(input: ParseStream) -> syn::Result<Self> {
680        let async_token = input.parse::<Token![async]>()?;
681        let async_block = input.parse()?;
682        let else_at_token = input.parse::<Token![@]>()?;
683        let else_token = input.parse::<Token![else]>()?;
684        let mut else_block: ControlBlock<ElementNode> = input.parse()?;
685        let else_block_first_elem_idx = else_block
686            .nodes
687            .0
688            .iter_mut()
689            .position(|n| matches!(n, ElementNode::Element(_)))
690            .ok_or_else(|| {
691                syn::Error::new_spanned(
692                    else_token,
693                    "expected at least a single element in the `else` block of `async`",
694                )
695            })?;
696
697        Ok(Self {
698            async_token,
699            async_block,
700            else_at_token,
701            else_token,
702            else_block,
703            else_block_first_elem_idx,
704        })
705    }
706}
707
708impl Async {
709    fn stream_tokens_expr(
710        async_token: Token![async],
711        content_code: &TokenStream,
712        key: &str,
713    ) -> TokenStream {
714        let marker_ident = ElementNode::CONTEXT.marker_type();
715        let buffer_ident = Generator::buffer_ident();
716        let template_start = format!(r#"<template data-ssr="{key}-t">"#);
717        let stream_script =
718            format!(r#"</template><script data-ssr="{key}-s">__ssrStream('{key}')</script>"#);
719
720        quote! {
721            ::cheers::__internal::futures::stream::once(#async_token move {
722                let mut buffer = ::cheers::prelude::Buffer::<#marker_ident>::new();
723                // XSS SAFETY: the key is computed by us
724                buffer.dangerously_get_string().push_str(#template_start);
725                let #buffer_ident = &mut buffer;
726                #content_code
727                // XSS SAFETY: the key is computed by us
728                buffer.dangerously_get_string().push_str(#stream_script);
729
730                ::cheers::Raw::<_, #marker_ident>::dangerously_create(
731                    buffer.rendered().into_inner()
732                ).render()
733            })
734        }
735    }
736
737    fn stream_with_hot_render_call_tokens_expr(
738        async_token: Token![async],
739        load_code: &TokenStream,
740        render_code: &TokenStream,
741        leading_bindings: &[(TokenStream, Ident)],
742        key: &str,
743    ) -> TokenStream {
744        let marker_ident = ElementNode::CONTEXT.marker_type();
745        let buffer_ident = Generator::buffer_ident();
746        let template_start = format!(r#"<template data-ssr="{key}-t">"#);
747        let stream_script =
748            format!(r#"</template><script data-ssr="{key}-s">__ssrStream('{key}')</script>"#);
749        let binding_params = leading_bindings.iter().map(|(param, _)| param);
750        let binding_args = leading_bindings.iter().map(|(_, arg)| arg);
751
752        quote! {
753            ::cheers::__internal::futures::stream::once(#async_token move {
754                #load_code
755
756                let mut buffer = ::cheers::prelude::Buffer::<#marker_ident>::new();
757                // XSS SAFETY: the key is computed by us
758                buffer.dangerously_get_string().push_str(#template_start);
759                let #buffer_ident = &mut buffer;
760                ::cheers::__internal::subsecond::hot_call_with_arg(
761                    |(#buffer_ident, #(#binding_params),*)| {
762                        use ::cheers::validation::attributes::*;
763                        #render_code
764                    },
765                    (#buffer_ident, #(#binding_args),*),
766                );
767                // XSS SAFETY: the key is computed by us
768                buffer.dangerously_get_string().push_str(#stream_script);
769
770                ::cheers::Raw::<_, #marker_ident>::dangerously_create(
771                    buffer.rendered().into_inner()
772                ).render()
773            })
774        }
775    }
776
777    fn stream_with_nested_tokens_expr(
778        async_token: Token![async],
779        content_code: &TokenStream,
780        key: &str,
781    ) -> TokenStream {
782        let marker_ident = ElementNode::CONTEXT.marker_type();
783        let buffer_ident = Generator::buffer_ident();
784        let template_start = format!(r#"<template data-ssr="{key}-t">"#);
785        let stream_script =
786            format!(r#"</template><script data-ssr="{key}-s">__ssrStream('{key}')</script>"#);
787
788        quote! {
789            ::cheers::__internal::futures::StreamExt::flat_map(
790                ::cheers::__internal::futures::stream::once(#async_token move {
791                    let mut buffer = ::std::boxed::Box::new(
792                        ::cheers::prelude::Buffer::<#marker_ident>::new()
793                    );
794                    let __cheers_async_stream_collection =
795                        ::cheers::__internal::async_streams::enter(&mut *buffer);
796                    // XSS SAFETY: the key is computed by us
797                    buffer.dangerously_get_string().push_str(#template_start);
798                    let #buffer_ident = &mut *buffer;
799                    #content_code
800                    // XSS SAFETY: the key is computed by us
801                    buffer.dangerously_get_string().push_str(#stream_script);
802
803                    let __cheers_nested_streams = __cheers_async_stream_collection.finish();
804                    let buffer = *buffer;
805                    let __cheers_parent_rendered = ::cheers::Raw::<_, #marker_ident>::dangerously_create(
806                        buffer.rendered().into_inner()
807                    ).render();
808
809                    (__cheers_parent_rendered, __cheers_nested_streams)
810                }),
811                |(__cheers_parent_rendered, __cheers_nested_streams)| {
812                    ::cheers::__internal::futures::StreamExt::chain(
813                        ::cheers::__internal::futures::stream::once(async move {
814                            __cheers_parent_rendered
815                        }),
816                        ::cheers::__internal::futures::stream::select_all(
817                            __cheers_nested_streams
818                        ),
819                    )
820                },
821            )
822        }
823    }
824
825    fn hot_island_stream_tokens_expr(
826        async_token: Token![async],
827        load_code: &TokenStream,
828        render_code: &TokenStream,
829        key: &str,
830    ) -> TokenStream {
831        let marker_ident = ElementNode::CONTEXT.marker_type();
832        let buffer_ident = Generator::buffer_ident();
833        let template_start = format!(r#"<template data-ssr="{key}-t">"#);
834        let stream_script =
835            format!(r#"</template><script data-ssr="{key}-s">__ssrStream('{key}')</script>"#);
836
837        quote! {
838            ::cheers::__internal::futures::stream::once(#async_token move {
839                #load_code
840
841                let __cheers_async_island_render_fn: fn() -> ::std::string::String = || {
842                    let mut buffer = ::cheers::prelude::Buffer::<#marker_ident>::new();
843                    let #buffer_ident = &mut buffer;
844                    use ::cheers::validation::attributes::*;
845                    #render_code
846                    buffer.rendered().into_inner()
847                };
848                let mut __cheers_async_island_render = move || {
849                    ::cheers::__internal::subsecond::call(__cheers_async_island_render_fn)
850                };
851
852                let __cheers_async_island_html = __cheers_async_island_render();
853                ::cheers::__internal::async_islands::register(
854                    #key,
855                    __cheers_async_island_render,
856                );
857
858                let mut buffer = ::cheers::prelude::Buffer::<#marker_ident>::new();
859                // XSS SAFETY: the key is computed by us
860                buffer.dangerously_get_string().push_str(#template_start);
861                // XSS SAFETY: the async-island render body is generated by Cheers' renderer.
862                buffer.dangerously_get_string().push_str(&__cheers_async_island_html);
863                // XSS SAFETY: the key is computed by us
864                buffer.dangerously_get_string().push_str(#stream_script);
865
866                ::cheers::Raw::<_, #marker_ident>::dangerously_create(
867                    buffer.rendered().into_inner()
868                ).render()
869            })
870        }
871    }
872
873    fn add_data_ssr_key(&mut self) -> String {
874        let key = {
875            let span = self.async_token.span;
876            let file = span.file();
877            let start = span.start();
878            let line = start.line;
879            let column = start.column;
880
881            let mut hasher = FxHasher::default();
882            file.hash(&mut hasher);
883            // Keep the browser-side streaming key stable across edits that only move this
884            // `@async` block up or down. Subsecond can temporarily serve a mix of old and new
885            // hot-patched functions; if the fallback anchor and the async stream chunk disagree
886            // because a line above the block was deleted, the page remains stuck on the fallback.
887            // Use the block's source-order ordinal rather than its absolute line number; fall back
888            // to the line only when the source file cannot be read by the proc macro host.
889            async_source_ordinal(&file, line, column)
890                .unwrap_or(line)
891                .hash(&mut hasher);
892            column.hash(&mut hasher);
893            let hash64 = hasher.finish();
894            let hash32 = (hash64 as u32) ^ ((hash64 >> 32) as u32);
895            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash32.to_be_bytes())
896        };
897        let elem = self
898            .else_block
899            .nodes
900            .0
901            .get_mut(self.else_block_first_elem_idx)
902            .expect("the else block to have at least a single element node");
903        if let ElementNode::Element(elem) = elem {
904            elem.attrs.push(Attribute::Regular {
905                name: AttributeName::Unchecked(LitStr::new("data-ssr", Span::mixed_site())),
906                kind: AttributeKind::Value {
907                    value: AttributeValueNode::Literal(Literal::Str(LitStr::new(
908                        &key,
909                        Span::mixed_site(),
910                    ))),
911                    toggle: None,
912                },
913            });
914        } else {
915            panic!("the first element node is not an element")
916        }
917
918        key
919    }
920}
921
922impl Generate for Async {
923    const CONTEXT: Context = ElementNode::CONTEXT;
924
925    fn generate(&mut self, g: &mut Generator<'_>) {
926        let source_key = self.add_data_ssr_key();
927        let leading_let_count = self
928            .async_block
929            .nodes
930            .0
931            .iter()
932            .take_while(|node| {
933                matches!(
934                    node,
935                    ElementNode::Control(Control {
936                        kind: ControlKind::Let(_),
937                        ..
938                    })
939                )
940            })
941            .count();
942        let render_nodes = &self.async_block.nodes.0[leading_let_count..];
943        let has_nested_async = element_nodes_contain_async(render_nodes);
944        let render_is_static = element_nodes_are_static(render_nodes);
945        let leading_bindings = leading_let_bindings(&self.async_block.nodes.0, leading_let_count);
946        let can_split_leading_lets = leading_let_count > 0 && !has_nested_async;
947
948        let async_token = self.async_token;
949        let else_block = self.else_block.block(g);
950        let buffer_ident = Generator::buffer_ident();
951        let async_root_start =
952            format!(r#"<div data-cheers-async-root="{source_key}" data-ssr="{source_key}">"#);
953        let else_block = quote! {
954            if ::cheers::__internal::async_islands::enabled() {
955                // XSS SAFETY: the key is computed by us
956                #buffer_ident.dangerously_get_string().push_str(#async_root_start);
957                #else_block
958                // XSS SAFETY: static wrapper markup
959                #buffer_ident.dangerously_get_string().push_str("</div>");
960            } else {
961                #else_block
962            }
963        };
964
965        let async_block = if has_nested_async {
966            g.with_async_stream_collection(|g| {
967                g.block_with(
968                    self.async_block.brace_token,
969                    |g| {
970                        g.push(&mut self.async_block.nodes);
971                    },
972                    false,
973                )
974            })
975        } else if can_split_leading_lets {
976            g.block_with(
977                self.async_block.brace_token,
978                |g| {
979                    for node in self.async_block.nodes.0.iter_mut().skip(leading_let_count) {
980                        g.push(node);
981                    }
982                },
983                false,
984            )
985        } else {
986            g.block_with(
987                self.async_block.brace_token,
988                |g| {
989                    g.push(&mut self.async_block.nodes);
990                },
991                false,
992            )
993        };
994        let content_code = &async_block.stmts;
995        debug_assert!(
996            async_block.async_stmts.is_empty(),
997            "nested @async streams should be emitted through the buffer-scoped collector"
998        );
999
1000        let load_code = if can_split_leading_lets {
1001            self.async_block
1002                .nodes
1003                .0
1004                .iter()
1005                .take(leading_let_count)
1006                .map(|node| match node {
1007                    ElementNode::Control(Control {
1008                        kind: ControlKind::Let(Let(local)),
1009                        ..
1010                    }) => {
1011                        quote!(#local;)
1012                    }
1013                    _ => TokenStream::new(),
1014                })
1015                .collect::<TokenStream>()
1016        } else {
1017            TokenStream::new()
1018        };
1019
1020        let render_contains_await = tokens_contain_ident(content_code, "await");
1021        let can_register_hot_island = render_is_static && !has_nested_async;
1022        let can_move_leading_lets_into_hot_args = can_split_leading_lets
1023            && leading_lets_can_be_moved_into_hot_args(
1024                &self.async_block.nodes.0,
1025                leading_let_count,
1026            );
1027        // Keep the dynamic async hot boundary when the load phase can be split from
1028        // the render phase. Leading `@let` bindings are passed into the hot closure
1029        // as arguments so the render body may move them without making the closure
1030        // `FnOnce`. Skip this when a leading initializer borrows an earlier leading
1031        // binding; moving those bindings into tuple arguments would invalidate the
1032        // borrow relationship.
1033        let can_hot_call_dynamic_render =
1034            can_move_leading_lets_into_hot_args && !render_contains_await;
1035
1036        let async_stream = if !has_nested_async {
1037            let stream = if can_register_hot_island {
1038                Self::hot_island_stream_tokens_expr(
1039                    async_token,
1040                    &load_code,
1041                    content_code,
1042                    &source_key,
1043                )
1044            } else if can_hot_call_dynamic_render {
1045                Self::stream_with_hot_render_call_tokens_expr(
1046                    async_token,
1047                    &load_code,
1048                    content_code,
1049                    &leading_bindings,
1050                    &source_key,
1051                )
1052            } else if can_split_leading_lets {
1053                let stream_content_code = quote! {
1054                    #load_code
1055                    #content_code
1056                };
1057                Self::stream_tokens_expr(async_token, &stream_content_code, &source_key)
1058            } else {
1059                Self::stream_tokens_expr(async_token, content_code, &source_key)
1060            };
1061            quote! {
1062                {
1063                    ::std::boxed::Box::pin(#stream) as ::std::pin::Pin<::std::boxed::Box<dyn ::cheers::__internal::futures::stream::Stream<Item = ::cheers::Rendered<::std::string::String>> + ::std::marker::Send>>
1064                }
1065            }
1066        } else {
1067            let stream =
1068                Self::stream_with_nested_tokens_expr(async_token, content_code, &source_key);
1069            quote! {
1070                {
1071                    #stream
1072                }
1073            }
1074        };
1075
1076        g.push_async_stmt(async_stream);
1077        g.push_stmt(else_block);
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use quote::quote;
1084
1085    use super::async_marker_count;
1086    use crate::{Document, generate::lazy};
1087
1088    #[test]
1089    fn async_marker_count_accepts_rust_trivia_after_at() {
1090        let source = concat!(
1091            "@async {}\n",
1092            "@ async {}\n",
1093            "@\nasync {}\n",
1094            "@/* comment */async {}\n",
1095            "@ /* nested /* comment */ still comment */ async {}\n",
1096            "@asyncness {}\n",
1097        );
1098
1099        assert_eq!(async_marker_count(source), 5);
1100    }
1101
1102    #[test]
1103    fn split_dynamic_async_render_uses_argument_hot_call() {
1104        let expanded = lazy::<Document>(quote! {
1105            div {
1106                @async {
1107                    @let items = load_items().await;
1108                    List items;
1109                } @else {
1110                    p { "Loading" }
1111                }
1112            }
1113        })
1114        .expect("document should generate")
1115        .to_string();
1116
1117        assert!(expanded.contains("hot_call_with_arg"), "{expanded}");
1118    }
1119
1120    #[test]
1121    fn dependent_leading_lets_skip_argument_hot_call() {
1122        let expanded = lazy::<Document>(quote! {
1123            div {
1124                @async {
1125                    @let owner = String::from("Data");
1126                    @let borrow = owner.as_str();
1127                    p { (borrow) }
1128                } @else {
1129                    p { "Loading" }
1130                }
1131            }
1132        })
1133        .expect("document should generate")
1134        .to_string();
1135
1136        assert!(!expanded.contains("hot_call_with_arg"), "{expanded}");
1137    }
1138
1139    #[test]
1140    fn async_render_with_await_skips_argument_hot_call() {
1141        let expanded = lazy::<Document>(quote! {
1142            div {
1143                @async {
1144                    @let items = load_items().await;
1145                    p { (render_later(items).await) }
1146                } @else {
1147                    p { "Loading" }
1148                }
1149            }
1150        })
1151        .expect("document should generate")
1152        .to_string();
1153
1154        assert!(!expanded.contains("hot_call_with_arg"), "{expanded}");
1155    }
1156}