Skip to main content

aver/ir/
buffer_build.rs

1//! Buffer-build sink detection.
2//!
3//! Identifies user fns that match the canonical functional list-builder
4//! shape consumed by `String.join`:
5//!
6//! ```aver
7//! fn build(..., acc: List<T>) -> List<T>
8//!     match <cond>
9//!         true  -> List.reverse(acc)
10//!         false -> build(..., List.prepend(<elem>, acc))
11//! ```
12//!
13//! When such a fn is called from `String.join(build(..., []), sep)`, the
14//! whole pipeline is semantically equivalent to a single buffer-write
15//! loop — Wadler 1990 shortcut fusion / deforestation. This module is
16//! Phase 1 of the deforestation work for 0.15 "Traversal": it detects
17//! candidate fns. Lowering (rewriting matched fns + their `String.join`
18//! call sites) lives in a separate pass.
19//!
20//! Detection is intentionally local — the analyzer looks only at the fn
21//! body, not its call sites. A matched fn may or may not actually be
22//! consumed by `String.join`; the lowering pass cross-references call
23//! sites separately and only fuses when both ends of the pipeline agree.
24
25use std::collections::HashMap;
26use std::sync::Arc;
27
28use crate::ast::{Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, TailCallData};
29
30/// Where the matched builder puts the `List.reverse` step that gives
31/// the result its forward order.
32///
33/// `prepend(elem, acc)` builds the accumulator in reverse-of-input
34/// order. To get a forward list (the order we'd hand to `String.join`)
35/// it has to be reversed exactly once. Two equally common Aver idioms
36/// place that reverse in different spots:
37///
38/// - `InternalReverse`: the sink itself has `true -> List.reverse(acc)`
39///   in its base case. Caller writes `String.join(<sink>(args, []), sep)`.
40///   This is the classic "loop with reversed accumulator + reverse on
41///   exit" shape.
42/// - `ExternalReverse`: the sink has `[] -> acc` (no reverse) and
43///   matches on the input list directly. Caller writes
44///   `String.join(List.reverse(<sink>(args, [])), sep)`. Common in the
45///   payment_ops / workflow_engine codebases under the `*Into` naming
46///   convention (e.g. `serializeEntriesInto`, `filterSubjectInto`).
47///
48/// Both shapes lower to the same buffered variant — appending in
49/// processing order (which is forward order of the input) yields the
50/// final string in the right order without any explicit reverse.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum BufferBuildKind {
53    InternalReverse,
54    ExternalReverse,
55}
56
57/// Information about a fn that matches the buffer-build sink shape.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct BufferBuildShape {
60    /// 0-based index of the `acc: List<T>` parameter in the fn signature.
61    /// Identifies which arg in tail-call positions threads the
62    /// accumulator and which `Ident` in the base-case arm is returned.
63    pub acc_param_idx: usize,
64    /// The accumulator parameter's binding name (looked up in tail-call
65    /// args and in the base-case return position).
66    pub acc_param_name: String,
67    /// Which of the two reverse-placement idioms this sink follows;
68    /// determines (a) what shape the original base arm has and (b)
69    /// whether the call site is `String.join(<sink>(...), sep)` or
70    /// `String.join(List.reverse(<sink>(...)), sep)`.
71    pub kind: BufferBuildKind,
72}
73
74/// What the matched builder feeds into. Different consumers compile
75/// to different buffer types and finalizers, but all share the same
76/// underlying deforestation: skip the intermediate List, write
77/// elements straight to the consumer's storage.
78///
79/// Phase 2 implements `StringJoin` only — the canonical case from the
80/// fractal demo. Future variants land as separate phases:
81/// `VectorFromList` (already half-fused via `Vector.set` owned-mutate
82/// in 0.14.0; deforestation closes the cons-cell side), and `ListFold`
83/// for stream-fusion-style consumer rewrites.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum ConsumerKind {
86    /// `String.join(builder(...), sep)` — write each element + sep
87    /// directly into a `Vec<u8>`-shaped buffer in linear memory.
88    StringJoin,
89}
90
91/// One detected fusion site: a builder call whose result is consumed
92/// by a known sink (currently just `String.join`). Lowering rewrites
93/// the producer + consumer pair into a single buffer-write loop.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct FusionSite {
96    /// Name of the enclosing user fn that contains the call.
97    pub enclosing_fn: String,
98    /// Line of the consumer call.
99    pub line: usize,
100    /// The matched buffer-build fn being wrapped.
101    pub sink_fn: String,
102    /// What's consuming the builder's result.
103    pub consumer: ConsumerKind,
104}
105
106/// Walk all fns in `fns`, return a map from fn name to detected shape
107/// for fns that match the buffer-build sink pattern. Fns that don't
108/// match are absent from the result.
109pub fn compute_buffer_build_sinks(fns: &[&FnDef]) -> HashMap<String, BufferBuildShape> {
110    let mut out = HashMap::new();
111    for fd in fns {
112        if let Some(shape) = match_buffer_build_shape(fd) {
113            out.insert(fd.name.clone(), shape);
114        }
115    }
116    out
117}
118
119/// Walk every expression in every fn body looking for fusion sites:
120/// `String.join(matched_fn(...), sep)` calls where `matched_fn` is a
121/// key in `sinks`. Returns one `FusionSite` per call. The lowering
122/// pass rewrites each site to call a buffered variant of `matched_fn`
123/// directly into a pre-allocated buffer.
124pub fn find_fusion_sites(
125    fns: &[&FnDef],
126    sinks: &HashMap<String, BufferBuildShape>,
127) -> Vec<FusionSite> {
128    let mut out = Vec::new();
129    for fd in fns {
130        for stmt in fd.body.stmts() {
131            match stmt {
132                Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
133                    walk_expr_for_fusion_sites(&expr.node, expr.line, &fd.name, sinks, &mut out);
134                }
135            }
136        }
137    }
138    out
139}
140
141/// Recursively walk an expression tree, recording any fusion site we
142/// find. The fallback `expr_line` is used when a sub-expression has no
143/// own line info.
144fn walk_expr_for_fusion_sites(
145    expr: &Expr,
146    expr_line: usize,
147    enclosing_fn: &str,
148    sinks: &HashMap<String, BufferBuildShape>,
149    out: &mut Vec<FusionSite>,
150) {
151    if let Some(inner_name) = match_string_join_fusion_site(expr, sinks) {
152        out.push(FusionSite {
153            enclosing_fn: enclosing_fn.to_string(),
154            line: expr_line,
155            sink_fn: inner_name,
156            consumer: ConsumerKind::StringJoin,
157        });
158    }
159    // Recurse into all sub-expressions regardless of whether this node
160    // matched (a fusion site can sit inside another fusion site's args
161    // — rare but valid; we'd record both and let the lowering decide).
162    visit_subexprs(expr, expr_line, enclosing_fn, sinks, out);
163}
164
165/// Helper: recurse into the sub-expressions of `expr`. Mirrors the
166/// shape coverage of `expr_allocates` in `alloc_info.rs` so we don't
167/// miss any node kind.
168fn visit_subexprs(
169    expr: &Expr,
170    fallback_line: usize,
171    enclosing_fn: &str,
172    sinks: &HashMap<String, BufferBuildShape>,
173    out: &mut Vec<FusionSite>,
174) {
175    let line_of = |s: &crate::ast::Spanned<Expr>| {
176        if s.line > 0 { s.line } else { fallback_line }
177    };
178    match expr {
179        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } | Expr::Constructor(_, None) => {}
180        Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::ErrorProp(inner) => {
181            walk_expr_for_fusion_sites(&inner.node, line_of(inner), enclosing_fn, sinks, out);
182        }
183        Expr::FnCall(callee, args) => {
184            walk_expr_for_fusion_sites(&callee.node, line_of(callee), enclosing_fn, sinks, out);
185            for a in args {
186                walk_expr_for_fusion_sites(&a.node, line_of(a), enclosing_fn, sinks, out);
187            }
188        }
189        Expr::TailCall(data) => {
190            for a in &data.args {
191                walk_expr_for_fusion_sites(&a.node, line_of(a), enclosing_fn, sinks, out);
192            }
193        }
194        Expr::BinOp(_, l, r) => {
195            walk_expr_for_fusion_sites(&l.node, line_of(l), enclosing_fn, sinks, out);
196            walk_expr_for_fusion_sites(&r.node, line_of(r), enclosing_fn, sinks, out);
197        }
198        Expr::Neg(inner) => {
199            walk_expr_for_fusion_sites(&inner.node, line_of(inner), enclosing_fn, sinks, out);
200        }
201        Expr::Match { subject, arms } => {
202            walk_expr_for_fusion_sites(&subject.node, line_of(subject), enclosing_fn, sinks, out);
203            for arm in arms {
204                walk_expr_for_fusion_sites(
205                    &arm.body.node,
206                    line_of(&arm.body),
207                    enclosing_fn,
208                    sinks,
209                    out,
210                );
211            }
212        }
213        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
214            for it in items {
215                walk_expr_for_fusion_sites(&it.node, line_of(it), enclosing_fn, sinks, out);
216            }
217        }
218        Expr::MapLiteral(entries) => {
219            for (k, v) in entries {
220                walk_expr_for_fusion_sites(&k.node, line_of(k), enclosing_fn, sinks, out);
221                walk_expr_for_fusion_sites(&v.node, line_of(v), enclosing_fn, sinks, out);
222            }
223        }
224        Expr::RecordCreate { fields, .. } => {
225            for (_, v) in fields {
226                walk_expr_for_fusion_sites(&v.node, line_of(v), enclosing_fn, sinks, out);
227            }
228        }
229        Expr::RecordUpdate { base, updates, .. } => {
230            walk_expr_for_fusion_sites(&base.node, line_of(base), enclosing_fn, sinks, out);
231            for (_, v) in updates {
232                walk_expr_for_fusion_sites(&v.node, line_of(v), enclosing_fn, sinks, out);
233            }
234        }
235        Expr::InterpolatedStr(parts) => {
236            for part in parts {
237                if let crate::ast::StrPart::Parsed(inner) = part {
238                    walk_expr_for_fusion_sites(
239                        &inner.node,
240                        line_of(inner),
241                        enclosing_fn,
242                        sinks,
243                        out,
244                    );
245                }
246            }
247        }
248    }
249}
250
251/// Pattern-match a single fn against the buffer-build shape.
252fn match_buffer_build_shape(fd: &FnDef) -> Option<BufferBuildShape> {
253    // The accumulator must be a parameter of type `List<...>`. The
254    // params vector stores type strings, not parsed `Type` values, so we
255    // match the textual form. Aver's surface syntax accepts both
256    // `List<T>` and (rarely) `[T]`-like sugar; canonical form is
257    // `List<T>`.
258    // Take the *rightmost* List<...> parameter as the accumulator. The
259    // InternalReverse shape (`fn build(n: Int, acc: List<T>)`) typically
260    // has only one list param; the ExternalReverse shape often has two
261    // (`fn build(input: List<T>, acc: List<U>)`) and the accumulator is
262    // by convention the last argument. Picking the first match would
263    // misidentify `input` as the accumulator and the rest of the
264    // detection would silently fail.
265    let (acc_idx, acc_name) = fd
266        .params
267        .iter()
268        .enumerate()
269        .rfind(|(_, (_, ty))| is_list_type_str(ty))
270        .map(|(i, (name, _))| (i, name.clone()))?;
271
272    // Body must be a single expression statement holding the match.
273    let match_expr = single_match_body(&fd.body)?;
274    let (subject_expr, arms) = match match_expr {
275        Expr::Match { subject, arms } => (subject, arms),
276        _ => return None,
277    };
278
279    // Try the InternalReverse shape first: `match <bool> { true -> List.reverse(acc); false -> recurse(... prepend(_, acc)) }`.
280    if let Some((true_body, false_body)) = pair_bool_arms(arms) {
281        let _ = subject_expr;
282        if is_list_reverse_of(true_body, &acc_name)
283            && is_self_tail_with_prepend_acc(false_body, &fd.name, acc_idx, &acc_name)
284        {
285            return Some(BufferBuildShape {
286                acc_param_idx: acc_idx,
287                acc_param_name: acc_name,
288                kind: BufferBuildKind::InternalReverse,
289            });
290        }
291    }
292
293    // Otherwise try the ExternalReverse shape: `match <list> { [] -> acc;
294    // [_, .._] -> recurse(... prepend(_, acc)) }`. The reverse lives at
295    // the caller, e.g. `List.reverse(<this>(args, []))` (see the
296    // `BufferBuildKind` doc above for context).
297    if let Some((nil_body, cons_body)) = pair_nil_cons_arms(arms)
298        && is_ident_named(nil_body, &acc_name)
299        && is_self_tail_with_prepend_acc(cons_body, &fd.name, acc_idx, &acc_name)
300    {
301        return Some(BufferBuildShape {
302            acc_param_idx: acc_idx,
303            acc_param_name: acc_name,
304            kind: BufferBuildKind::ExternalReverse,
305        });
306    }
307
308    None
309}
310
311/// Match a 2-arm match where one arm is `[]` and the other is
312/// `[head, ..tail]`. Returns `(nil_body, cons_body)` (both as `&Expr`,
313/// matching `pair_bool_arms`). `None` if the arms don't exactly cover
314/// those two patterns.
315fn pair_nil_cons_arms(arms: &[MatchArm]) -> Option<(&Expr, &Expr)> {
316    if arms.len() != 2 {
317        return None;
318    }
319    let mut nil_body: Option<&Expr> = None;
320    let mut cons_body: Option<&Expr> = None;
321    for arm in arms {
322        match &arm.pattern {
323            Pattern::EmptyList => nil_body = Some(&arm.body.node),
324            Pattern::Cons(_, _) => cons_body = Some(&arm.body.node),
325            _ => return None,
326        }
327    }
328    match (nil_body, cons_body) {
329        (Some(n), Some(c)) => Some((n, c)),
330        _ => None,
331    }
332}
333
334/// True if `expr` is just an identifier reference to `name`.
335fn is_ident_named(expr: &Expr, name: &str) -> bool {
336    matches!(expr, Expr::Ident(n) if n == name)
337}
338
339/// Recognise a `String.join` first-arg as either a direct sink call or
340/// a `List.reverse(<sink>(args, []))` wrapper. Returns the matched
341/// sink fn name when the kind matches the shape we expect:
342///   InternalReverse sinks → only the direct call form
343///   ExternalReverse sinks → only the `List.reverse(...)` form
344/// Returning the name only when the kinds line up keeps us from
345/// accidentally fusing a sink against a call site that's missing
346/// its required reverse (or has an extraneous one).
347/// Single source of truth for "is this expression a rewriteable
348/// `String.join(<sink>(args, []), sep)` fusion site?". Returns the
349/// matched sink name when **all** preconditions hold:
350/// 1. The expression is a `String.join(<inner>, _)` call.
351/// 2. `<inner>` is either a direct sink call (for InternalReverse
352///    sinks) or `List.reverse(<sink>(...))` (for ExternalReverse).
353/// 3. The reverse-placement on the call site matches the sink's kind
354///    — mismatch would silently drop or double-reverse.
355/// 4. The acc-position arg in the inner call is a literal empty list.
356///    Anything else means the user is starting the fold with a
357///    non-empty accumulator, and the buffered rewrite would silently
358///    drop those initial elements.
359///
360/// Both `find_fusion_sites` (diagnostics) and `try_rewrite_fusion_site`
361/// (the actual AST rewrite) call this so the two stay in lockstep —
362/// `aver check` can never report a site that the rewrite then refuses
363/// to take.
364fn match_string_join_fusion_site(
365    expr: &Expr,
366    sinks: &HashMap<String, BufferBuildShape>,
367) -> Option<String> {
368    let Expr::FnCall(callee, args) = expr else {
369        return None;
370    };
371    if !is_dotted_ident(&callee.node, "String", "join") || args.len() != 2 {
372        return None;
373    }
374    let consumer_arg = &args[0].node;
375
376    // Peel an optional `List.reverse(...)` wrapper.
377    let (inner_call_expr, saw_external_reverse) = match consumer_arg {
378        Expr::FnCall(rev_callee, rev_args)
379            if is_dotted_ident(&rev_callee.node, "List", "reverse") && rev_args.len() == 1 =>
380        {
381            (&rev_args[0].node, true)
382        }
383        other => (other, false),
384    };
385
386    let Expr::FnCall(inner_callee, inner_args) = inner_call_expr else {
387        return None;
388    };
389    let Expr::Ident(name) = &inner_callee.node else {
390        return None;
391    };
392    let shape = sinks.get(name)?;
393
394    let kinds_align = matches!(
395        (saw_external_reverse, &shape.kind),
396        (false, BufferBuildKind::InternalReverse) | (true, BufferBuildKind::ExternalReverse)
397    );
398    if !kinds_align {
399        return None;
400    }
401
402    let acc_arg = inner_args.get(shape.acc_param_idx)?;
403    if !matches!(&acc_arg.node, Expr::List(items) if items.is_empty()) {
404        return None;
405    }
406
407    Some(name.clone())
408}
409
410/// True if a parameter type-string parses as `List<...>`.
411fn is_list_type_str(ty: &str) -> bool {
412    let t = ty.trim();
413    t.starts_with("List<") && t.ends_with('>')
414}
415
416/// Extract the single match expression that forms a fn's entire body.
417/// Returns `None` if the body is empty, has multiple statements, or its
418/// single statement isn't a match expression.
419fn single_match_body(body: &FnBody) -> Option<&Expr> {
420    let stmts = body.stmts();
421    if stmts.len() != 1 {
422        return None;
423    }
424    match &stmts[0] {
425        Stmt::Expr(spanned) => match &spanned.node {
426            Expr::Match { .. } => Some(&spanned.node),
427            _ => None,
428        },
429        Stmt::Binding(_, _, _) => None,
430    }
431}
432
433/// If `arms` is exactly two arms with `Bool(true)` / `Bool(false)`
434/// patterns, return `(true_body, false_body)` references. Order in
435/// source doesn't matter — we sort by pattern.
436fn pair_bool_arms(arms: &[MatchArm]) -> Option<(&Expr, &Expr)> {
437    if arms.len() != 2 {
438        return None;
439    }
440    let mut t = None;
441    let mut f = None;
442    for arm in arms {
443        match &arm.pattern {
444            Pattern::Literal(Literal::Bool(true)) => {
445                if t.is_some() {
446                    return None;
447                }
448                t = Some(&arm.body.node);
449            }
450            Pattern::Literal(Literal::Bool(false)) => {
451                if f.is_some() {
452                    return None;
453                }
454                f = Some(&arm.body.node);
455            }
456            _ => return None,
457        }
458    }
459    Some((t?, f?))
460}
461
462/// True if `expr` is `List.reverse(<Ident(acc_name)>)`.
463fn is_list_reverse_of(expr: &Expr, acc_name: &str) -> bool {
464    let (callee, args) = match expr {
465        Expr::FnCall(c, a) => (c, a),
466        _ => return false,
467    };
468    if !is_dotted_ident(&callee.node, "List", "reverse") {
469        return false;
470    }
471    if args.len() != 1 {
472        return false;
473    }
474    matches!(&args[0].node, Expr::Ident(name) if name == acc_name)
475}
476
477/// True if `expr` is a tail-call to `self_name` whose argument list
478/// contains `List.prepend(<anything>, <Ident(acc_name)>)` in any
479/// position. The position should match the `acc_param_idx` but the
480/// caller may have other params before it; we only require the
481/// `prepend` to terminate in the expected accumulator binding.
482fn is_self_tail_with_prepend_acc(
483    expr: &Expr,
484    self_name: &str,
485    acc_idx: usize,
486    acc_name: &str,
487) -> bool {
488    let data = match expr {
489        Expr::TailCall(data) => data,
490        _ => return false,
491    };
492    if data.target != self_name {
493        return false;
494    }
495    // The prepend has to land in the *acc* position specifically — the
496    // synthesizer extracts the element expression from `args[acc_idx]`.
497    // A loose `any` here would let through fns where some other arg
498    // happens to be a prepend, the synth would later return None, but
499    // detection had already promised the sink to call-site rewriting:
500    // `String.join(<sink>(...))` would be rewritten to call a
501    // `<sink>__buffered` that never gets generated. Require the exact
502    // shape here so detection and synthesis agree.
503    let acc_arg = match data.args.get(acc_idx) {
504        Some(a) => a,
505        None => return false,
506    };
507    is_list_prepend_to_acc(&acc_arg.node, acc_name)
508}
509
510/// True if `expr` is `List.prepend(<anything>, <Ident(acc_name)>)`.
511fn is_list_prepend_to_acc(expr: &Expr, acc_name: &str) -> bool {
512    let (callee, args) = match expr {
513        Expr::FnCall(c, a) => (c, a),
514        _ => return false,
515    };
516    if !is_dotted_ident(&callee.node, "List", "prepend") {
517        return false;
518    }
519    if args.len() != 2 {
520        return false;
521    }
522    matches!(&args[1].node, Expr::Ident(name) if name == acc_name)
523}
524
525/// True if `expr` is `<Module>.<Member>` access (the un-called callee
526/// shape of `Module.member(...)`).
527fn is_dotted_ident(expr: &Expr, module: &str, member: &str) -> bool {
528    let (base, attr) = match expr {
529        Expr::Attr(b, a) => (b, a),
530        _ => return false,
531    };
532    if attr != member {
533        return false;
534    }
535    matches!(&base.node, Expr::Ident(name) if name == module)
536}
537
538/// Synthesize a `<fn>__buffered` variant for each matched buffer-build
539/// sink. The synthesized FnDef walks the same shape as the original but
540/// threads a runtime `Buffer` through tail-call args instead of building
541/// a `List<T>` of strings:
542///
543/// Original:
544/// ```aver
545/// fn build(.., acc: List<T>) -> List<T>
546///     match <cond>
547///         true  -> List.reverse(acc)
548///         false -> build(.., List.prepend(<elem>, acc))
549/// ```
550///
551/// Synthesized:
552/// ```aver
553/// fn build__buffered(.., __buf: Buffer, __sep: String) -> Buffer
554///     match <cond>
555///         true  -> __buf
556///         false -> build__buffered(..,
557///             __buf_append(
558///                 __buf_append_sep_unless_first(__buf, __sep),
559///                 <elem>
560///             ),
561///             __sep
562///         )
563/// ```
564///
565/// Threading is via expression composition: the inner
566/// `__buf_append_sep_unless_first` returns the (possibly grown) buffer,
567/// the outer `__buf_append` writes the element and again returns
568/// the (possibly grown) buffer, and that final pointer is what the tail
569/// call sees as `__buf`. No `_ =` discards anywhere — the C' review
570/// explicitly required this to avoid use-after-grow corruption.
571///
572/// Returns one `FnDef` per matched fn. Caller appends to the user-fn
573/// list before WASM emission so both original and buffered variants
574/// reach codegen through the same pipeline.
575pub fn synthesize_buffered_variants(
576    fns: &[&FnDef],
577    sinks: &HashMap<String, BufferBuildShape>,
578) -> Vec<FnDef> {
579    let mut out = Vec::new();
580    for fd in fns {
581        if let Some(shape) = sinks.get(&fd.name)
582            && let Some(buffered) = build_buffered_variant(fd, shape)
583        {
584            out.push(buffered);
585        }
586    }
587    out
588}
589
590/// Wrap an `Expr` as `Spanned<Expr>` carrying the same line as the
591/// matched fn (best effort — the synthesized code is internal and
592/// won't be source-located by the user, but having a non-zero line
593/// keeps downstream visitors happy).
594fn sp_at(line: usize, expr: Expr) -> Spanned<Expr> {
595    Spanned::new(expr, line)
596}
597
598fn sp_at_typed(line: usize, expr: Expr, ty: crate::types::Type) -> Spanned<Expr> {
599    let s = Spanned::new(expr, line);
600    s.set_ty(ty);
601    s
602}
603
604/// Build `<intrinsic>(args...)` as a Spanned<Expr>. Intrinsic names
605/// are bare identifiers (no module dot) — `__buf_append`,
606/// `__buf_append_sep_unless_first`. The WASM emitter recognises them
607/// in the builtin dispatch.
608///
609/// This pass runs *after* the type checker, so synthesized nodes never
610/// see `infer_type`. Callers that want the result `Spanned` to carry a
611/// type for downstream readers (Rust codegen reads `expr.ty()` to gate
612/// owned-mutable `Buffer` hoists) should use [`buffer_intrinsic_call`]
613/// or [`finalize_intrinsic_call`].
614fn intrinsic_call(line: usize, name: &str, args: Vec<Spanned<Expr>>) -> Spanned<Expr> {
615    let callee = sp_at(line, Expr::Ident(name.to_string()));
616    sp_at(line, Expr::FnCall(Box::new(callee), args))
617}
618
619/// Same as [`intrinsic_call`] but stamps the result with `Buffer` —
620/// every `__buf_new` / `__buf_append` / `__buf_append_sep_unless_first`
621/// returns the (possibly-grown) owned buffer.
622fn buffer_intrinsic_call(line: usize, name: &str, args: Vec<Spanned<Expr>>) -> Spanned<Expr> {
623    let call = intrinsic_call(line, name, args);
624    call.set_ty(crate::types::Type::named("Buffer"));
625    call
626}
627
628/// `__buf_finalize` consumes the buffer and yields the final `String`.
629fn finalize_intrinsic_call(line: usize, args: Vec<Spanned<Expr>>) -> Spanned<Expr> {
630    let call = intrinsic_call(line, "__buf_finalize", args);
631    call.set_ty(crate::types::Type::Str);
632    call
633}
634
635/// Run the full buffer-build deforestation pass on a program: detect
636/// sinks, synthesize buffered variants, rewrite fusion sites in place,
637/// and APPEND the synthesized FnDefs to the items list as new
638/// top-level fns. Caller is responsible for invoking this AFTER
639/// `tco::transform_program` (the detector requires `Expr::TailCall`
640/// nodes) and BEFORE `resolver::resolve_program` (the detector +
641/// rewrite both match on `Expr::Ident` shapes that the resolver
642/// rewrites to `Expr::Resolved`).
643///
644/// Returns a [`BufferBuildPassReport`] describing what fired, for
645/// diagnostic / bench reporting and `--explain-passes`.
646pub fn run_buffer_build_pass(items: &mut Vec<crate::ast::TopLevel>) -> BufferBuildPassReport {
647    let fn_refs: Vec<&FnDef> = items
648        .iter()
649        .filter_map(|it| match it {
650            crate::ast::TopLevel::FnDef(fd) => Some(fd),
651            _ => None,
652        })
653        .collect();
654    let all_sinks = compute_buffer_build_sinks(&fn_refs);
655    if all_sinks.is_empty() {
656        return BufferBuildPassReport::default();
657    }
658    let sites = find_fusion_sites(&fn_refs, &all_sinks);
659
660    // Synthesize a buffered variant only for sinks that actually have
661    // at least one rewriteable call site. The earlier shape produced
662    // a `<sink>__buffered` for every detected sink — bloat in the
663    // common case (most detected sinks aren't called via the canonical
664    // String.join shape) and a real risk of name-shadowing a user fn
665    // named `<sink>__buffered`. Restricting to used sinks keeps the
666    // synthetic surface tight.
667    let mut used_sinks: HashMap<String, BufferBuildShape> = HashMap::new();
668    for site in &sites {
669        if let Some(shape) = all_sinks.get(&site.sink_fn) {
670            used_sinks.insert(site.sink_fn.clone(), shape.clone());
671        }
672    }
673    let synthesized = synthesize_buffered_variants(&fn_refs, &used_sinks);
674    let sinks = used_sinks;
675    drop(fn_refs);
676
677    let mut fn_defs_owned: Vec<&mut FnDef> = items
678        .iter_mut()
679        .filter_map(|it| match it {
680            crate::ast::TopLevel::FnDef(fd) => Some(fd),
681            _ => None,
682        })
683        .collect();
684    // rewrite_fusion_sites takes &mut [FnDef], so pull a fresh
685    // mutable view across owned slots. We can't pass &mut [&mut FnDef]
686    // directly — instead, walk and rewrite each fn body individually.
687    for fd in fn_defs_owned.iter_mut() {
688        rewrite_one_fn(fd, &sinks);
689    }
690
691    items.reserve(synthesized.len());
692    for fd in synthesized.iter() {
693        items.push(crate::ast::TopLevel::FnDef(fd.clone()));
694    }
695
696    let mut sink_fns: Vec<String> = sinks.keys().cloned().collect();
697    sink_fns.sort();
698    let synthesized_fns: Vec<String> = synthesized.iter().map(|fd| fd.name.clone()).collect();
699
700    let mut rewrites_by_sink: std::collections::BTreeMap<String, usize> =
701        std::collections::BTreeMap::new();
702    for site in &sites {
703        *rewrites_by_sink.entry(site.sink_fn.clone()).or_default() += 1;
704    }
705
706    BufferBuildPassReport {
707        rewrites: sites.len(),
708        synthesized: synthesized_fns,
709        sink_fns,
710        rewrites_by_sink,
711    }
712}
713
714/// Per-pass report — what buffer_build did during a single pipeline run.
715/// Drives `aver compile --explain-passes`; consumed by the bench
716/// regression checks (e.g. "fail if buffer_build no longer fires on the
717/// canonical shape").
718#[derive(Debug, Clone, Default)]
719pub struct BufferBuildPassReport {
720    /// Number of fusion sites rewritten in place.
721    pub rewrites: usize,
722    /// Names of synthesized `<sink>__buffered` variants appended to
723    /// the items list.
724    pub synthesized: Vec<String>,
725    /// Sink fns whose buffered variant actually fired (matches one of
726    /// `synthesized` minus the `__buffered` suffix). Sorted.
727    pub sink_fns: Vec<String>,
728    /// Per-sink rewrite counts. Sorted alphabetically by sink fn.
729    pub rewrites_by_sink: std::collections::BTreeMap<String, usize>,
730}
731
732/// Apply fusion-site rewrite to a single fn body. Internal helper
733/// for `run_buffer_build_pass` since `rewrite_fusion_sites` takes a
734/// slice and we have an iterator-of-mut-refs here.
735fn rewrite_one_fn(fd: &mut FnDef, sinks: &HashMap<String, BufferBuildShape>) {
736    let body_arc = std::sync::Arc::make_mut(&mut fd.body);
737    let FnBody::Block(stmts) = body_arc;
738    for stmt in stmts.iter_mut() {
739        match stmt {
740            Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
741                rewrite_expr_in_place(expr, sinks);
742            }
743        }
744    }
745}
746
747/// Walk every expression in `fn_defs` and rewrite `String.join`
748/// fusion sites in place: `String.join(matched_fn(args, []), sep)` →
749/// `__buf_finalize(matched_fn__buffered(args_without_acc, __buf_new(8192), sep))`.
750///
751/// Conservative trigger per the C' review: only fires when the
752/// acc-position arg is a literal `Expr::List([])`. A non-empty
753/// initial accumulator would silently lose elements after rewrite,
754/// so we skip in that case.
755///
756/// The rewrite is recursive: nested fusion sites (a fusion site
757/// inside another fusion site's args) all get rewritten in one pass.
758pub fn rewrite_fusion_sites(fn_defs: &mut [FnDef], sinks: &HashMap<String, BufferBuildShape>) {
759    if sinks.is_empty() {
760        return;
761    }
762    for fd in fn_defs.iter_mut() {
763        let body_arc = std::sync::Arc::make_mut(&mut fd.body);
764        let FnBody::Block(stmts) = body_arc;
765        for stmt in stmts.iter_mut() {
766            match stmt {
767                Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
768                    rewrite_expr_in_place(expr, sinks);
769                }
770            }
771        }
772    }
773}
774
775/// Recursive expression-tree walker that rewrites fusion sites in
776/// place. Rewrite is "outermost first" — if the whole expression is
777/// a fusion site, transform it before descending into the new shape's
778/// children, so we don't double-rewrite.
779fn rewrite_expr_in_place(expr: &mut Spanned<Expr>, sinks: &HashMap<String, BufferBuildShape>) {
780    if let Some(replacement) = try_rewrite_fusion_site(expr, sinks) {
781        *expr = replacement;
782        // The replacement contains the original elem expressions
783        // (possibly themselves containing fusion sites in deep
784        // gradient builders). Recurse into the new tree.
785        descend_into_subexprs(expr, sinks);
786        return;
787    }
788    descend_into_subexprs(expr, sinks);
789}
790
791/// Recurse into the children of an Expr, applying `rewrite_expr_in_place`
792/// to each. Mirrors the shape coverage of `walk_expr_for_fusion_sites`
793/// in this module so we don't miss any node kind.
794fn descend_into_subexprs(expr: &mut Spanned<Expr>, sinks: &HashMap<String, BufferBuildShape>) {
795    match &mut expr.node {
796        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } | Expr::Constructor(_, None) => {}
797        Expr::Constructor(_, Some(inner)) | Expr::Attr(inner, _) | Expr::ErrorProp(inner) => {
798            rewrite_expr_in_place(inner, sinks);
799        }
800        Expr::FnCall(callee, args) => {
801            rewrite_expr_in_place(callee, sinks);
802            for a in args.iter_mut() {
803                rewrite_expr_in_place(a, sinks);
804            }
805        }
806        Expr::TailCall(data) => {
807            for a in data.args.iter_mut() {
808                rewrite_expr_in_place(a, sinks);
809            }
810        }
811        Expr::BinOp(_, l, r) => {
812            rewrite_expr_in_place(l, sinks);
813            rewrite_expr_in_place(r, sinks);
814        }
815        Expr::Neg(inner) => rewrite_expr_in_place(inner, sinks),
816        Expr::Match { subject, arms } => {
817            rewrite_expr_in_place(subject, sinks);
818            for arm in arms.iter_mut() {
819                rewrite_expr_in_place(&mut arm.body, sinks);
820            }
821        }
822        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
823            for it in items.iter_mut() {
824                rewrite_expr_in_place(it, sinks);
825            }
826        }
827        Expr::MapLiteral(entries) => {
828            for (k, v) in entries.iter_mut() {
829                rewrite_expr_in_place(k, sinks);
830                rewrite_expr_in_place(v, sinks);
831            }
832        }
833        Expr::RecordCreate { fields, .. } => {
834            for (_, v) in fields.iter_mut() {
835                rewrite_expr_in_place(v, sinks);
836            }
837        }
838        Expr::RecordUpdate { base, updates, .. } => {
839            rewrite_expr_in_place(base, sinks);
840            for (_, v) in updates.iter_mut() {
841                rewrite_expr_in_place(v, sinks);
842            }
843        }
844        Expr::InterpolatedStr(parts) => {
845            for part in parts.iter_mut() {
846                if let crate::ast::StrPart::Parsed(inner) = part {
847                    rewrite_expr_in_place(inner, sinks);
848                }
849            }
850        }
851    }
852}
853
854/// If `expr` is a `String.join(matched_fn(args, []), sep)` with
855/// matched_fn in `sinks` and acc-position arg a literal empty list,
856/// return the rewritten Spanned<Expr>. Else return None.
857fn try_rewrite_fusion_site(
858    expr: &Spanned<Expr>,
859    sinks: &HashMap<String, BufferBuildShape>,
860) -> Option<Spanned<Expr>> {
861    let line = expr.line;
862
863    // Match the same predicate used by `find_fusion_sites` so the
864    // diagnostic count and the rewrite count are guaranteed equal.
865    let sink_name = match_string_join_fusion_site(&expr.node, sinks)?;
866    let shape = sinks.get(&sink_name)?;
867
868    // Re-extract the inner sink call and its args. The match predicate
869    // above already verified the shape; this is just to recover the
870    // pieces we need to assemble the rewrite.
871    let outer_args = match &expr.node {
872        Expr::FnCall(_, a) => a,
873        _ => return None,
874    };
875    let consumer_arg = &outer_args[0].node;
876    let inner_call_expr = if let Expr::FnCall(rev_callee, rev_args) = consumer_arg
877        && is_dotted_ident(&rev_callee.node, "List", "reverse")
878        && rev_args.len() == 1
879    {
880        &rev_args[0].node
881    } else {
882        consumer_arg
883    };
884    let inner_args = match inner_call_expr {
885        Expr::FnCall(_, a) => a,
886        _ => return None,
887    };
888
889    // Build the rewrite:
890    //   __buf_finalize(
891    //     <fn>__buffered(
892    //       <args without acc-pos>,
893    //       __buf_new(8192),
894    //       <sep>
895    //     )
896    //   )
897    let sep_expr = outer_args[1].clone();
898    let buf_new = buffer_intrinsic_call(
899        line,
900        "__buf_new",
901        vec![sp_at_typed(
902            line,
903            Expr::Literal(Literal::Int(8192)),
904            crate::types::Type::Int,
905        )],
906    );
907    let mut buffered_args: Vec<Spanned<Expr>> = inner_args
908        .iter()
909        .enumerate()
910        .filter_map(|(i, a)| (i != shape.acc_param_idx).then_some(a).cloned())
911        .collect();
912    buffered_args.push(buf_new);
913    buffered_args.push(sep_expr);
914    // `<sink>__buffered` returns `String` (the buffered variant signature
915    // in `synthesize_buffered_fn` types it that way).
916    let buffered_call = sp_at_typed(
917        line,
918        Expr::FnCall(
919            Box::new(sp_at(line, Expr::Ident(format!("{}__buffered", sink_name)))),
920            buffered_args,
921        ),
922        crate::types::Type::Str,
923    );
924    Some(finalize_intrinsic_call(line, vec![buffered_call]))
925}
926
927/// Construct the buffered FnDef for a single matched fn. Returns
928/// `None` if the original body shape doesn't match what we expect
929/// (defensive: detection should have caught this, but if the body
930/// changed shape between detection and synthesis, skip).
931fn build_buffered_variant(fd: &FnDef, shape: &BufferBuildShape) -> Option<FnDef> {
932    // Original body: `match <subject> { <terminating-arm>; <recursive-arm> }`.
933    // The two BufferBuildKind variants pair different patterns:
934    //   InternalReverse: `true -> List.reverse(acc)`, `false -> recurse(...)`.
935    //   ExternalReverse: `[] -> acc`, `[head, ..rest] -> recurse(...)`.
936    // We extract the recursive arm in both cases (for the prepend tail
937    // call) and rebuild the match with terminating arm `... -> __buf`.
938    let stmts = fd.body.stmts();
939    if stmts.len() != 1 {
940        return None;
941    }
942    let outer_expr = match &stmts[0] {
943        Stmt::Expr(spanned) => spanned,
944        _ => return None,
945    };
946    let (subject_orig, arms_orig) = match &outer_expr.node {
947        Expr::Match { subject, arms } => (subject, arms),
948        _ => return None,
949    };
950    let recursive_body: &Spanned<Expr> = match shape.kind {
951        BufferBuildKind::InternalReverse => arms_orig
952            .iter()
953            .find(|a| matches!(a.pattern, Pattern::Literal(Literal::Bool(false))))
954            .map(|a| a.body.as_ref())?,
955        BufferBuildKind::ExternalReverse => arms_orig
956            .iter()
957            .find(|a| matches!(a.pattern, Pattern::Cons(_, _)))
958            .map(|a| a.body.as_ref())?,
959    };
960    let tail_data = match &recursive_body.node {
961        Expr::TailCall(data) => data,
962        _ => return None,
963    };
964
965    // The acc-position arg in the original tail call is
966    // `List.prepend(<elem>, acc)`. Extract the element expression.
967    let acc_arg_orig = tail_data.args.get(shape.acc_param_idx)?;
968    let elem_expr = match &acc_arg_orig.node {
969        Expr::FnCall(callee, args) => {
970            if !is_dotted_ident(&callee.node, "List", "prepend") {
971                return None;
972            }
973            if args.len() != 2 {
974                return None;
975            }
976            // args[0] is elem, args[1] is acc ident — verify acc.
977            match &args[1].node {
978                Expr::Ident(name) if name == &shape.acc_param_name => {}
979                _ => return None,
980            }
981            args[0].clone()
982        }
983        _ => return None,
984    };
985
986    let line = fd.line;
987    let buf_name = "__buf";
988    let sep_name = "__sep";
989    let buffered_target = format!("{}__buffered", fd.name);
990
991    // Synthesized false arm body:
992    //   <self>__buffered(<orig args minus acc>, __buf_append(<sep_unless_first>, <elem>), __sep)
993    //
994    // Build the buffer-threading expression first: the inner intrinsic
995    // appends `__sep` if the buffer is non-empty (otherwise no-op),
996    // returning the possibly-grown buffer. The outer intrinsic appends
997    // the user's element. The result is what gets passed as the
998    // buffered variant's `__buf` arg in the recursive call.
999    let buffer_ty = crate::types::Type::named("Buffer");
1000    let buf_ident = || sp_at_typed(line, Expr::Ident(buf_name.to_string()), buffer_ty.clone());
1001    let sep_ident = || {
1002        sp_at_typed(
1003            line,
1004            Expr::Ident(sep_name.to_string()),
1005            crate::types::Type::Str,
1006        )
1007    };
1008    let sep_then_buf = buffer_intrinsic_call(
1009        line,
1010        "__buf_append_sep_unless_first",
1011        vec![buf_ident(), sep_ident()],
1012    );
1013    let final_buf = buffer_intrinsic_call(line, "__buf_append", vec![sep_then_buf, elem_expr]);
1014
1015    // Build new tail-call args: original args with acc-pos replaced by
1016    // the threaded buffer expression, then `__sep` appended at end.
1017    let mut new_args: Vec<Spanned<Expr>> = tail_data
1018        .args
1019        .iter()
1020        .enumerate()
1021        .map(|(i, a)| {
1022            if i == shape.acc_param_idx {
1023                final_buf.clone()
1024            } else {
1025                a.clone()
1026            }
1027        })
1028        .collect();
1029    new_args.push(sep_ident());
1030
1031    // The recursive tail-call lands on `<fn>__buffered`, which returns
1032    // Buffer; stamp the type so the typed WASM emitter doesn't have to
1033    // re-derive it from the call target.
1034    let new_recursive_body = sp_at_typed(
1035        line,
1036        Expr::TailCall(Box::new(TailCallData {
1037            target: buffered_target.clone(),
1038            args: new_args,
1039        })),
1040        buffer_ty.clone(),
1041    );
1042
1043    // Terminating arm body: just return `__buf` — the buffer IS the result.
1044    // Pattern depends on which sink shape we matched: `true` for the
1045    // InternalReverse idiom (where the original returned `List.reverse(acc)`),
1046    // `[]` for ExternalReverse (where the original returned `acc`).
1047    let new_arms = match shape.kind {
1048        BufferBuildKind::InternalReverse => vec![
1049            MatchArm {
1050                pattern: Pattern::Literal(Literal::Bool(true)),
1051                body: Box::new(buf_ident()),
1052                binding_slots: std::sync::OnceLock::new(),
1053            },
1054            MatchArm {
1055                pattern: Pattern::Literal(Literal::Bool(false)),
1056                body: Box::new(new_recursive_body),
1057                binding_slots: std::sync::OnceLock::new(),
1058            },
1059        ],
1060        BufferBuildKind::ExternalReverse => {
1061            // Re-use the cons binding names from the original arm so
1062            // any `head` / `rest` references inside the recursive body
1063            // continue to resolve.
1064            let cons_pat = arms_orig
1065                .iter()
1066                .find_map(|a| match &a.pattern {
1067                    Pattern::Cons(h, t) => Some(Pattern::Cons(h.clone(), t.clone())),
1068                    _ => None,
1069                })
1070                .unwrap_or(Pattern::Cons("__head".to_string(), "__tail".to_string()));
1071            vec![
1072                MatchArm {
1073                    pattern: Pattern::EmptyList,
1074                    body: Box::new(buf_ident()),
1075                    binding_slots: std::sync::OnceLock::new(),
1076                },
1077                MatchArm {
1078                    pattern: cons_pat,
1079                    body: Box::new(new_recursive_body),
1080                    binding_slots: std::sync::OnceLock::new(),
1081                },
1082            ]
1083        }
1084    };
1085
1086    // Synthesised Match returns Buffer — both arms produce one
1087    // (terminating arm: bare `__buf` ident; recursive arm: a
1088    // `<fn>__buffered` tail-call returning Buffer). Stamp it so the
1089    // Step 2 type-driven WASM emitter can read the type without a
1090    // fallback.
1091    let new_match = sp_at_typed(
1092        line,
1093        Expr::Match {
1094            subject: subject_orig.clone(),
1095            arms: new_arms,
1096        },
1097        crate::types::Type::named("Buffer"),
1098    );
1099
1100    let new_body = FnBody::Block(vec![Stmt::Expr(new_match)]);
1101
1102    // Params: original minus acc + (__buf, "Buffer") + (__sep, "String").
1103    let mut new_params: Vec<(String, String)> = fd
1104        .params
1105        .iter()
1106        .enumerate()
1107        .filter_map(|(i, p)| (i != shape.acc_param_idx).then_some(p).cloned())
1108        .collect();
1109    new_params.push((buf_name.to_string(), "Buffer".to_string()));
1110    new_params.push((sep_name.to_string(), "String".to_string()));
1111
1112    Some(FnDef {
1113        name: buffered_target,
1114        line,
1115        params: new_params,
1116        return_type: "Buffer".to_string(),
1117        // Synthesized variants inherit effects from the original — if
1118        // the matched fn calls effectful helpers (like `renderRow`
1119        // calling `Console.print`), the buffered variant calls them
1120        // too at the same positions. Conservative.
1121        effects: fd.effects.clone(),
1122        desc: Some(format!(
1123            "Synthesized buffered variant of `{}` for deforestation \
1124             lowering. Call sites that match `String.join({}(...), sep)` \
1125             are rewritten to alloc a buffer + call this variant + \
1126             finalize, skipping the intermediate List.",
1127            fd.name, fd.name
1128        )),
1129        body: Arc::new(new_body),
1130        resolution: None,
1131    })
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137    use crate::ast::{BinOp, FnBody, FnDef, Literal, Spanned, TailCallData};
1138    use std::sync::Arc;
1139
1140    fn sp<T>(value: T) -> Spanned<T> {
1141        Spanned::new(value, 1)
1142    }
1143
1144    fn ident(name: &str) -> Spanned<Expr> {
1145        sp(Expr::Ident(name.to_string()))
1146    }
1147
1148    fn dotted(module: &str, member: &str) -> Spanned<Expr> {
1149        sp(Expr::Attr(Box::new(ident(module)), member.to_string()))
1150    }
1151
1152    fn call(callee: Spanned<Expr>, args: Vec<Spanned<Expr>>) -> Spanned<Expr> {
1153        sp(Expr::FnCall(Box::new(callee), args))
1154    }
1155
1156    /// Build a canonical buffer-build fn: takes (col: Int, acc: List<Int>),
1157    /// matches col >= 10, true → reverse(acc), false → tail-call self
1158    /// with prepend(col, acc).
1159    fn canonical_builder(name: &str) -> FnDef {
1160        let true_body = call(dotted("List", "reverse"), vec![ident("acc")]);
1161        let prepend = call(dotted("List", "prepend"), vec![ident("col"), ident("acc")]);
1162        let false_body = sp(Expr::TailCall(Box::new(TailCallData {
1163            target: name.to_string(),
1164            args: vec![
1165                sp(Expr::BinOp(
1166                    BinOp::Add,
1167                    Box::new(ident("col")),
1168                    Box::new(sp(Expr::Literal(Literal::Int(1)))),
1169                )),
1170                prepend,
1171            ],
1172        })));
1173        let match_expr = sp(Expr::Match {
1174            subject: Box::new(sp(Expr::BinOp(
1175                BinOp::Gte,
1176                Box::new(ident("col")),
1177                Box::new(sp(Expr::Literal(Literal::Int(10)))),
1178            ))),
1179            arms: vec![
1180                MatchArm {
1181                    pattern: Pattern::Literal(Literal::Bool(true)),
1182                    body: Box::new(true_body),
1183                    binding_slots: std::sync::OnceLock::new(),
1184                },
1185                MatchArm {
1186                    pattern: Pattern::Literal(Literal::Bool(false)),
1187                    body: Box::new(false_body),
1188                    binding_slots: std::sync::OnceLock::new(),
1189                },
1190            ],
1191        });
1192        FnDef {
1193            name: name.to_string(),
1194            line: 1,
1195            params: vec![
1196                ("col".to_string(), "Int".to_string()),
1197                ("acc".to_string(), "List<Int>".to_string()),
1198            ],
1199            return_type: "List<Int>".to_string(),
1200            effects: vec![],
1201            desc: None,
1202            body: Arc::new(FnBody::Block(vec![Stmt::Expr(match_expr)])),
1203            resolution: None,
1204        }
1205    }
1206
1207    #[test]
1208    fn matches_canonical_buffer_build() {
1209        let fd = canonical_builder("build");
1210        let info = compute_buffer_build_sinks(&[&fd]);
1211        let shape = info.get("build").expect("expected match");
1212        assert_eq!(shape.acc_param_idx, 1);
1213        assert_eq!(shape.acc_param_name, "acc");
1214    }
1215
1216    #[test]
1217    fn rejects_fn_without_list_param() {
1218        let mut fd = canonical_builder("build");
1219        // Strip the List<...> param.
1220        fd.params = vec![("col".to_string(), "Int".to_string())];
1221        let info = compute_buffer_build_sinks(&[&fd]);
1222        assert!(info.is_empty(), "fn without List param should not match");
1223    }
1224
1225    #[test]
1226    fn rejects_when_true_arm_isnt_reverse() {
1227        let mut fd = canonical_builder("build");
1228        // Replace true arm body with a different expression.
1229        if let FnBody::Block(stmts) = Arc::make_mut(&mut fd.body)
1230            && let Stmt::Expr(spanned) = &mut stmts[0]
1231            && let Expr::Match { arms, .. } = &mut spanned.node
1232        {
1233            *arms[0].body = ident("acc");
1234        }
1235        let info = compute_buffer_build_sinks(&[&fd]);
1236        assert!(
1237            info.is_empty(),
1238            "fn returning bare acc instead of reverse should not match"
1239        );
1240    }
1241
1242    #[test]
1243    fn rejects_when_false_arm_uses_append_not_prepend() {
1244        let mut fd = canonical_builder("build");
1245        // Swap List.prepend → List.append in the false arm tail call.
1246        if let FnBody::Block(stmts) = Arc::make_mut(&mut fd.body)
1247            && let Stmt::Expr(spanned) = &mut stmts[0]
1248            && let Expr::Match { arms, .. } = &mut spanned.node
1249        {
1250            let false_body = arms[1].body.as_mut();
1251            if let Expr::TailCall(data) = &mut false_body.node
1252                && let Expr::FnCall(callee, _) = &mut data.args[1].node
1253                && let Expr::Attr(_, attr) = &mut callee.node
1254            {
1255                *attr = "append".to_string();
1256            }
1257        }
1258        let info = compute_buffer_build_sinks(&[&fd]);
1259        assert!(
1260            info.is_empty(),
1261            "fn using List.append instead of prepend should not match"
1262        );
1263    }
1264
1265    #[test]
1266    fn rejects_tail_call_to_different_fn() {
1267        let mut fd = canonical_builder("build");
1268        if let FnBody::Block(stmts) = Arc::make_mut(&mut fd.body)
1269            && let Stmt::Expr(spanned) = &mut stmts[0]
1270            && let Expr::Match { arms, .. } = &mut spanned.node
1271        {
1272            let false_body = arms[1].body.as_mut();
1273            if let Expr::TailCall(data) = &mut false_body.node {
1274                data.target = "someone_else".to_string();
1275            }
1276        }
1277        let info = compute_buffer_build_sinks(&[&fd]);
1278        assert!(
1279            info.is_empty(),
1280            "fn whose recursive call targets a different name should not match"
1281        );
1282    }
1283
1284    #[test]
1285    fn rejects_match_with_non_bool_arms() {
1286        let mut fd = canonical_builder("build");
1287        if let FnBody::Block(stmts) = Arc::make_mut(&mut fd.body)
1288            && let Stmt::Expr(spanned) = &mut stmts[0]
1289            && let Expr::Match { arms, .. } = &mut spanned.node
1290        {
1291            arms[0].pattern = Pattern::Literal(Literal::Int(0));
1292        }
1293        let info = compute_buffer_build_sinks(&[&fd]);
1294        assert!(
1295            info.is_empty(),
1296            "match on non-bool patterns should not be detected as buffer-build"
1297        );
1298    }
1299
1300    /// End-to-end: parse a small Aver source, run TCO, then detect.
1301    /// The TCO transform is what produces `Expr::TailCall` nodes from
1302    /// raw `Expr::FnCall` self-recursion; detection runs on the post-TCO
1303    /// AST.
1304    #[test]
1305    fn detects_via_parser_after_tco() {
1306        let src = r#"
1307fn build(n: Int, acc: List<Int>) -> List<Int>
1308    match n <= 0
1309        true  -> List.reverse(acc)
1310        false -> build(n - 1, List.prepend(n, acc))
1311"#;
1312        let mut lexer = crate::lexer::Lexer::new(src);
1313        let tokens = lexer.tokenize().expect("lex");
1314        let mut parser = crate::parser::Parser::new(tokens);
1315        let mut items = parser.parse().expect("parse");
1316        crate::ir::pipeline::tco(&mut items);
1317        let fns: Vec<&FnDef> = items
1318            .iter()
1319            .filter_map(|it| match it {
1320                crate::ast::TopLevel::FnDef(fd) => Some(fd),
1321                _ => None,
1322            })
1323            .collect();
1324        let info = compute_buffer_build_sinks(&fns);
1325        let shape = info
1326            .get("build")
1327            .expect("expected end-to-end shape match for canonical builder");
1328        assert_eq!(shape.acc_param_idx, 1);
1329        assert_eq!(shape.acc_param_name, "acc");
1330    }
1331
1332    /// End-to-end fusion-site detection: builder + caller `String.join`
1333    /// site recognised, line recorded, sink name attached.
1334    #[test]
1335    fn finds_fusion_site_via_parser() {
1336        let src = r#"
1337fn build(n: Int, acc: List<Int>) -> List<Int>
1338    match n <= 0
1339        true  -> List.reverse(acc)
1340        false -> build(n - 1, List.prepend(n, acc))
1341
1342fn main() -> String
1343    String.join(build(5, []), ",")
1344"#;
1345        let mut lexer = crate::lexer::Lexer::new(src);
1346        let tokens = lexer.tokenize().expect("lex");
1347        let mut parser = crate::parser::Parser::new(tokens);
1348        let mut items = parser.parse().expect("parse");
1349        crate::ir::pipeline::tco(&mut items);
1350        let fns: Vec<&FnDef> = items
1351            .iter()
1352            .filter_map(|it| match it {
1353                crate::ast::TopLevel::FnDef(fd) => Some(fd),
1354                _ => None,
1355            })
1356            .collect();
1357        let sinks = compute_buffer_build_sinks(&fns);
1358        let sites = find_fusion_sites(&fns, &sinks);
1359        assert_eq!(sites.len(), 1, "expected one fusion site, got {sites:?}");
1360        let site = &sites[0];
1361        assert_eq!(site.enclosing_fn, "main");
1362        assert_eq!(site.sink_fn, "build");
1363        assert!(site.line > 0, "expected real line info, got 0");
1364    }
1365
1366    /// Caller passes the matched fn's result to a non-`String.join`
1367    /// destination — should NOT register as a fusion site (no buffer
1368    /// to write into).
1369    #[test]
1370    fn ignores_call_when_not_wrapped_in_string_join() {
1371        let src = r#"
1372fn build(n: Int, acc: List<Int>) -> List<Int>
1373    match n <= 0
1374        true  -> List.reverse(acc)
1375        false -> build(n - 1, List.prepend(n, acc))
1376
1377fn main() -> List<Int>
1378    build(5, [])
1379"#;
1380        let mut lexer = crate::lexer::Lexer::new(src);
1381        let tokens = lexer.tokenize().expect("lex");
1382        let mut parser = crate::parser::Parser::new(tokens);
1383        let mut items = parser.parse().expect("parse");
1384        crate::ir::pipeline::tco(&mut items);
1385        let fns: Vec<&FnDef> = items
1386            .iter()
1387            .filter_map(|it| match it {
1388                crate::ast::TopLevel::FnDef(fd) => Some(fd),
1389                _ => None,
1390            })
1391            .collect();
1392        let sinks = compute_buffer_build_sinks(&fns);
1393        let sites = find_fusion_sites(&fns, &sinks);
1394        assert!(
1395            sites.is_empty(),
1396            "build called outside String.join must not be a fusion site, got {sites:?}"
1397        );
1398    }
1399
1400    /// Counter-test: a recursive fn that returns `acc` directly (no
1401    /// reverse) — semantically valid Aver, but its result order is
1402    /// reversed relative to natural read order, so deforestation can't
1403    /// safely rewrite to a forward-emit buffer loop without explicit
1404    /// authorisation. Detector must reject it.
1405    #[test]
1406    fn rejects_via_parser_when_true_arm_returns_bare_acc() {
1407        let src = r#"
1408fn build(n: Int, acc: List<Int>) -> List<Int>
1409    match n <= 0
1410        true  -> acc
1411        false -> build(n - 1, List.prepend(n, acc))
1412"#;
1413        let mut lexer = crate::lexer::Lexer::new(src);
1414        let tokens = lexer.tokenize().expect("lex");
1415        let mut parser = crate::parser::Parser::new(tokens);
1416        let mut items = parser.parse().expect("parse");
1417        crate::ir::pipeline::tco(&mut items);
1418        let fns: Vec<&FnDef> = items
1419            .iter()
1420            .filter_map(|it| match it {
1421                crate::ast::TopLevel::FnDef(fd) => Some(fd),
1422                _ => None,
1423            })
1424            .collect();
1425        let info = compute_buffer_build_sinks(&fns);
1426        assert!(
1427            info.is_empty(),
1428            "fn returning bare acc must not be detected as a deforestation candidate"
1429        );
1430    }
1431
1432    /// End-to-end synthesis: parse a small builder, run TCO, detect
1433    /// it as a sink, then synthesize the buffered variant. Verify the
1434    /// shape: name suffix, dropped acc param, added __buf/__sep
1435    /// params, true arm returns __buf ident, false arm tail-calls
1436    /// __buffered self with threaded buffer expression.
1437    #[test]
1438    fn synthesizes_buffered_variant_from_real_builder() {
1439        let src = r#"
1440fn build(n: Int, acc: List<Int>) -> List<Int>
1441    match n <= 0
1442        true  -> List.reverse(acc)
1443        false -> build(n - 1, List.prepend(n, acc))
1444"#;
1445        let mut lexer = crate::lexer::Lexer::new(src);
1446        let tokens = lexer.tokenize().expect("lex");
1447        let mut parser = crate::parser::Parser::new(tokens);
1448        let mut items = parser.parse().expect("parse");
1449        crate::ir::pipeline::tco(&mut items);
1450        let fns: Vec<&FnDef> = items
1451            .iter()
1452            .filter_map(|it| match it {
1453                crate::ast::TopLevel::FnDef(fd) => Some(fd),
1454                _ => None,
1455            })
1456            .collect();
1457        let sinks = compute_buffer_build_sinks(&fns);
1458        assert!(sinks.contains_key("build"));
1459        let synthesized = synthesize_buffered_variants(&fns, &sinks);
1460        assert_eq!(
1461            synthesized.len(),
1462            1,
1463            "expected exactly one synthesized variant"
1464        );
1465        let bf = &synthesized[0];
1466
1467        // Name + signature shape.
1468        assert_eq!(bf.name, "build__buffered");
1469        assert_eq!(bf.return_type, "Buffer");
1470        let param_names: Vec<&str> = bf.params.iter().map(|(n, _)| n.as_str()).collect();
1471        let param_types: Vec<&str> = bf.params.iter().map(|(_, t)| t.as_str()).collect();
1472        assert_eq!(param_names, vec!["n", "__buf", "__sep"]);
1473        assert_eq!(param_types, vec!["Int", "Buffer", "String"]);
1474
1475        // Body: single Stmt::Expr holding a 2-arm match.
1476        let stmts = bf.body.stmts();
1477        assert_eq!(stmts.len(), 1);
1478        let match_expr = match &stmts[0] {
1479            Stmt::Expr(s) => match &s.node {
1480                Expr::Match { subject: _, arms } => arms,
1481                _ => panic!("body root must be a match"),
1482            },
1483            _ => panic!("body root must be Stmt::Expr"),
1484        };
1485        assert_eq!(match_expr.len(), 2);
1486
1487        // True arm: body is `__buf` ident.
1488        let true_arm = match_expr
1489            .iter()
1490            .find(|a| matches!(a.pattern, Pattern::Literal(Literal::Bool(true))))
1491            .expect("true arm");
1492        match &true_arm.body.node {
1493            Expr::Ident(name) => assert_eq!(name, "__buf"),
1494            other => panic!("true arm should be Ident(__buf), got {other:?}"),
1495        }
1496
1497        // False arm: tail-call to build__buffered with threaded buf.
1498        let false_arm = match_expr
1499            .iter()
1500            .find(|a| matches!(a.pattern, Pattern::Literal(Literal::Bool(false))))
1501            .expect("false arm");
1502        let tail_data = match &false_arm.body.node {
1503            Expr::TailCall(d) => d,
1504            other => panic!("false arm should be TailCall, got {other:?}"),
1505        };
1506        assert_eq!(tail_data.target, "build__buffered");
1507        // Args: [n - 1, threaded-buffer-expr, __sep_ident]. acc-pos
1508        // (was index 1 in original) is now the threaded buffer; sep
1509        // appended at end.
1510        assert_eq!(tail_data.args.len(), 3);
1511        // Arg 1 is the buffer-threading composition; verify it's
1512        // `__buf_append(__buf_append_sep_unless_first(__buf, __sep), n)`.
1513        let outer = match &tail_data.args[1].node {
1514            Expr::FnCall(callee, args) => {
1515                match &callee.node {
1516                    Expr::Ident(name) => assert_eq!(name, "__buf_append"),
1517                    _ => panic!("expected Ident callee"),
1518                }
1519                args
1520            }
1521            _ => panic!("expected outer __buf_append FnCall"),
1522        };
1523        assert_eq!(outer.len(), 2);
1524        // First arg of outer = inner sep-then-buf.
1525        match &outer[0].node {
1526            Expr::FnCall(callee, _) => match &callee.node {
1527                Expr::Ident(name) => assert_eq!(name, "__buf_append_sep_unless_first"),
1528                _ => panic!("expected Ident callee for inner intrinsic"),
1529            },
1530            _ => panic!("expected inner __buf_append_sep_unless_first FnCall"),
1531        }
1532        // Second arg of outer = original `n` (the prepend's element).
1533        match &outer[1].node {
1534            Expr::Ident(name) => assert_eq!(name, "n"),
1535            _ => panic!("expected `n` ident as elem"),
1536        }
1537        // Last tail-call arg = __sep ident.
1538        match &tail_data.args[2].node {
1539            Expr::Ident(name) => assert_eq!(name, "__sep"),
1540            _ => panic!("expected __sep ident as last arg"),
1541        }
1542    }
1543
1544    #[test]
1545    fn detects_acc_param_at_arbitrary_index() {
1546        // Builder where the List<T> param is first and the tail-call
1547        // body wires the prepend at the same index. Detection has to
1548        // pin the acc position to where the prepend actually lands —
1549        // an earlier loose `any` check would silently pass even on
1550        // mismatched param/arg orderings, then synthesis would fail
1551        // to extract the element expression. Keep the body and the
1552        // params consistent so we exercise the real path.
1553        let true_body = call(dotted("List", "reverse"), vec![ident("acc")]);
1554        let prepend = call(dotted("List", "prepend"), vec![ident("col"), ident("acc")]);
1555        // Tail call: build(prepend(col, acc), col + 1)
1556        // — acc-position arg is at index 0, col+1 at index 1.
1557        let false_body = sp(Expr::TailCall(Box::new(TailCallData {
1558            target: "build".to_string(),
1559            args: vec![
1560                prepend,
1561                sp(Expr::BinOp(
1562                    BinOp::Add,
1563                    Box::new(ident("col")),
1564                    Box::new(sp(Expr::Literal(Literal::Int(1)))),
1565                )),
1566            ],
1567        })));
1568        let match_expr = sp(Expr::Match {
1569            subject: Box::new(sp(Expr::BinOp(
1570                BinOp::Gte,
1571                Box::new(ident("col")),
1572                Box::new(sp(Expr::Literal(Literal::Int(10)))),
1573            ))),
1574            arms: vec![
1575                MatchArm {
1576                    pattern: Pattern::Literal(Literal::Bool(true)),
1577                    body: Box::new(true_body),
1578                    binding_slots: std::sync::OnceLock::new(),
1579                },
1580                MatchArm {
1581                    pattern: Pattern::Literal(Literal::Bool(false)),
1582                    body: Box::new(false_body),
1583                    binding_slots: std::sync::OnceLock::new(),
1584                },
1585            ],
1586        });
1587        let fd = FnDef {
1588            name: "build".to_string(),
1589            line: 1,
1590            params: vec![
1591                ("acc".to_string(), "List<Int>".to_string()),
1592                ("col".to_string(), "Int".to_string()),
1593            ],
1594            return_type: "List<Int>".to_string(),
1595            effects: vec![],
1596            desc: None,
1597            body: Arc::new(FnBody::Block(vec![Stmt::Expr(match_expr)])),
1598            resolution: None,
1599        };
1600        let info = compute_buffer_build_sinks(&[&fd]);
1601        let shape = info.get("build").expect("expected match");
1602        assert_eq!(shape.acc_param_idx, 0);
1603        assert_eq!(shape.acc_param_name, "acc");
1604    }
1605
1606    #[test]
1607    fn rejects_loose_prepend_in_non_acc_position() {
1608        // Earlier the detector accepted a fn whose tail call had a
1609        // prepend in *some* arg, regardless of position. That let
1610        // detection promise a sink the synthesizer couldn't actually
1611        // build. Make sure the tightened predicate refuses this.
1612        let mut fd = canonical_builder("build");
1613        // Reorder tail-call args so prepend ends up at index 0 instead
1614        // of index 1 — but keep params [(col, Int), (acc, List<Int>)],
1615        // so acc-position is index 1, where there's now a `col + 1`
1616        // expression (no prepend). Detection should refuse.
1617        {
1618            let body = std::sync::Arc::make_mut(&mut fd.body);
1619            let FnBody::Block(stmts) = body;
1620            if let Stmt::Expr(spanned) = &mut stmts[0]
1621                && let Expr::Match { arms, .. } = &mut spanned.node
1622            {
1623                for arm in arms.iter_mut() {
1624                    if matches!(arm.pattern, Pattern::Literal(Literal::Bool(false)))
1625                        && let Expr::TailCall(data) = &mut arm.body.node
1626                    {
1627                        data.args.reverse();
1628                    }
1629                }
1630            }
1631        }
1632        let info = compute_buffer_build_sinks(&[&fd]);
1633        assert!(
1634            !info.contains_key("build"),
1635            "loose-prepend (prepend not at acc-position) must not be detected"
1636        );
1637    }
1638
1639    #[test]
1640    fn skips_synth_when_no_rewriteable_call_site() {
1641        // A fn that matches the sink shape but whose only call site
1642        // doesn't fit the canonical fusion pattern (e.g. starts with a
1643        // non-empty initial accumulator, or the wrapper is an unrelated
1644        // function call rather than `String.join`) should NOT get a
1645        // synthesized `__buffered` variant. Generating one is bloat
1646        // and risks shadowing user fns.
1647        let sink = canonical_builder("build");
1648        // Dummy caller that uses `build` but not via `String.join(...)`.
1649        let caller = FnDef {
1650            name: "use_build".to_string(),
1651            line: 2,
1652            params: vec![],
1653            return_type: "List<Int>".to_string(),
1654            effects: vec![],
1655            desc: None,
1656            body: Arc::new(FnBody::Block(vec![Stmt::Expr(call(
1657                ident_expr("build"),
1658                vec![sp(Expr::Literal(Literal::Int(0))), sp(Expr::List(vec![]))],
1659            ))])),
1660            resolution: None,
1661        };
1662        let mut items = vec![
1663            crate::ast::TopLevel::FnDef(sink),
1664            crate::ast::TopLevel::FnDef(caller),
1665        ];
1666        let initial_count = items.len();
1667        let report = run_buffer_build_pass(&mut items);
1668        assert_eq!(report.rewrites, 0, "no fusion sites — no rewriteable call");
1669        assert_eq!(
1670            report.synthesized.len(),
1671            0,
1672            "no synth — nothing to fuse against"
1673        );
1674        assert_eq!(items.len(), initial_count, "no buffered variant appended");
1675    }
1676
1677    #[test]
1678    fn external_reverse_pattern_round_trips() {
1679        // `match list { [] -> acc; [h, ..t] -> recurse(t, prepend(_, acc)) }`
1680        // sink + `String.join(List.reverse(<sink>(args, [])), sep)` call
1681        // site should detect, synth, and rewrite as a single fusion.
1682        let nil_body = ident("acc");
1683        let prepend = call(dotted("List", "prepend"), vec![ident("h"), ident("acc")]);
1684        let cons_body = sp(Expr::TailCall(Box::new(TailCallData {
1685            target: "build".to_string(),
1686            args: vec![ident("t"), prepend],
1687        })));
1688        let match_expr = sp(Expr::Match {
1689            subject: Box::new(ident("xs")),
1690            arms: vec![
1691                MatchArm {
1692                    pattern: Pattern::EmptyList,
1693                    body: Box::new(nil_body),
1694                    binding_slots: std::sync::OnceLock::new(),
1695                },
1696                MatchArm {
1697                    pattern: Pattern::Cons("h".to_string(), "t".to_string()),
1698                    body: Box::new(cons_body),
1699                    binding_slots: std::sync::OnceLock::new(),
1700                },
1701            ],
1702        });
1703        let sink = FnDef {
1704            name: "build".to_string(),
1705            line: 1,
1706            params: vec![
1707                ("xs".to_string(), "List<Int>".to_string()),
1708                ("acc".to_string(), "List<String>".to_string()),
1709            ],
1710            return_type: "List<String>".to_string(),
1711            effects: vec![],
1712            desc: None,
1713            body: Arc::new(FnBody::Block(vec![Stmt::Expr(match_expr)])),
1714            resolution: None,
1715        };
1716        let info = compute_buffer_build_sinks(&[&sink]);
1717        let shape = info
1718            .get("build")
1719            .expect("external-reverse sink should be detected");
1720        assert_eq!(shape.kind, BufferBuildKind::ExternalReverse);
1721        assert_eq!(shape.acc_param_idx, 1);
1722
1723        // Caller: `String.join(List.reverse(build(xs, [])), "\n")`
1724        let join_call = call(
1725            dotted("String", "join"),
1726            vec![
1727                call(
1728                    dotted("List", "reverse"),
1729                    vec![call(
1730                        ident_expr("build"),
1731                        vec![ident("xs"), sp(Expr::List(vec![]))],
1732                    )],
1733                ),
1734                sp(Expr::Literal(Literal::Str("\n".to_string()))),
1735            ],
1736        );
1737        let caller = FnDef {
1738            name: "render".to_string(),
1739            line: 2,
1740            params: vec![("xs".to_string(), "List<Int>".to_string())],
1741            return_type: "String".to_string(),
1742            effects: vec![],
1743            desc: None,
1744            body: Arc::new(FnBody::Block(vec![Stmt::Expr(join_call)])),
1745            resolution: None,
1746        };
1747
1748        let mut items = vec![
1749            crate::ast::TopLevel::FnDef(sink),
1750            crate::ast::TopLevel::FnDef(caller),
1751        ];
1752        let report = run_buffer_build_pass(&mut items);
1753        assert_eq!(
1754            report.rewrites, 1,
1755            "external-reverse pattern should be one fusion site"
1756        );
1757        assert_eq!(
1758            report.synthesized.len(),
1759            1,
1760            "exactly one buffered variant for the used sink"
1761        );
1762
1763        // The synthesized variant should be appended.
1764        let synth_present = items.iter().any(|it| match it {
1765            crate::ast::TopLevel::FnDef(fd) => fd.name == "build__buffered",
1766            _ => false,
1767        });
1768        assert!(synth_present, "build__buffered must be appended");
1769    }
1770
1771    fn ident_expr(name: &str) -> Spanned<Expr> {
1772        sp(Expr::Ident(name.to_string()))
1773    }
1774}