Skip to main content

stryke/
fmt.rs

1//! Pretty-print parsed Perl back to source (`stryke --fmt`).
2//! Regenerate with `python3 tools/gen_fmt.py` after `ast.rs` changes.
3
4#![allow(unused_variables)] // generated `match` arms name fields not always used
5
6use crate::ast::*;
7
8const INDENT: &str = "    ";
9
10/// Format a whole program as Perl-like source.
11pub fn format_program(p: &Program) -> String {
12    p.statements
13        .iter()
14        .map(|s| format_statement_indent(s, 0))
15        .collect::<Vec<_>>()
16        .join("\n")
17}
18
19pub(crate) fn format_sub_sig_param(p: &SubSigParam) -> String {
20    use crate::ast::MatchArrayElem;
21    match p {
22        SubSigParam::Scalar(name, ty) => {
23            let mut s = format!("${}", name);
24            if let Some(t) = ty {
25                s.push_str(": ");
26                s.push_str(&t.display_name());
27            }
28            s
29        }
30        SubSigParam::Array(name) => format!("@{}", name),
31        SubSigParam::Hash(name) => format!("%{}", name),
32        SubSigParam::ArrayDestruct(elems) => {
33            let inner = elems
34                .iter()
35                .map(|x| match x {
36                    MatchArrayElem::Expr(e) => format_expr(e),
37                    MatchArrayElem::CaptureScalar(name) => format!("${}", name),
38                    MatchArrayElem::Rest => "*".to_string(),
39                    MatchArrayElem::RestBind(name) => format!("@{}", name),
40                })
41                .collect::<Vec<_>>()
42                .join(", ");
43            format!("[{inner}]")
44        }
45        SubSigParam::HashDestruct(pairs) => {
46            let inner = pairs
47                .iter()
48                .map(|(k, v)| format!("{} => ${}", k, v))
49                .collect::<Vec<_>>()
50                .join(", ");
51            format!("{{ {inner} }}")
52        }
53    }
54}
55
56#[allow(dead_code)]
57fn format_statement(s: &Statement) -> String {
58    format_statement_indent(s, 0)
59}
60
61fn format_statement_indent(s: &Statement, depth: usize) -> String {
62    let prefix = INDENT.repeat(depth);
63    let lab = s
64        .label
65        .as_ref()
66        .map(|l| format!("{}: ", l))
67        .unwrap_or_default();
68    let body = match &s.kind {
69        StmtKind::Expression(e) => format_expr(e),
70        StmtKind::If {
71            condition,
72            body,
73            elsifs,
74            else_block,
75        } => {
76            let mut s = format!(
77                "if ({}) {{\n{}\n{}}}",
78                format_expr(condition),
79                format_block_indent(body, depth + 1),
80                prefix
81            );
82            for (c, b) in elsifs {
83                s.push_str(&format!(
84                    " elsif ({}) {{\n{}\n{}}}",
85                    format_expr(c),
86                    format_block_indent(b, depth + 1),
87                    prefix
88                ));
89            }
90            if let Some(eb) = else_block {
91                s.push_str(&format!(
92                    " else {{\n{}\n{}}}",
93                    format_block_indent(eb, depth + 1),
94                    prefix
95                ));
96            }
97            s
98        }
99        StmtKind::Unless {
100            condition,
101            body,
102            else_block,
103        } => {
104            let mut s = format!(
105                "unless ({}) {{\n{}\n{}}}",
106                format_expr(condition),
107                format_block_indent(body, depth + 1),
108                prefix
109            );
110            if let Some(eb) = else_block {
111                s.push_str(&format!(
112                    " else {{\n{}\n{}}}",
113                    format_block_indent(eb, depth + 1),
114                    prefix
115                ));
116            }
117            s
118        }
119        StmtKind::While {
120            condition,
121            body,
122            label,
123            continue_block,
124        } => {
125            let lb = label
126                .as_ref()
127                .map(|l| format!("{}: ", l))
128                .unwrap_or_default();
129            let mut s = format!(
130                "{}while ({}) {{\n{}\n{}}}",
131                lb,
132                format_expr(condition),
133                format_block_indent(body, depth + 1),
134                prefix
135            );
136            if let Some(cb) = continue_block {
137                s.push_str(&format!(
138                    " continue {{\n{}\n{}}}",
139                    format_block_indent(cb, depth + 1),
140                    prefix
141                ));
142            }
143            s
144        }
145        StmtKind::Until {
146            condition,
147            body,
148            label,
149            continue_block,
150        } => {
151            let lb = label
152                .as_ref()
153                .map(|l| format!("{}: ", l))
154                .unwrap_or_default();
155            let mut s = format!(
156                "{}until ({}) {{\n{}\n{}}}",
157                lb,
158                format_expr(condition),
159                format_block_indent(body, depth + 1),
160                prefix
161            );
162            if let Some(cb) = continue_block {
163                s.push_str(&format!(
164                    " continue {{\n{}\n{}}}",
165                    format_block_indent(cb, depth + 1),
166                    prefix
167                ));
168            }
169            s
170        }
171        StmtKind::DoWhile { body, condition } => {
172            format!(
173                "do {{\n{}\n{}}} while ({})",
174                format_block_indent(body, depth + 1),
175                prefix,
176                format_expr(condition)
177            )
178        }
179        StmtKind::For {
180            init,
181            condition,
182            step,
183            body,
184            label,
185            continue_block,
186        } => {
187            let lb = label
188                .as_ref()
189                .map(|l| format!("{}: ", l))
190                .unwrap_or_default();
191            let ini = init
192                .as_ref()
193                .map(|s| format_statement_indent(s, 0))
194                .unwrap_or_default();
195            let cond = condition.as_ref().map(format_expr).unwrap_or_default();
196            let st = step.as_ref().map(format_expr).unwrap_or_default();
197            let mut s = format!(
198                "{}for ({}; {}; {}) {{\n{}\n{}}}",
199                lb,
200                ini,
201                cond,
202                st,
203                format_block_indent(body, depth + 1),
204                prefix
205            );
206            if let Some(cb) = continue_block {
207                s.push_str(&format!(
208                    " continue {{\n{}\n{}}}",
209                    format_block_indent(cb, depth + 1),
210                    prefix
211                ));
212            }
213            s
214        }
215        StmtKind::Foreach {
216            var,
217            list,
218            body,
219            label,
220            continue_block,
221        } => {
222            let lb = label
223                .as_ref()
224                .map(|l| format!("{}: ", l))
225                .unwrap_or_default();
226            let mut s = format!(
227                "{}for ${} ({}) {{\n{}\n{}}}",
228                lb,
229                var,
230                format_expr(list),
231                format_block_indent(body, depth + 1),
232                prefix
233            );
234            if let Some(cb) = continue_block {
235                s.push_str(&format!(
236                    " continue {{\n{}\n{}}}",
237                    format_block_indent(cb, depth + 1),
238                    prefix
239                ));
240            }
241            s
242        }
243        StmtKind::SubDecl {
244            name,
245            params,
246            body,
247            prototype,
248        } => {
249            let sig = if !params.is_empty() {
250                format!(
251                    " ({})",
252                    params
253                        .iter()
254                        .map(format_sub_sig_param)
255                        .collect::<Vec<_>>()
256                        .join(", ")
257                )
258            } else {
259                prototype
260                    .as_ref()
261                    .map(|p| format!(" ({})", p))
262                    .unwrap_or_default()
263            };
264            format!(
265                "sub {}{} {{\n{}\n{}}}",
266                name,
267                sig,
268                format_block_indent(body, depth + 1),
269                prefix
270            )
271        }
272        StmtKind::Package { name } => format!("package {}", name),
273        StmtKind::UsePerlVersion { version } => {
274            if version.fract() == 0.0 && *version >= 0.0 {
275                format!("use {}", *version as i64)
276            } else {
277                format!("use {}", version)
278            }
279        }
280        StmtKind::Use { module, imports } => {
281            if imports.is_empty() {
282                format!("use {}", module)
283            } else {
284                format!("use {} {}", module, format_expr_list(imports))
285            }
286        }
287        StmtKind::UseOverload { pairs } => {
288            let inner = pairs
289                .iter()
290                .map(|(k, v)| {
291                    format!(
292                        "'{}' => '{}'",
293                        k.replace('\'', "\\'"),
294                        v.replace('\'', "\\'")
295                    )
296                })
297                .collect::<Vec<_>>()
298                .join(", ");
299            format!("use overload {inner}")
300        }
301        StmtKind::No { module, imports } => {
302            if imports.is_empty() {
303                format!("no {}", module)
304            } else {
305                format!("no {} {}", module, format_expr_list(imports))
306            }
307        }
308        StmtKind::Return(e) => e
309            .as_ref()
310            .map(|x| format!("return {}", format_expr(x)))
311            .unwrap_or_else(|| "return".to_string()),
312        StmtKind::Last(l) => l
313            .as_ref()
314            .map(|x| format!("last {}", x))
315            .unwrap_or_else(|| "last".to_string()),
316        StmtKind::Next(l) => l
317            .as_ref()
318            .map(|x| format!("next {}", x))
319            .unwrap_or_else(|| "next".to_string()),
320        StmtKind::Redo(l) => l
321            .as_ref()
322            .map(|x| format!("redo {}", x))
323            .unwrap_or_else(|| "redo".to_string()),
324        StmtKind::My(decls) => format!("my {}", format_var_decls(decls)),
325        StmtKind::Our(decls) => format!("our {}", format_var_decls(decls)),
326        StmtKind::Local(decls) => format!("local {}", format_var_decls(decls)),
327        StmtKind::State(decls) => format!("state {}", format_var_decls(decls)),
328        StmtKind::LocalExpr {
329            target,
330            initializer,
331        } => {
332            let mut s = format!("local {}", format_expr(target));
333            if let Some(init) = initializer {
334                s.push_str(&format!(" = {}", format_expr(init)));
335            }
336            s
337        }
338        StmtKind::MySync(decls) => format!("mysync {}", format_var_decls(decls)),
339        StmtKind::StmtGroup(b) => format_block_indent(b, depth),
340        StmtKind::Block(b) => format!("{{\n{}\n{}}}", format_block_indent(b, depth + 1), prefix),
341        StmtKind::Begin(b) => format!(
342            "BEGIN {{\n{}\n{}}}",
343            format_block_indent(b, depth + 1),
344            prefix
345        ),
346        StmtKind::UnitCheck(b) => format!(
347            "UNITCHECK {{\n{}\n{}}}",
348            format_block_indent(b, depth + 1),
349            prefix
350        ),
351        StmtKind::Check(b) => format!(
352            "CHECK {{\n{}\n{}}}",
353            format_block_indent(b, depth + 1),
354            prefix
355        ),
356        StmtKind::Init(b) => format!(
357            "INIT {{\n{}\n{}}}",
358            format_block_indent(b, depth + 1),
359            prefix
360        ),
361        StmtKind::End(b) => format!(
362            "END {{\n{}\n{}}}",
363            format_block_indent(b, depth + 1),
364            prefix
365        ),
366        StmtKind::Empty => String::new(),
367        StmtKind::Goto { target } => format!("goto {}", format_expr(target)),
368        StmtKind::Continue(b) => format!(
369            "continue {{\n{}\n{}}}",
370            format_block_indent(b, depth + 1),
371            prefix
372        ),
373        StmtKind::StructDecl { def } => {
374            let fields = def
375                .fields
376                .iter()
377                .map(|f| format!("{} => {}", f.name, f.ty.display_name()))
378                .collect::<Vec<_>>()
379                .join(", ");
380            format!("struct {} {{ {} }}", def.name, fields)
381        }
382        StmtKind::EnumDecl { def } => {
383            let variants = def
384                .variants
385                .iter()
386                .map(|v| {
387                    if let Some(ty) = &v.ty {
388                        format!("{} => {}", v.name, ty.display_name())
389                    } else {
390                        v.name.clone()
391                    }
392                })
393                .collect::<Vec<_>>()
394                .join(", ");
395            format!("enum {} {{ {} }}", def.name, variants)
396        }
397        StmtKind::ClassDecl { def } => {
398            let prefix = if def.is_abstract {
399                "abstract "
400            } else if def.is_final {
401                "final "
402            } else {
403                ""
404            };
405            let mut header = format!("{}class {}", prefix, def.name);
406            if !def.extends.is_empty() {
407                header.push_str(&format!(" extends {}", def.extends.join(", ")));
408            }
409            if !def.implements.is_empty() {
410                header.push_str(&format!(" impl {}", def.implements.join(", ")));
411            }
412            let fields = def
413                .fields
414                .iter()
415                .map(|f| {
416                    let vis = match f.visibility {
417                        crate::ast::Visibility::Private => "priv ",
418                        crate::ast::Visibility::Protected => "prot ",
419                        crate::ast::Visibility::Public => "",
420                    };
421                    format!("{}{}: {}", vis, f.name, f.ty.display_name())
422                })
423                .collect::<Vec<_>>()
424                .join("; ");
425            format!("{} {{ {} }}", header, fields)
426        }
427        StmtKind::TraitDecl { def } => {
428            let methods = def
429                .methods
430                .iter()
431                .map(|m| format!("fn {}", m.name))
432                .collect::<Vec<_>>()
433                .join("; ");
434            format!("trait {} {{ {} }}", def.name, methods)
435        }
436        StmtKind::EvalTimeout { timeout, body } => {
437            format!(
438                "eval_timeout {} {{\n{}\n{}}}",
439                format_expr(timeout),
440                format_block_indent(body, depth + 1),
441                prefix
442            )
443        }
444        StmtKind::TryCatch {
445            try_block,
446            catch_var,
447            catch_block,
448            finally_block,
449        } => {
450            let fin = finally_block
451                .as_ref()
452                .map(|b| {
453                    format!(
454                        " finally {{\n{}\n{}}}",
455                        format_block_indent(b, depth + 1),
456                        prefix
457                    )
458                })
459                .unwrap_or_default();
460            format!(
461                "try {{\n{}\n{}}} catch (${}) {{\n{}\n{}}}{}",
462                format_block_indent(try_block, depth + 1),
463                prefix,
464                catch_var,
465                format_block_indent(catch_block, depth + 1),
466                prefix,
467                fin
468            )
469        }
470        StmtKind::Given { topic, body } => {
471            format!(
472                "given ({}) {{\n{}\n{}}}",
473                format_expr(topic),
474                format_block_indent(body, depth + 1),
475                prefix
476            )
477        }
478        StmtKind::When { cond, body } => {
479            format!(
480                "when ({}) {{\n{}\n{}}}",
481                format_expr(cond),
482                format_block_indent(body, depth + 1),
483                prefix
484            )
485        }
486        StmtKind::DefaultCase { body } => format!(
487            "default {{\n{}\n{}}}",
488            format_block_indent(body, depth + 1),
489            prefix
490        ),
491        StmtKind::FormatDecl { name, lines } => {
492            let mut s = format!("format {} =\n", name);
493            for ln in lines {
494                s.push_str(ln);
495                s.push('\n');
496            }
497            s.push('.');
498            s
499        }
500        StmtKind::Tie {
501            target,
502            class,
503            args,
504        } => {
505            let target_s = match target {
506                crate::ast::TieTarget::Hash(h) => format!("%{}", h),
507                crate::ast::TieTarget::Array(a) => format!("@{}", a),
508                crate::ast::TieTarget::Scalar(s) => format!("${}", s),
509            };
510            let mut s = format!("tie {} {}", target_s, format_expr(class));
511            for a in args {
512                s.push_str(&format!(", {}", format_expr(a)));
513            }
514            s
515        }
516    };
517    format!("{}{}{}", prefix, lab, body)
518}
519
520pub fn format_block(b: &Block) -> String {
521    format_block_indent(b, 0)
522}
523
524fn format_block_indent(b: &Block, depth: usize) -> String {
525    b.iter()
526        .map(|s| format_statement_indent(s, depth))
527        .collect::<Vec<_>>()
528        .join("\n")
529}
530
531/// Format a block as a single line for inline use (short blocks in expressions).
532fn format_block_inline(b: &Block) -> String {
533    b.iter()
534        .map(|s| format_statement_indent(s, 0))
535        .collect::<Vec<_>>()
536        .join("; ")
537}
538
539fn format_var_decls(decls: &[VarDecl]) -> String {
540    decls
541        .iter()
542        .map(|d| {
543            let sig = match d.sigil {
544                Sigil::Scalar => "$",
545                Sigil::Array => "@",
546                Sigil::Hash => "%",
547                Sigil::Typeglob => "*",
548            };
549            let mut s = format!("{}{}", sig, d.name);
550            if let Some(ref t) = d.type_annotation {
551                s.push_str(&format!(" : {}", t.display_name()));
552            }
553            if let Some(ref init) = d.initializer {
554                s.push_str(&format!(" = {}", format_expr(init)));
555            }
556            s
557        })
558        .collect::<Vec<_>>()
559        .join(", ")
560}
561
562pub(crate) fn format_expr_list(es: &[Expr]) -> String {
563    es.iter().map(format_expr).collect::<Vec<_>>().join(", ")
564}
565
566pub(crate) fn format_binop(op: BinOp) -> &'static str {
567    match op {
568        BinOp::Add => "+",
569        BinOp::Sub => "-",
570        BinOp::Mul => "*",
571        BinOp::Div => "/",
572        BinOp::Mod => "%",
573        BinOp::Pow => "**",
574        BinOp::Concat => ".",
575        BinOp::NumEq => "==",
576        BinOp::NumNe => "!=",
577        BinOp::NumLt => "<",
578        BinOp::NumGt => ">",
579        BinOp::NumLe => "<=",
580        BinOp::NumGe => ">=",
581        BinOp::Spaceship => "<=>",
582        BinOp::StrEq => "eq",
583        BinOp::StrNe => "ne",
584        BinOp::StrLt => "lt",
585        BinOp::StrGt => "gt",
586        BinOp::StrLe => "le",
587        BinOp::StrGe => "ge",
588        BinOp::StrCmp => "cmp",
589        BinOp::LogAnd => "&&",
590        BinOp::LogOr => "||",
591        BinOp::DefinedOr => "//",
592        BinOp::BitAnd => "&",
593        BinOp::BitOr => "|",
594        BinOp::BitXor => "^",
595        BinOp::ShiftLeft => "<<",
596        BinOp::ShiftRight => ">>",
597        BinOp::LogAndWord => "and",
598        BinOp::LogOrWord => "or",
599        BinOp::BindMatch => "=~",
600        BinOp::BindNotMatch => "!~",
601    }
602}
603
604pub(crate) fn format_unary(op: UnaryOp) -> &'static str {
605    match op {
606        UnaryOp::Negate => "-",
607        UnaryOp::LogNot => "!",
608        UnaryOp::BitNot => "~",
609        UnaryOp::LogNotWord => "not",
610        UnaryOp::PreIncrement => "++",
611        UnaryOp::PreDecrement => "--",
612        UnaryOp::Ref => "\\",
613    }
614}
615
616pub(crate) fn format_postfix(op: PostfixOp) -> &'static str {
617    match op {
618        PostfixOp::Increment => "++",
619        PostfixOp::Decrement => "--",
620    }
621}
622
623pub(crate) fn format_string_part(p: &StringPart) -> String {
624    match p {
625        StringPart::Literal(s) => escape_interpolated_literal(s),
626        StringPart::ScalarVar(n) => format!("${{{}}}", n),
627        StringPart::ArrayVar(n) => format!("@{{{}}}", n),
628        StringPart::Expr(e) => format_expr(e),
629    }
630}
631
632/// Escape special characters inside the literal portions of an interpolated string.
633pub(crate) fn escape_interpolated_literal(s: &str) -> String {
634    let mut out = String::new();
635    for c in s.chars() {
636        match c {
637            '\\' => out.push_str("\\\\"),
638            '"' => out.push_str("\\\""),
639            '\n' => out.push_str("\\n"),
640            '\r' => out.push_str("\\r"),
641            '\t' => out.push_str("\\t"),
642            '\x1b' => out.push_str("\\e"),
643            c if c.is_control() => {
644                out.push_str(&format!("\\x{{{:02x}}}", c as u32));
645            }
646            _ => out.push(c),
647        }
648    }
649    out
650}
651
652/// Escape control characters in regex pattern/replacement strings.
653/// Does not escape `/` since that's handled by the delimiter.
654pub(crate) fn escape_regex_part(s: &str) -> String {
655    let mut out = String::new();
656    for c in s.chars() {
657        match c {
658            '\n' => out.push_str("\\n"),
659            '\r' => out.push_str("\\r"),
660            '\t' => out.push_str("\\t"),
661            '\x1b' => out.push_str("\\x1b"),
662            c if c.is_control() => {
663                out.push_str(&format!("\\x{:02x}", c as u32));
664            }
665            _ => out.push(c),
666        }
667    }
668    out
669}
670
671pub(crate) fn format_string_literal(s: &str) -> String {
672    let mut out = String::new();
673    out.push('"');
674    for c in s.chars() {
675        match c {
676            '\\' => out.push_str("\\\\"),
677            '"' => out.push_str("\\\""),
678            '\n' => out.push_str("\\n"),
679            '\r' => out.push_str("\\r"),
680            '\t' => out.push_str("\\t"),
681            '\x1b' => out.push_str("\\e"),
682            c if c.is_control() => {
683                out.push_str(&format!("\\x{{{:02x}}}", c as u32));
684            }
685            _ => out.push(c),
686        }
687    }
688    out.push('"');
689    out
690}
691
692/// Format an expression; aims for readable Perl-like output.
693pub fn format_expr(e: &Expr) -> String {
694    match &e.kind {
695        ExprKind::Integer(n) => n.to_string(),
696        ExprKind::Float(f) => format!("{}", f),
697        ExprKind::String(s) => format_string_literal(s),
698        ExprKind::Bareword(s) => s.clone(),
699        ExprKind::Regex(p, fl) => format!("/{}/{}/", p, fl),
700        ExprKind::QW(ws) => format!("qw({})", ws.join(" ")),
701        ExprKind::Undef => "undef".to_string(),
702        ExprKind::MagicConst(crate::ast::MagicConstKind::File) => "__FILE__".to_string(),
703        ExprKind::MagicConst(crate::ast::MagicConstKind::Line) => "__LINE__".to_string(),
704        ExprKind::MagicConst(crate::ast::MagicConstKind::Sub) => "__SUB__".to_string(),
705        ExprKind::InterpolatedString(parts) => {
706            format!(
707                "\"{}\"",
708                parts.iter().map(format_string_part).collect::<String>()
709            )
710        }
711        ExprKind::ScalarVar(name) => format!("${}", name),
712        ExprKind::ArrayVar(name) => format!("@{}", name),
713        ExprKind::HashVar(name) => format!("%{}", name),
714        ExprKind::Typeglob(name) => format!("*{}", name),
715        ExprKind::TypeglobExpr(e) => format!("*{{ {} }}", format_expr(e)),
716        ExprKind::ArrayElement { array, index } => format!("${}[{}]", array, format_expr(index)),
717        ExprKind::HashElement { hash, key } => format!("${}{{{}}}", hash, format_expr(key)),
718        ExprKind::ArraySlice { array, indices } => format!(
719            "@{}[{}]",
720            array,
721            indices
722                .iter()
723                .map(format_expr)
724                .collect::<Vec<_>>()
725                .join(", ")
726        ),
727        ExprKind::HashSlice { hash, keys } => format!(
728            "@{}{{{}}}",
729            hash,
730            keys.iter().map(format_expr).collect::<Vec<_>>().join(", ")
731        ),
732        ExprKind::HashSliceDeref { container, keys } => format!(
733            "@{}{{{}}}",
734            format_expr(container),
735            keys.iter().map(format_expr).collect::<Vec<_>>().join(", ")
736        ),
737        ExprKind::AnonymousListSlice { source, indices } => format!(
738            "({})[{}]",
739            format_expr(source),
740            indices
741                .iter()
742                .map(format_expr)
743                .collect::<Vec<_>>()
744                .join(", ")
745        ),
746        ExprKind::ScalarRef(inner) => format!("\\{}", format_expr(inner)),
747        ExprKind::ArrayRef(elems) => format!("[{}]", format_expr_list(elems)),
748        ExprKind::HashRef(pairs) => {
749            let inner = pairs
750                .iter()
751                .map(|(k, v)| format!("{} => {}", format_expr(k), format_expr(v)))
752                .collect::<Vec<_>>()
753                .join(", ");
754            format!("{{{}}}", inner)
755        }
756        ExprKind::CodeRef { params, body } => {
757            if params.is_empty() {
758                format!("sub {{ {} }}", format_block_inline(body))
759            } else {
760                let sig = params
761                    .iter()
762                    .map(format_sub_sig_param)
763                    .collect::<Vec<_>>()
764                    .join(", ");
765                format!("sub ({}) {{ {} }}", sig, format_block_inline(body))
766            }
767        }
768        ExprKind::SubroutineRef(name) => format!("&{}", name),
769        ExprKind::SubroutineCodeRef(name) => format!("\\&{}", name),
770        ExprKind::DynamicSubCodeRef(e) => format!("\\&{{ {} }}", format_expr(e)),
771        ExprKind::Deref { expr, kind } => match kind {
772            Sigil::Scalar => format!("${{{}}}", format_expr(expr)),
773            Sigil::Array => format!("@{{${}}}", format_expr(expr)),
774            Sigil::Hash => format!("%{{${}}}", format_expr(expr)),
775            Sigil::Typeglob => format!("*{{${}}}", format_expr(expr)),
776        },
777        ExprKind::ArrowDeref { expr, index, kind } => match kind {
778            DerefKind::Array => format!("({})->[{}]", format_expr(expr), format_expr(index)),
779            DerefKind::Hash => format!("({})->{{{}}}", format_expr(expr), format_expr(index)),
780            DerefKind::Call => format!("({})->({})", format_expr(expr), format_expr(index)),
781        },
782        ExprKind::BinOp { left, op, right } => format!(
783            "{} {} {}",
784            format_expr(left),
785            format_binop(*op),
786            format_expr(right)
787        ),
788        ExprKind::UnaryOp { op, expr } => format!("{}{}", format_unary(*op), format_expr(expr)),
789        ExprKind::PostfixOp { expr, op } => {
790            format!("{}{}", format_expr(expr), format_postfix(*op))
791        }
792        ExprKind::Assign { target, value } => {
793            format!("{} = {}", format_expr(target), format_expr(value))
794        }
795        ExprKind::CompoundAssign { target, op, value } => format!(
796            "{} {}= {}",
797            format_expr(target),
798            format_binop(*op),
799            format_expr(value)
800        ),
801        ExprKind::Ternary {
802            condition,
803            then_expr,
804            else_expr,
805        } => format!(
806            "{} ? {} : {}",
807            format_expr(condition),
808            format_expr(then_expr),
809            format_expr(else_expr)
810        ),
811        ExprKind::Repeat { expr, count } => {
812            format!("{} x {}", format_expr(expr), format_expr(count))
813        }
814        ExprKind::Range {
815            from,
816            to,
817            exclusive,
818        } => {
819            let op = if *exclusive { "..." } else { ".." };
820            format!("{} {} {}", format_expr(from), op, format_expr(to))
821        }
822        ExprKind::FuncCall { name, args } => format!(
823            "{}({})",
824            name,
825            args.iter().map(format_expr).collect::<Vec<_>>().join(", ")
826        ),
827        ExprKind::MethodCall {
828            object,
829            method,
830            args,
831            super_call,
832        } => {
833            let m = if *super_call {
834                format!("SUPER::{}", method)
835            } else {
836                method.clone()
837            };
838            format!(
839                "{}->{}({})",
840                format_expr(object),
841                m,
842                args.iter().map(format_expr).collect::<Vec<_>>().join(", ")
843            )
844        }
845        ExprKind::IndirectCall {
846            target,
847            args,
848            ampersand,
849            pass_caller_arglist,
850        } => {
851            if *pass_caller_arglist && args.is_empty() {
852                format!("&{}", format_expr(target))
853            } else {
854                let inner = format!(
855                    "{}({})",
856                    format_expr(target),
857                    args.iter().map(format_expr).collect::<Vec<_>>().join(", ")
858                );
859                if *ampersand {
860                    format!("&{}", inner)
861                } else {
862                    inner
863                }
864            }
865        }
866        ExprKind::Print { handle, args } => {
867            let h = handle
868                .as_ref()
869                .map(|h| format!("{} ", h))
870                .unwrap_or_default();
871            format!("print {}{}", h, format_expr_list(args))
872        }
873        ExprKind::Say { handle, args } => {
874            let h = handle
875                .as_ref()
876                .map(|h| format!("{} ", h))
877                .unwrap_or_default();
878            format!("say {}{}", h, format_expr_list(args))
879        }
880        ExprKind::Printf { handle, args } => {
881            let h = handle
882                .as_ref()
883                .map(|h| format!("{} ", h))
884                .unwrap_or_default();
885            format!("printf {}{}", h, format_expr_list(args))
886        }
887        ExprKind::Die(args) => {
888            if args.is_empty() {
889                "die".to_string()
890            } else {
891                format!("die {}", format_expr_list(args))
892            }
893        }
894        ExprKind::Warn(args) => {
895            if args.is_empty() {
896                "warn".to_string()
897            } else {
898                format!("warn {}", format_expr_list(args))
899            }
900        }
901        ExprKind::Match {
902            expr,
903            pattern,
904            flags,
905            scalar_g: _,
906            delim: _,
907        } => format!("{} =~ /{}/{}", format_expr(expr), pattern, flags),
908        ExprKind::Substitution {
909            expr,
910            pattern,
911            replacement,
912            flags,
913            delim: _,
914        } => format!(
915            "{} =~ s/{}/{}/{}",
916            format_expr(expr),
917            pattern,
918            replacement,
919            flags
920        ),
921        ExprKind::Transliterate {
922            expr,
923            from,
924            to,
925            flags,
926            delim: _,
927        } => format!("{} =~ tr/{}/{}/{}", format_expr(expr), from, to, flags),
928        ExprKind::MapExpr {
929            block,
930            list,
931            flatten_array_refs,
932            stream,
933        } => {
934            let kw = match (*flatten_array_refs, *stream) {
935                (true, true) => "flat_maps",
936                (true, false) => "flat_map",
937                (false, true) => "maps",
938                (false, false) => "map",
939            };
940            format!(
941                "{kw} {{ {} }} {}",
942                format_block_inline(block),
943                format_expr(list)
944            )
945        }
946        ExprKind::MapExprComma {
947            expr,
948            list,
949            flatten_array_refs,
950            stream,
951        } => {
952            let kw = match (*flatten_array_refs, *stream) {
953                (true, true) => "flat_maps",
954                (true, false) => "flat_map",
955                (false, true) => "maps",
956                (false, false) => "map",
957            };
958            format!("{kw} {}, {}", format_expr(expr), format_expr(list))
959        }
960        ExprKind::GrepExpr {
961            block,
962            list,
963            keyword,
964        } => {
965            format!(
966                "{} {{ {} }} {}",
967                keyword.as_str(),
968                format_block_inline(block),
969                format_expr(list)
970            )
971        }
972        ExprKind::GrepExprComma {
973            expr,
974            list,
975            keyword,
976        } => {
977            format!(
978                "{} {}, {}",
979                keyword.as_str(),
980                format_expr(expr),
981                format_expr(list)
982            )
983        }
984        ExprKind::ForEachExpr { block, list } => {
985            format!(
986                "fore {{ {} }} {}",
987                format_block_inline(block),
988                format_expr(list)
989            )
990        }
991        ExprKind::SortExpr { cmp, list } => match cmp {
992            Some(crate::ast::SortComparator::Block(b)) => {
993                format!(
994                    "sort {{ {} }} {}",
995                    format_block_inline(b),
996                    format_expr(list)
997                )
998            }
999            Some(crate::ast::SortComparator::Code(e)) => {
1000                format!("sort {} {}", format_expr(e), format_expr(list))
1001            }
1002            None => format!("sort {}", format_expr(list)),
1003        },
1004        ExprKind::ReverseExpr(e) => format!("reverse {}", format_expr(e)),
1005        ExprKind::ScalarReverse(e) => format!("rev {}", format_expr(e)),
1006        ExprKind::JoinExpr { separator, list } => {
1007            format!("join({}, {})", format_expr(separator), format_expr(list))
1008        }
1009        ExprKind::SplitExpr {
1010            pattern,
1011            string,
1012            limit,
1013        } => match limit {
1014            Some(l) => format!(
1015                "split({}, {}, {})",
1016                format_expr(pattern),
1017                format_expr(string),
1018                format_expr(l)
1019            ),
1020            None => format!("split({}, {})", format_expr(pattern), format_expr(string)),
1021        },
1022        ExprKind::PMapExpr {
1023            block,
1024            list,
1025            progress,
1026            flat_outputs,
1027            on_cluster,
1028            stream: _,
1029        } => {
1030            let kw = match (flat_outputs, on_cluster.is_some()) {
1031                (true, true) => "pflat_map_on",
1032                (true, false) => "pflat_map",
1033                (false, true) => "pmap_on",
1034                (false, false) => "pmap",
1035            };
1036            let base = if let Some(c) = on_cluster {
1037                format!(
1038                    "{kw} {} {{ {} }} {}",
1039                    format_expr(c),
1040                    format_block_inline(block),
1041                    format_expr(list)
1042                )
1043            } else {
1044                format!(
1045                    "{kw} {{ {} }} {}",
1046                    format_block_inline(block),
1047                    format_expr(list)
1048                )
1049            };
1050            match progress {
1051                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1052                None => base,
1053            }
1054        }
1055        ExprKind::PMapChunkedExpr {
1056            chunk_size,
1057            block,
1058            list,
1059            progress,
1060        } => {
1061            let base = format!(
1062                "pmap_chunked {} {{ {} }} {}",
1063                format_expr(chunk_size),
1064                format_block_inline(block),
1065                format_expr(list)
1066            );
1067            match progress {
1068                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1069                None => base,
1070            }
1071        }
1072        ExprKind::PGrepExpr {
1073            block,
1074            list,
1075            progress,
1076            stream: _,
1077        } => {
1078            let base = format!(
1079                "pgrep {{ {} }} {}",
1080                format_block_inline(block),
1081                format_expr(list)
1082            );
1083            match progress {
1084                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1085                None => base,
1086            }
1087        }
1088        ExprKind::PForExpr {
1089            block,
1090            list,
1091            progress,
1092        } => {
1093            let base = format!(
1094                "pfor {{ {} }} {}",
1095                format_block_inline(block),
1096                format_expr(list)
1097            );
1098            match progress {
1099                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1100                None => base,
1101            }
1102        }
1103        ExprKind::ParLinesExpr {
1104            path,
1105            callback,
1106            progress,
1107        } => match progress {
1108            Some(p) => format!(
1109                "par_lines({}, {}, progress => {})",
1110                format_expr(path),
1111                format_expr(callback),
1112                format_expr(p)
1113            ),
1114            None => format!(
1115                "par_lines({}, {})",
1116                format_expr(path),
1117                format_expr(callback)
1118            ),
1119        },
1120        ExprKind::ParWalkExpr {
1121            path,
1122            callback,
1123            progress,
1124        } => match progress {
1125            Some(p) => format!(
1126                "par_walk({}, {}, progress => {})",
1127                format_expr(path),
1128                format_expr(callback),
1129                format_expr(p)
1130            ),
1131            None => format!("par_walk({}, {})", format_expr(path), format_expr(callback)),
1132        },
1133        ExprKind::PwatchExpr { path, callback } => {
1134            format!("pwatch({}, {})", format_expr(path), format_expr(callback))
1135        }
1136        ExprKind::PSortExpr {
1137            cmp,
1138            list,
1139            progress,
1140        } => {
1141            let base = match cmp {
1142                Some(b) => format!(
1143                    "psort {{ {} }} {}",
1144                    format_block_inline(b),
1145                    format_expr(list)
1146                ),
1147                None => format!("psort {}", format_expr(list)),
1148            };
1149            match progress {
1150                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1151                None => base,
1152            }
1153        }
1154        ExprKind::ReduceExpr { block, list } => format!(
1155            "reduce {{ {} }} {}",
1156            format_block_inline(block),
1157            format_expr(list)
1158        ),
1159        ExprKind::PReduceExpr {
1160            block,
1161            list,
1162            progress,
1163        } => {
1164            let base = format!(
1165                "preduce {{ {} }} {}",
1166                format_block_inline(block),
1167                format_expr(list)
1168            );
1169            match progress {
1170                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1171                None => base,
1172            }
1173        }
1174        ExprKind::PReduceInitExpr {
1175            init,
1176            block,
1177            list,
1178            progress,
1179        } => {
1180            let base = format!(
1181                "preduce_init {}, {{ {} }} {}",
1182                format_expr(init),
1183                format_block_inline(block),
1184                format_expr(list)
1185            );
1186            match progress {
1187                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1188                None => base,
1189            }
1190        }
1191        ExprKind::PMapReduceExpr {
1192            map_block,
1193            reduce_block,
1194            list,
1195            progress,
1196        } => {
1197            let base = format!(
1198                "pmap_reduce {{ {} }} {{ {} }} {}",
1199                format_block_inline(map_block),
1200                format_block_inline(reduce_block),
1201                format_expr(list)
1202            );
1203            match progress {
1204                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1205                None => base,
1206            }
1207        }
1208        ExprKind::PcacheExpr {
1209            block,
1210            list,
1211            progress,
1212        } => {
1213            let base = format!(
1214                "pcache {{ {} }} {}",
1215                format_block_inline(block),
1216                format_expr(list)
1217            );
1218            match progress {
1219                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1220                None => base,
1221            }
1222        }
1223        ExprKind::PselectExpr { receivers, timeout } => {
1224            let inner = receivers
1225                .iter()
1226                .map(format_expr)
1227                .collect::<Vec<_>>()
1228                .join(", ");
1229            match timeout {
1230                Some(t) => format!("pselect({}, timeout => {})", inner, format_expr(t)),
1231                None => format!("pselect({})", inner),
1232            }
1233        }
1234        ExprKind::FanExpr {
1235            count,
1236            block,
1237            progress,
1238            capture,
1239        } => {
1240            let kw = if *capture { "fan_cap" } else { "fan" };
1241            let base = match count {
1242                Some(c) => format!(
1243                    "{} {} {{ {} }}",
1244                    kw,
1245                    format_expr(c),
1246                    format_block_inline(block)
1247                ),
1248                None => format!("{} {{ {} }}", kw, format_block_inline(block)),
1249            };
1250            match progress {
1251                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1252                None => base,
1253            }
1254        }
1255        ExprKind::AsyncBlock { body } => format!("async {{ {} }}", format_block_inline(body)),
1256        ExprKind::SpawnBlock { body } => format!("spawn {{ {} }}", format_block_inline(body)),
1257        ExprKind::Trace { body } => format!("trace {{ {} }}", format_block_inline(body)),
1258        ExprKind::Timer { body } => format!("timer {{ {} }}", format_block_inline(body)),
1259        ExprKind::Bench { body, times } => format!(
1260            "bench {{ {} }} {}",
1261            format_block_inline(body),
1262            format_expr(times)
1263        ),
1264        ExprKind::Await(e) => format!("await {}", format_expr(e)),
1265        ExprKind::Slurp(e) => format!("slurp {}", format_expr(e)),
1266        ExprKind::Capture(e) => format!("capture {}", format_expr(e)),
1267        ExprKind::Qx(e) => format!("qx {}", format_expr(e)),
1268        ExprKind::FetchUrl(e) => format!("fetch_url {}", format_expr(e)),
1269        ExprKind::Pchannel { capacity } => match capacity {
1270            Some(c) => format!("pchannel({})", format_expr(c)),
1271            None => "pchannel()".to_string(),
1272        },
1273        ExprKind::Push { array, values } => {
1274            format!("push({}, {})", format_expr(array), format_expr_list(values))
1275        }
1276        ExprKind::Pop(e) => format!("pop {}", format_expr(e)),
1277        ExprKind::Shift(e) => format!("shift {}", format_expr(e)),
1278        ExprKind::Unshift { array, values } => format!(
1279            "unshift({}, {})",
1280            format_expr(array),
1281            format_expr_list(values)
1282        ),
1283        ExprKind::Splice {
1284            array,
1285            offset,
1286            length,
1287            replacement,
1288        } => {
1289            let mut parts = vec![format_expr(array)];
1290            if let Some(o) = offset {
1291                parts.push(format_expr(o));
1292            }
1293            if let Some(l) = length {
1294                parts.push(format_expr(l));
1295            }
1296            if !replacement.is_empty() {
1297                parts.push(format_expr_list(replacement));
1298            }
1299            format!("splice({})", parts.join(", "))
1300        }
1301        ExprKind::Delete(e) => format!("delete {}", format_expr(e)),
1302        ExprKind::Exists(e) => format!("exists {}", format_expr(e)),
1303        ExprKind::Keys(e) => format!("keys {}", format_expr(e)),
1304        ExprKind::Values(e) => format!("values {}", format_expr(e)),
1305        ExprKind::Each(e) => format!("each {}", format_expr(e)),
1306        ExprKind::Chomp(e) => format!("chomp {}", format_expr(e)),
1307        ExprKind::Chop(e) => format!("chop {}", format_expr(e)),
1308        ExprKind::Length(e) => format!("length {}", format_expr(e)),
1309        ExprKind::Substr {
1310            string,
1311            offset,
1312            length,
1313            replacement,
1314        } => {
1315            let mut parts = vec![format_expr(string), format_expr(offset)];
1316            if let Some(l) = length {
1317                parts.push(format_expr(l));
1318            }
1319            if let Some(r) = replacement {
1320                parts.push(format_expr(r));
1321            }
1322            format!("substr({})", parts.join(", "))
1323        }
1324        ExprKind::Index {
1325            string,
1326            substr,
1327            position,
1328        } => match position {
1329            Some(p) => format!(
1330                "index({}, {}, {})",
1331                format_expr(string),
1332                format_expr(substr),
1333                format_expr(p)
1334            ),
1335            None => format!("index({}, {})", format_expr(string), format_expr(substr)),
1336        },
1337        ExprKind::Rindex {
1338            string,
1339            substr,
1340            position,
1341        } => match position {
1342            Some(p) => format!(
1343                "rindex({}, {}, {})",
1344                format_expr(string),
1345                format_expr(substr),
1346                format_expr(p)
1347            ),
1348            None => format!("rindex({}, {})", format_expr(string), format_expr(substr)),
1349        },
1350        ExprKind::Sprintf { format, args } => format!(
1351            "sprintf({}, {})",
1352            format_expr(format),
1353            format_expr_list(args)
1354        ),
1355        ExprKind::Abs(e) => format!("abs {}", format_expr(e)),
1356        ExprKind::Int(e) => format!("int {}", format_expr(e)),
1357        ExprKind::Sqrt(e) => format!("sqrt {}", format_expr(e)),
1358        ExprKind::Sin(e) => format!("sin {}", format_expr(e)),
1359        ExprKind::Cos(e) => format!("cos {}", format_expr(e)),
1360        ExprKind::Atan2 { y, x } => format!("atan2({}, {})", format_expr(y), format_expr(x)),
1361        ExprKind::Exp(e) => format!("exp {}", format_expr(e)),
1362        ExprKind::Log(e) => format!("log {}", format_expr(e)),
1363        ExprKind::Rand(opt) => match opt {
1364            Some(e) => format!("rand({})", format_expr(e)),
1365            None => "rand".to_string(),
1366        },
1367        ExprKind::Srand(opt) => match opt {
1368            Some(e) => format!("srand({})", format_expr(e)),
1369            None => "srand".to_string(),
1370        },
1371        ExprKind::Hex(e) => format!("hex {}", format_expr(e)),
1372        ExprKind::Oct(e) => format!("oct {}", format_expr(e)),
1373        ExprKind::Lc(e) => format!("lc {}", format_expr(e)),
1374        ExprKind::Uc(e) => format!("uc {}", format_expr(e)),
1375        ExprKind::Lcfirst(e) => format!("lcfirst {}", format_expr(e)),
1376        ExprKind::Ucfirst(e) => format!("ucfirst {}", format_expr(e)),
1377        ExprKind::Fc(e) => format!("fc {}", format_expr(e)),
1378        ExprKind::Crypt { plaintext, salt } => {
1379            format!("crypt({}, {})", format_expr(plaintext), format_expr(salt))
1380        }
1381        ExprKind::Pos(opt) => match opt {
1382            Some(e) => format!("pos({})", format_expr(e)),
1383            None => "pos".to_string(),
1384        },
1385        ExprKind::Study(e) => format!("study {}", format_expr(e)),
1386        ExprKind::Defined(e) => format!("defined {}", format_expr(e)),
1387        ExprKind::Ref(e) => format!("ref {}", format_expr(e)),
1388        ExprKind::ScalarContext(e) => format!("scalar {}", format_expr(e)),
1389        ExprKind::Chr(e) => format!("chr {}", format_expr(e)),
1390        ExprKind::Ord(e) => format!("ord {}", format_expr(e)),
1391        ExprKind::OpenMyHandle { name } => format!("my ${}", name),
1392        ExprKind::Open { handle, mode, file } => match file {
1393            Some(f) => format!(
1394                "open({}, {}, {})",
1395                format_expr(handle),
1396                format_expr(mode),
1397                format_expr(f)
1398            ),
1399            None => format!("open({}, {})", format_expr(handle), format_expr(mode)),
1400        },
1401        ExprKind::Close(e) => format!("close {}", format_expr(e)),
1402        ExprKind::ReadLine(handle) => match handle {
1403            Some(h) => {
1404                if h.starts_with(|c: char| c.is_uppercase()) {
1405                    format!("<{}>", h)
1406                } else {
1407                    format!("<${}>", h)
1408                }
1409            }
1410            None => "<STDIN>".to_string(),
1411        },
1412        ExprKind::Eof(opt) => match opt {
1413            Some(e) => format!("eof({})", format_expr(e)),
1414            None => "eof".to_string(),
1415        },
1416        ExprKind::Opendir { handle, path } => {
1417            format!("opendir({}, {})", format_expr(handle), format_expr(path))
1418        }
1419        ExprKind::Readdir(e) => format!("readdir {}", format_expr(e)),
1420        ExprKind::Closedir(e) => format!("closedir {}", format_expr(e)),
1421        ExprKind::Rewinddir(e) => format!("rewinddir {}", format_expr(e)),
1422        ExprKind::Telldir(e) => format!("telldir {}", format_expr(e)),
1423        ExprKind::Seekdir { handle, position } => format!(
1424            "seekdir({}, {})",
1425            format_expr(handle),
1426            format_expr(position)
1427        ),
1428        ExprKind::FileTest { op, expr } => format!("-{}{}", op, format_expr(expr)),
1429        ExprKind::System(args) => format!("system({})", format_expr_list(args)),
1430        ExprKind::Exec(args) => format!("exec({})", format_expr_list(args)),
1431        ExprKind::Eval(e) => format!("eval {}", format_expr(e)),
1432        ExprKind::Do(e) => format!("do {}", format_expr(e)),
1433        ExprKind::Require(e) => format!("require {}", format_expr(e)),
1434        ExprKind::Exit(opt) => match opt {
1435            Some(e) => format!("exit({})", format_expr(e)),
1436            None => "exit".to_string(),
1437        },
1438        ExprKind::Chdir(e) => format!("chdir {}", format_expr(e)),
1439        ExprKind::Mkdir { path, mode } => match mode {
1440            Some(m) => format!("mkdir({}, {})", format_expr(path), format_expr(m)),
1441            None => format!("mkdir({})", format_expr(path)),
1442        },
1443        ExprKind::Unlink(args) => format!("unlink({})", format_expr_list(args)),
1444        ExprKind::Rename { old, new } => {
1445            format!("rename({}, {})", format_expr(old), format_expr(new))
1446        }
1447        ExprKind::Chmod(args) => format!("chmod({})", format_expr_list(args)),
1448        ExprKind::Chown(args) => format!("chown({})", format_expr_list(args)),
1449        ExprKind::Stat(e) => format!("stat {}", format_expr(e)),
1450        ExprKind::Lstat(e) => format!("lstat {}", format_expr(e)),
1451        ExprKind::Link { old, new } => format!("link({}, {})", format_expr(old), format_expr(new)),
1452        ExprKind::Symlink { old, new } => {
1453            format!("symlink({}, {})", format_expr(old), format_expr(new))
1454        }
1455        ExprKind::Readlink(e) => format!("readlink {}", format_expr(e)),
1456        ExprKind::Glob(args) => format!("glob({})", format_expr_list(args)),
1457        ExprKind::Files(args) => format!("files({})", format_expr_list(args)),
1458        ExprKind::Filesf(args) => format!("filesf({})", format_expr_list(args)),
1459        ExprKind::FilesfRecursive(args) => format!("fr({})", format_expr_list(args)),
1460        ExprKind::Dirs(args) => format!("dirs({})", format_expr_list(args)),
1461        ExprKind::DirsRecursive(args) => format!("dr({})", format_expr_list(args)),
1462        ExprKind::SymLinks(args) => format!("sym_links({})", format_expr_list(args)),
1463        ExprKind::Sockets(args) => format!("sockets({})", format_expr_list(args)),
1464        ExprKind::Pipes(args) => format!("pipes({})", format_expr_list(args)),
1465        ExprKind::BlockDevices(args) => format!("block_devices({})", format_expr_list(args)),
1466        ExprKind::CharDevices(args) => format!("char_devices({})", format_expr_list(args)),
1467        ExprKind::GlobPar { args, progress } => {
1468            let base = format!("glob_par({})", format_expr_list(args));
1469            match progress {
1470                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1471                None => base,
1472            }
1473        }
1474        ExprKind::ParSed { args, progress } => {
1475            let base = format!("par_sed({})", format_expr_list(args));
1476            match progress {
1477                Some(p) => format!("{}, progress => {}", base, format_expr(p)),
1478                None => base,
1479            }
1480        }
1481        ExprKind::Bless { ref_expr, class } => match class {
1482            Some(c) => format!("bless({}, {})", format_expr(ref_expr), format_expr(c)),
1483            None => format!("bless({})", format_expr(ref_expr)),
1484        },
1485        ExprKind::Caller(opt) => match opt {
1486            Some(e) => format!("caller({})", format_expr(e)),
1487            None => "caller".to_string(),
1488        },
1489        ExprKind::Wantarray => "wantarray".to_string(),
1490        ExprKind::List(exprs) => format!("({})", format_expr_list(exprs)),
1491        ExprKind::PostfixIf { expr, condition } => {
1492            format!("{} if {}", format_expr(expr), format_expr(condition))
1493        }
1494        ExprKind::PostfixUnless { expr, condition } => {
1495            format!("{} unless {}", format_expr(expr), format_expr(condition))
1496        }
1497        ExprKind::PostfixWhile { expr, condition } => {
1498            format!("{} while {}", format_expr(expr), format_expr(condition))
1499        }
1500        ExprKind::PostfixUntil { expr, condition } => {
1501            format!("{} until {}", format_expr(expr), format_expr(condition))
1502        }
1503        ExprKind::PostfixForeach { expr, list } => {
1504            format!("{} foreach {}", format_expr(expr), format_expr(list))
1505        }
1506        ExprKind::AlgebraicMatch { subject, arms } => {
1507            let arms_s = arms
1508                .iter()
1509                .map(|a| {
1510                    let guard_s = a
1511                        .guard
1512                        .as_ref()
1513                        .map(|g| format!(" if {}", format_expr(g)))
1514                        .unwrap_or_default();
1515                    format!(
1516                        "{}{} => {}",
1517                        format_match_pattern(&a.pattern),
1518                        guard_s,
1519                        format_expr(&a.body)
1520                    )
1521                })
1522                .collect::<Vec<_>>()
1523                .join(", ");
1524            format!("match ({}) {{ {} }}", format_expr(subject), arms_s)
1525        }
1526        ExprKind::RetryBlock {
1527            body,
1528            times,
1529            backoff,
1530        } => {
1531            let bo = match backoff {
1532                crate::ast::RetryBackoff::None => "none",
1533                crate::ast::RetryBackoff::Linear => "linear",
1534                crate::ast::RetryBackoff::Exponential => "exponential",
1535            };
1536            format!(
1537                "retry {{ {} }} times => {}, backoff => {}",
1538                format_block_inline(body),
1539                format_expr(times),
1540                bo
1541            )
1542        }
1543        ExprKind::RateLimitBlock {
1544            max, window, body, ..
1545        } => {
1546            format!(
1547                "rate_limit({}, {}) {{ {} }}",
1548                format_expr(max),
1549                format_expr(window),
1550                format_block_inline(body)
1551            )
1552        }
1553        ExprKind::EveryBlock { interval, body } => {
1554            format!(
1555                "every({}) {{ {} }}",
1556                format_expr(interval),
1557                format_block_inline(body)
1558            )
1559        }
1560        ExprKind::GenBlock { body } => {
1561            format!("gen {{ {} }}", format_block_inline(body))
1562        }
1563        ExprKind::Yield(e) => {
1564            format!("yield {}", format_expr(e))
1565        }
1566        ExprKind::Spinner { message, body } => {
1567            format!(
1568                "spinner {} {{ {} }}",
1569                format_expr(message),
1570                body.iter()
1571                    .map(format_statement)
1572                    .collect::<Vec<_>>()
1573                    .join("; ")
1574            )
1575        }
1576        ExprKind::MyExpr { keyword, decls } => {
1577            // Render `my $x = …` etc. inline. Single-decl is the common case
1578            // (e.g. `if (my $x = …)`); list-decl reuses the same formatter.
1579            let parts: Vec<String> = decls
1580                .iter()
1581                .map(|d| {
1582                    let sigil = match d.sigil {
1583                        crate::ast::Sigil::Scalar => '$',
1584                        crate::ast::Sigil::Array => '@',
1585                        crate::ast::Sigil::Hash => '%',
1586                        crate::ast::Sigil::Typeglob => '*',
1587                    };
1588                    let mut s = format!("{}{}", sigil, d.name);
1589                    if let Some(init) = &d.initializer {
1590                        s.push_str(" = ");
1591                        s.push_str(&format_expr(init));
1592                    }
1593                    s
1594                })
1595                .collect();
1596            if parts.len() == 1 {
1597                format!("{} {}", keyword, parts[0])
1598            } else {
1599                format!("{} ({})", keyword, parts.join(", "))
1600            }
1601        }
1602    }
1603}
1604
1605pub(crate) fn format_match_pattern(p: &crate::ast::MatchPattern) -> String {
1606    use crate::ast::{MatchArrayElem, MatchHashPair, MatchPattern};
1607    match p {
1608        MatchPattern::Any => "_".to_string(),
1609        MatchPattern::Regex { pattern, flags } => {
1610            if flags.is_empty() {
1611                format!("/{}/", pattern)
1612            } else {
1613                format!("/{}/{}/", pattern, flags)
1614            }
1615        }
1616        MatchPattern::Value(e) => format_expr(e),
1617        MatchPattern::Array(elems) => {
1618            let inner = elems
1619                .iter()
1620                .map(|x| match x {
1621                    MatchArrayElem::Expr(e) => format_expr(e),
1622                    MatchArrayElem::CaptureScalar(name) => format!("${}", name),
1623                    MatchArrayElem::Rest => "*".to_string(),
1624                    MatchArrayElem::RestBind(name) => format!("@{}", name),
1625                })
1626                .collect::<Vec<_>>()
1627                .join(", ");
1628            format!("[{}]", inner)
1629        }
1630        MatchPattern::Hash(pairs) => {
1631            let inner = pairs
1632                .iter()
1633                .map(|pair| match pair {
1634                    MatchHashPair::KeyOnly { key } => {
1635                        format!("{} => _", format_expr(key))
1636                    }
1637                    MatchHashPair::Capture { key, name } => {
1638                        format!("{} => ${}", format_expr(key), name)
1639                    }
1640                })
1641                .collect::<Vec<_>>()
1642                .join(", ");
1643            format!("{{ {} }}", inner)
1644        }
1645        MatchPattern::OptionSome(name) => format!("Some({})", name),
1646    }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651    use super::*;
1652    use crate::parse;
1653
1654    #[test]
1655    fn format_program_expression_statement_includes_binop() {
1656        let p = parse("2 + 3").expect("parse");
1657        let out = format_program(&p);
1658        assert!(
1659            out.contains("2") && out.contains("3") && out.contains("+"),
1660            "unexpected format: {out}"
1661        );
1662    }
1663
1664    #[test]
1665    fn format_program_if_block() {
1666        let p = parse("if (1) { 2; }").expect("parse");
1667        let out = format_program(&p);
1668        assert!(out.contains("if") && out.contains('1'));
1669    }
1670
1671    #[test]
1672    fn format_program_package_line() {
1673        let p = parse("package Foo::Bar").expect("parse");
1674        let out = format_program(&p);
1675        assert!(out.contains("package Foo::Bar"));
1676    }
1677
1678    #[test]
1679    fn format_program_string_literal_escapes_quote() {
1680        let p = parse(r#"my $s = "a\"b""#).expect("parse");
1681        let out = format_program(&p);
1682        assert!(out.contains("\\\""), "expected escaped quote in: {out}");
1683    }
1684}