1use std::collections::HashMap;
26use std::sync::Arc;
27
28use crate::ast::{Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, TailCallData};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum BufferBuildKind {
53 InternalReverse,
54 ExternalReverse,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct BufferBuildShape {
60 pub acc_param_idx: usize,
64 pub acc_param_name: String,
67 pub kind: BufferBuildKind,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum ConsumerKind {
86 StringJoin,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct FusionSite {
96 pub enclosing_fn: String,
98 pub line: usize,
100 pub sink_fn: String,
102 pub consumer: ConsumerKind,
104}
105
106pub 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
119pub 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
141fn 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 visit_subexprs(expr, expr_line, enclosing_fn, sinks, out);
163}
164
165fn 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
251fn match_buffer_build_shape(fd: &FnDef) -> Option<BufferBuildShape> {
253 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 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 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 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
311fn 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
334fn is_ident_named(expr: &Expr, name: &str) -> bool {
336 matches!(expr, Expr::Ident(n) if n == name)
337}
338
339fn 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 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
410fn is_list_type_str(ty: &str) -> bool {
412 let t = ty.trim();
413 t.starts_with("List<") && t.ends_with('>')
414}
415
416fn 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
433fn 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
462fn 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
477fn 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 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
510fn 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
525fn 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
538pub 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
590fn 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
604fn 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
619fn 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
628fn 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
635pub 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 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 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#[derive(Debug, Clone, Default)]
719pub struct BufferBuildPassReport {
720 pub rewrites: usize,
722 pub synthesized: Vec<String>,
725 pub sink_fns: Vec<String>,
728 pub rewrites_by_sink: std::collections::BTreeMap<String, usize>,
730}
731
732fn 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
747pub 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
775fn 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 descend_into_subexprs(expr, sinks);
786 return;
787 }
788 descend_into_subexprs(expr, sinks);
789}
790
791fn 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
854fn try_rewrite_fusion_site(
858 expr: &Spanned<Expr>,
859 sinks: &HashMap<String, BufferBuildShape>,
860) -> Option<Spanned<Expr>> {
861 let line = expr.line;
862
863 let sink_name = match_string_join_fusion_site(&expr.node, sinks)?;
866 let shape = sinks.get(&sink_name)?;
867
868 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 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 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
927fn build_buffered_variant(fd: &FnDef, shape: &BufferBuildShape) -> Option<FnDef> {
932 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 assert_eq!(tail_data.args.len(), 3);
1511 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 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 match &outer[1].node {
1534 Expr::Ident(name) => assert_eq!(name, "n"),
1535 _ => panic!("expected `n` ident as elem"),
1536 }
1537 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 let true_body = call(dotted("List", "reverse"), vec![ident("acc")]);
1554 let prepend = call(dotted("List", "prepend"), vec![ident("col"), ident("acc")]);
1555 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 let mut fd = canonical_builder("build");
1613 {
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 let sink = canonical_builder("build");
1648 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 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 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 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}