Skip to main content

stryke/
convert.rs

1//! Convert standard Perl source to idiomatic stryke syntax.
2//!
3//! Transformations applied:
4//! - Nested function/builtin calls → `|>` pipe-forward chains
5//! - `map { BLOCK } LIST` → `LIST |> map { BLOCK }`
6//! - `grep { BLOCK } LIST` → `LIST |> grep { BLOCK }`
7//! - `sort [{ CMP }] LIST` → `LIST |> sort [{ CMP }]`
8//! - `join(SEP, LIST)` → `LIST |> join SEP`
9//! - No trailing semicolons (newline terminates statements)
10//! - 4-space indentation for block bodies
11//! - `#!/usr/bin/env stryke` shebang prepended
12//! - Pipe RHS uses bare args: `|> binmode ":utf8"` not `|> binmode(":utf8")`
13
14#![allow(unused_variables)]
15
16use crate::ast::*;
17use crate::fmt;
18use std::cell::RefCell;
19
20const INDENT: &str = "    ";
21
22thread_local! {
23    static OUTPUT_DELIM: RefCell<Option<char>> = const { RefCell::new(None) };
24}
25
26/// Options for the convert module.
27#[derive(Debug, Clone, Default)]
28pub struct ConvertOptions {
29    /// Custom delimiter for s///, tr///, m// patterns (e.g., '|', '#', '!').
30    pub output_delim: Option<char>,
31}
32
33fn get_output_delim() -> Option<char> {
34    OUTPUT_DELIM.with(|d| *d.borrow())
35}
36
37fn set_output_delim(delim: Option<char>) {
38    OUTPUT_DELIM.with(|d| *d.borrow_mut() = delim);
39}
40
41/// Choose the output delimiter: custom if set, else the original from the AST.
42fn choose_delim(original: char) -> char {
43    get_output_delim().unwrap_or(original)
44}
45
46// ── Public API ──────────────────────────────────────────────────────────────
47
48/// Convert a parsed Perl program to stryke syntax.
49pub fn convert_program(p: &Program) -> String {
50    convert_program_with_options(p, &ConvertOptions::default())
51}
52
53/// Convert a parsed Perl program to stryke syntax with custom options.
54pub fn convert_program_with_options(p: &Program, opts: &ConvertOptions) -> String {
55    set_output_delim(opts.output_delim);
56    let body = convert_statements(&p.statements, 0);
57    set_output_delim(None);
58    format!("#!/usr/bin/env stryke\n{}", body)
59}
60
61// ── Block / Statement ───────────────────────────────────────────────────────
62
63fn convert_block(b: &Block, depth: usize) -> String {
64    convert_statements(b, depth)
65}
66
67/// Convert a slice of statements, merging bare say/print with following string literals.
68fn convert_statements(stmts: &[Statement], depth: usize) -> String {
69    let mut out = Vec::new();
70    let mut i = 0;
71    while i < stmts.len() {
72        // Check for bare say/print followed by string literal
73        if let Some(merged) = try_merge_say_print(&stmts[i..], depth) {
74            out.push(merged);
75            i += 2; // skip both statements
76        } else {
77            out.push(convert_statement(&stmts[i], depth));
78            i += 1;
79        }
80    }
81    out.join("\n")
82}
83
84/// Try to merge a bare say/print statement with a following string literal.
85/// Returns Some(merged_string) if merge happened, None otherwise.
86fn try_merge_say_print(stmts: &[Statement], depth: usize) -> Option<String> {
87    if stmts.len() < 2 {
88        return None;
89    }
90    let pfx = indent(depth);
91
92    // First statement must be bare say or print (no args, no handle)
93    let (is_say, handle) = match &stmts[0].kind {
94        StmtKind::Expression(e) => match &e.kind {
95            ExprKind::Say { handle, args } if args.is_empty() => (true, handle),
96            ExprKind::Print { handle, args } if args.is_empty() => (false, handle),
97            _ => return None,
98        },
99        _ => return None,
100    };
101
102    // No handle allowed for merge
103    if handle.is_some() {
104        return None;
105    }
106
107    // Second statement must be a bare string expression
108    let str_expr = match &stmts[1].kind {
109        StmtKind::Expression(e) => e,
110        _ => return None,
111    };
112
113    // Format as: p "string" or print "string"
114    let cmd = if is_say { "p" } else { "print" };
115    let arg = convert_expr_top(str_expr);
116    Some(format!("{}{} {}", pfx, cmd, arg))
117}
118
119/// Indent a string by `depth` levels of 4 spaces.
120fn indent(depth: usize) -> String {
121    INDENT.repeat(depth)
122}
123
124fn convert_statement(s: &Statement, depth: usize) -> String {
125    let lab = s
126        .label
127        .as_ref()
128        .map(|l| format!("{}: ", l))
129        .unwrap_or_default();
130    let pfx = indent(depth);
131    let body = match &s.kind {
132        StmtKind::Expression(e) => convert_expr_top(e),
133        StmtKind::If {
134            condition,
135            body,
136            elsifs,
137            else_block,
138        } => {
139            let mut s = format!(
140                "if ({}) {{\n{}\n{}}}",
141                convert_expr_top(condition),
142                convert_block(body, depth + 1),
143                pfx
144            );
145            for (c, b) in elsifs {
146                s.push_str(&format!(
147                    " elsif ({}) {{\n{}\n{}}}",
148                    convert_expr_top(c),
149                    convert_block(b, depth + 1),
150                    pfx
151                ));
152            }
153            if let Some(eb) = else_block {
154                s.push_str(&format!(
155                    " else {{\n{}\n{}}}",
156                    convert_block(eb, depth + 1),
157                    pfx
158                ));
159            }
160            s
161        }
162        StmtKind::Unless {
163            condition,
164            body,
165            else_block,
166        } => {
167            let mut s = format!(
168                "unless ({}) {{\n{}\n{}}}",
169                convert_expr_top(condition),
170                convert_block(body, depth + 1),
171                pfx
172            );
173            if let Some(eb) = else_block {
174                s.push_str(&format!(
175                    " else {{\n{}\n{}}}",
176                    convert_block(eb, depth + 1),
177                    pfx
178                ));
179            }
180            s
181        }
182        StmtKind::While {
183            condition,
184            body,
185            label,
186            continue_block,
187        } => {
188            let lb = label
189                .as_ref()
190                .map(|l| format!("{}: ", l))
191                .unwrap_or_default();
192            let mut s = format!(
193                "{}while ({}) {{\n{}\n{}}}",
194                lb,
195                convert_expr_top(condition),
196                convert_block(body, depth + 1),
197                pfx
198            );
199            if let Some(cb) = continue_block {
200                s.push_str(&format!(
201                    " continue {{\n{}\n{}}}",
202                    convert_block(cb, depth + 1),
203                    pfx
204                ));
205            }
206            s
207        }
208        StmtKind::Until {
209            condition,
210            body,
211            label,
212            continue_block,
213        } => {
214            let lb = label
215                .as_ref()
216                .map(|l| format!("{}: ", l))
217                .unwrap_or_default();
218            let mut s = format!(
219                "{}until ({}) {{\n{}\n{}}}",
220                lb,
221                convert_expr_top(condition),
222                convert_block(body, depth + 1),
223                pfx
224            );
225            if let Some(cb) = continue_block {
226                s.push_str(&format!(
227                    " continue {{\n{}\n{}}}",
228                    convert_block(cb, depth + 1),
229                    pfx
230                ));
231            }
232            s
233        }
234        StmtKind::DoWhile { body, condition } => {
235            format!(
236                "do {{\n{}\n{}}} while ({})",
237                convert_block(body, depth + 1),
238                pfx,
239                convert_expr_top(condition)
240            )
241        }
242        StmtKind::For {
243            init,
244            condition,
245            step,
246            body,
247            label,
248            continue_block,
249        } => {
250            let lb = label
251                .as_ref()
252                .map(|l| format!("{}: ", l))
253                .unwrap_or_default();
254            let ini = init
255                .as_ref()
256                .map(|s| convert_statement_body(s))
257                .unwrap_or_default();
258            let cond = condition.as_ref().map(convert_expr).unwrap_or_default();
259            let st = step.as_ref().map(convert_expr).unwrap_or_default();
260            let mut s = format!(
261                "{}for ({}; {}; {}) {{\n{}\n{}}}",
262                lb,
263                ini,
264                cond,
265                st,
266                convert_block(body, depth + 1),
267                pfx
268            );
269            if let Some(cb) = continue_block {
270                s.push_str(&format!(
271                    " continue {{\n{}\n{}}}",
272                    convert_block(cb, depth + 1),
273                    pfx
274                ));
275            }
276            s
277        }
278        StmtKind::Foreach {
279            var,
280            list,
281            body,
282            label,
283            continue_block,
284        } => {
285            let lb = label
286                .as_ref()
287                .map(|l| format!("{}: ", l))
288                .unwrap_or_default();
289            let mut s = format!(
290                "{}for ${} ({}) {{\n{}\n{}}}",
291                lb,
292                var,
293                convert_expr(list),
294                convert_block(body, depth + 1),
295                pfx
296            );
297            if let Some(cb) = continue_block {
298                s.push_str(&format!(
299                    " continue {{\n{}\n{}}}",
300                    convert_block(cb, depth + 1),
301                    pfx
302                ));
303            }
304            s
305        }
306        StmtKind::SubDecl {
307            name,
308            params,
309            body,
310            prototype,
311        } => {
312            let sig = if !params.is_empty() {
313                format!(
314                    " ({})",
315                    params
316                        .iter()
317                        .map(fmt::format_sub_sig_param)
318                        .collect::<Vec<_>>()
319                        .join(", ")
320                )
321            } else {
322                prototype
323                    .as_ref()
324                    .map(|p| format!(" ({})", p))
325                    .unwrap_or_default()
326            };
327            format!(
328                "fn {}{} {{\n{}\n{}}}",
329                name,
330                sig,
331                convert_block(body, depth + 1),
332                pfx
333            )
334        }
335        StmtKind::Package { name } => format!("package {}", name),
336        StmtKind::UsePerlVersion { version } => {
337            if version.fract() == 0.0 && *version >= 0.0 {
338                format!("use {}", *version as i64)
339            } else {
340                format!("use {}", version)
341            }
342        }
343        StmtKind::Use { module, imports } => {
344            if imports.is_empty() {
345                format!("use {}", module)
346            } else {
347                format!("use {} {}", module, convert_expr_list(imports))
348            }
349        }
350        StmtKind::UseOverload { pairs } => {
351            let inner = pairs
352                .iter()
353                .map(|(k, v)| {
354                    format!(
355                        "'{}' => '{}'",
356                        k.replace('\'', "\\'"),
357                        v.replace('\'', "\\'")
358                    )
359                })
360                .collect::<Vec<_>>()
361                .join(", ");
362            format!("use overload {inner}")
363        }
364        StmtKind::No { module, imports } => {
365            if imports.is_empty() {
366                format!("no {}", module)
367            } else {
368                format!("no {} {}", module, convert_expr_list(imports))
369            }
370        }
371        StmtKind::Return(e) => e
372            .as_ref()
373            .map(|x| format!("return {}", convert_expr_top(x)))
374            .unwrap_or_else(|| "return".to_string()),
375        StmtKind::Last(l) => l
376            .as_ref()
377            .map(|x| format!("last {}", x))
378            .unwrap_or_else(|| "last".to_string()),
379        StmtKind::Next(l) => l
380            .as_ref()
381            .map(|x| format!("next {}", x))
382            .unwrap_or_else(|| "next".to_string()),
383        StmtKind::Redo(l) => l
384            .as_ref()
385            .map(|x| format!("redo {}", x))
386            .unwrap_or_else(|| "redo".to_string()),
387        StmtKind::My(decls) => format!("my {}", convert_var_decls(decls)),
388        StmtKind::Our(decls) => format!("our {}", convert_var_decls(decls)),
389        StmtKind::Local(decls) => format!("local {}", convert_var_decls(decls)),
390        StmtKind::State(decls) => format!("state {}", convert_var_decls(decls)),
391        StmtKind::LocalExpr {
392            target,
393            initializer,
394        } => {
395            let mut s = format!("local {}", convert_expr(target));
396            if let Some(init) = initializer {
397                s.push_str(&format!(" = {}", convert_expr_top(init)));
398            }
399            s
400        }
401        StmtKind::MySync(decls) => format!("mysync {}", convert_var_decls(decls)),
402        StmtKind::OurSync(decls) => format!("oursync {}", convert_var_decls(decls)),
403        StmtKind::StmtGroup(b) => convert_block(b, depth),
404        StmtKind::Block(b) => format!("{{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
405        StmtKind::Begin(b) => format!("BEGIN {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
406        StmtKind::UnitCheck(b) => {
407            format!("UNITCHECK {{\n{}\n{}}}", convert_block(b, depth + 1), pfx)
408        }
409        StmtKind::Check(b) => format!("CHECK {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
410        StmtKind::Init(b) => format!("INIT {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
411        StmtKind::End(b) => format!("END {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
412        StmtKind::Empty => String::new(),
413        StmtKind::Goto { target } => format!("goto {}", convert_expr(target)),
414        StmtKind::Continue(b) => format!("continue {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
415        StmtKind::StructDecl { def } => {
416            let fields = def
417                .fields
418                .iter()
419                .map(|f| format!("{} => {}", f.name, f.ty.display_name()))
420                .collect::<Vec<_>>()
421                .join(", ");
422            format!("struct {} {{ {} }}", def.name, fields)
423        }
424        StmtKind::EnumDecl { def } => {
425            let variants = def
426                .variants
427                .iter()
428                .map(|v| {
429                    if let Some(ty) = &v.ty {
430                        format!("{} => {}", v.name, ty.display_name())
431                    } else {
432                        v.name.clone()
433                    }
434                })
435                .collect::<Vec<_>>()
436                .join(", ");
437            format!("enum {} {{ {} }}", def.name, variants)
438        }
439        StmtKind::ClassDecl { def } => {
440            let prefix = if def.is_abstract {
441                "abstract "
442            } else if def.is_final {
443                "final "
444            } else {
445                ""
446            };
447            let mut parts = vec![format!("{}class {}", prefix, def.name)];
448            if !def.extends.is_empty() {
449                parts.push(format!("extends {}", def.extends.join(", ")));
450            }
451            if !def.implements.is_empty() {
452                parts.push(format!("impl {}", def.implements.join(", ")));
453            }
454            let fields = def
455                .fields
456                .iter()
457                .map(|f| {
458                    let vis = match f.visibility {
459                        crate::ast::Visibility::Private => "priv ",
460                        crate::ast::Visibility::Protected => "prot ",
461                        crate::ast::Visibility::Public => "",
462                    };
463                    format!("{}{}: {}", vis, f.name, f.ty.display_name())
464                })
465                .collect::<Vec<_>>()
466                .join("; ");
467            format!("{} {{ {} }}", parts.join(" "), fields)
468        }
469        StmtKind::TraitDecl { def } => {
470            let methods = def
471                .methods
472                .iter()
473                .map(|m| format!("fn {}", m.name))
474                .collect::<Vec<_>>()
475                .join("; ");
476            format!("trait {} {{ {} }}", def.name, methods)
477        }
478        StmtKind::EvalTimeout { timeout, body } => {
479            format!(
480                "eval_timeout {} {{\n{}\n{}}}",
481                convert_expr(timeout),
482                convert_block(body, depth + 1),
483                pfx
484            )
485        }
486        StmtKind::TryCatch {
487            try_block,
488            catch_var,
489            catch_block,
490            finally_block,
491        } => {
492            let fin = finally_block
493                .as_ref()
494                .map(|b| {
495                    format!(
496                        "\n{}finally {{\n{}\n{}}}",
497                        pfx,
498                        convert_block(b, depth + 1),
499                        pfx
500                    )
501                })
502                .unwrap_or_default();
503            format!(
504                "try {{\n{}\n{}}} catch (${}) {{\n{}\n{}}}{}",
505                convert_block(try_block, depth + 1),
506                pfx,
507                catch_var,
508                convert_block(catch_block, depth + 1),
509                pfx,
510                fin
511            )
512        }
513        StmtKind::Given { topic, body } => {
514            format!(
515                "given ({}) {{\n{}\n{}}}",
516                convert_expr(topic),
517                convert_block(body, depth + 1),
518                pfx
519            )
520        }
521        StmtKind::When { cond, body } => {
522            format!(
523                "when ({}) {{\n{}\n{}}}",
524                convert_expr(cond),
525                convert_block(body, depth + 1),
526                pfx
527            )
528        }
529        StmtKind::DefaultCase { body } => {
530            format!("default {{\n{}\n{}}}", convert_block(body, depth + 1), pfx)
531        }
532        StmtKind::FormatDecl { name, lines } => {
533            let mut s = format!("format {} =\n", name);
534            for ln in lines {
535                s.push_str(ln);
536                s.push('\n');
537            }
538            s.push('.');
539            s
540        }
541        StmtKind::AdviceDecl {
542            kind,
543            pattern,
544            body,
545        } => {
546            let kw = match kind {
547                crate::ast::AdviceKind::Before => "before",
548                crate::ast::AdviceKind::After => "after",
549                crate::ast::AdviceKind::Around => "around",
550            };
551            format!(
552                "{} \"{}\" {{\n{}\n{}}}",
553                kw,
554                pattern,
555                convert_block(body, depth + 1),
556                pfx
557            )
558        }
559        StmtKind::Tie {
560            target,
561            class,
562            args,
563        } => {
564            let target_s = match target {
565                crate::ast::TieTarget::Hash(h) => format!("%{}", h),
566                crate::ast::TieTarget::Array(a) => format!("@{}", a),
567                crate::ast::TieTarget::Scalar(s) => format!("${}", s),
568            };
569            let mut s = format!("tie {} {}", target_s, convert_expr(class));
570            for a in args {
571                s.push_str(&format!(", {}", convert_expr(a)));
572            }
573            s
574        }
575    };
576    format!("{}{}{}", pfx, lab, body)
577}
578
579/// Convert a statement body without indentation prefix (for C-style for init).
580fn convert_statement_body(s: &Statement) -> String {
581    let lab = s
582        .label
583        .as_ref()
584        .map(|l| format!("{}: ", l))
585        .unwrap_or_default();
586    let body = match &s.kind {
587        StmtKind::Expression(e) => convert_expr_top(e),
588        StmtKind::My(decls) => format!("my {}", convert_var_decls(decls)),
589        _ => convert_statement(s, 0).trim().to_string(),
590    };
591    format!("{}{}", lab, body)
592}
593
594// ── Variable declarations ───────────────────────────────────────────────────
595
596fn convert_var_decls(decls: &[VarDecl]) -> String {
597    decls
598        .iter()
599        .map(|d| {
600            let sig = match d.sigil {
601                Sigil::Scalar => "$",
602                Sigil::Array => "@",
603                Sigil::Hash => "%",
604                Sigil::Typeglob => "*",
605            };
606            let mut s = format!("{}{}", sig, d.name);
607            if let Some(ref t) = d.type_annotation {
608                s.push_str(&format!(" : {}", t.display_name()));
609            }
610            if let Some(ref init) = d.initializer {
611                s.push_str(&format!(" = {}", convert_expr_top(init)));
612            }
613            s
614        })
615        .collect::<Vec<_>>()
616        .join(", ")
617}
618
619// ── Expression conversion ───────────────────────────────────────────────────
620
621fn convert_expr_list(es: &[Expr]) -> String {
622    es.iter().map(convert_expr).collect::<Vec<_>>().join(", ")
623}
624
625/// Format a string part for converted output.
626/// Uses simple `$name` when possible, `${name}` only when needed.
627fn convert_string_part(p: &StringPart) -> String {
628    match p {
629        StringPart::Literal(s) => fmt::escape_interpolated_literal(s),
630        StringPart::ScalarVar(n) => {
631            // Use ${} only if name has special chars or would be ambiguous
632            if needs_braces(n) {
633                format!("${{{}}}", n)
634            } else {
635                format!("${}", n)
636            }
637        }
638        StringPart::ArrayVar(n) => {
639            if needs_braces(n) {
640                format!("@{{{}}}", n)
641            } else {
642                format!("@{}", n)
643            }
644        }
645        StringPart::Expr(e) => fmt::format_expr(e),
646    }
647}
648
649/// Check if a variable name needs braces in interpolation.
650fn needs_braces(name: &str) -> bool {
651    // Empty or starts with digit needs braces
652    if name.is_empty() || name.chars().next().is_some_and(|c| c.is_ascii_digit()) {
653        return true;
654    }
655    // Contains non-identifier chars
656    !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
657}
658
659/// Convert an expression at statement level or assignment RHS — pipe chains
660/// are emitted without outer parentheses.
661fn convert_expr_top(e: &Expr) -> String {
662    convert_expr_impl(e, true)
663}
664
665/// Convert an expression in a sub-expression context — pipe chains are wrapped
666/// in parentheses to preserve precedence.
667fn convert_expr(e: &Expr) -> String {
668    convert_expr_impl(e, false)
669}
670
671fn convert_expr_impl(e: &Expr, top: bool) -> String {
672    let mut segments: Vec<String> = Vec::new();
673    let source = extract_pipe_source(e, &mut segments);
674    if !segments.is_empty() {
675        segments.reverse();
676        // 1-2 stages: direct call syntax (e.g., `print "$x"`, `uc lc $x`)
677        // 3+ stages: thread macro (e.g., `t $x lc uc print`)
678        if segments.len() <= 2 {
679            let result = format!("{} {}", segments.join(" "), source);
680            if !top {
681                return format!("({})", result);
682            }
683            return result;
684        }
685        // 3+ stages: use thread macro
686        let stages = segments.join(" ");
687        // Strip outer parens from source if it's a parenthesized list/thread
688        let source = if source.starts_with("(t ") || source.starts_with("((") {
689            source[1..source.len() - 1].to_string()
690        } else {
691            source
692        };
693        let result = format!("t {} {}", source, stages);
694        if !top {
695            return format!("({})", result);
696        }
697        return result;
698    }
699    // No pipe chain — format with recursive sub-expression conversion.
700    convert_expr_direct(e, top)
701}
702
703// ── Pipe chain extraction ───────────────────────────────────────────────────
704//
705// Walks the expression tree from the outermost call inward, peeling off
706// each pipeable layer as a segment string.  Segments are pushed in
707// outer-to-inner order; the caller reverses before joining with `|>`.
708
709fn extract_pipe_source(e: &Expr, segments: &mut Vec<String>) -> String {
710    match &e.kind {
711        // ── Unary builtins ──────────────────────────────────────────────
712        ExprKind::Uc(inner) => {
713            segments.push("uc".into());
714            extract_pipe_source(inner, segments)
715        }
716        ExprKind::Lc(inner) => {
717            segments.push("lc".into());
718            extract_pipe_source(inner, segments)
719        }
720        ExprKind::Ucfirst(inner) => {
721            segments.push("ucfirst".into());
722            extract_pipe_source(inner, segments)
723        }
724        ExprKind::Lcfirst(inner) => {
725            segments.push("lcfirst".into());
726            extract_pipe_source(inner, segments)
727        }
728        ExprKind::Fc(inner) => {
729            segments.push("fc".into());
730            extract_pipe_source(inner, segments)
731        }
732        ExprKind::Chomp(inner) => {
733            segments.push("chomp".into());
734            extract_pipe_source(inner, segments)
735        }
736        ExprKind::Chop(inner) => {
737            segments.push("chop".into());
738            extract_pipe_source(inner, segments)
739        }
740        ExprKind::Length(inner) => {
741            segments.push("length".into());
742            extract_pipe_source(inner, segments)
743        }
744        ExprKind::Abs(inner) => {
745            segments.push("abs".into());
746            extract_pipe_source(inner, segments)
747        }
748        ExprKind::Int(inner) => {
749            segments.push("int".into());
750            extract_pipe_source(inner, segments)
751        }
752        ExprKind::Sqrt(inner) => {
753            segments.push("sqrt".into());
754            extract_pipe_source(inner, segments)
755        }
756        ExprKind::Sin(inner) => {
757            segments.push("sin".into());
758            extract_pipe_source(inner, segments)
759        }
760        ExprKind::Cos(inner) => {
761            segments.push("cos".into());
762            extract_pipe_source(inner, segments)
763        }
764        ExprKind::Exp(inner) => {
765            segments.push("exp".into());
766            extract_pipe_source(inner, segments)
767        }
768        ExprKind::Log(inner) => {
769            segments.push("log".into());
770            extract_pipe_source(inner, segments)
771        }
772        ExprKind::Hex(inner) => {
773            segments.push("hex".into());
774            extract_pipe_source(inner, segments)
775        }
776        ExprKind::Oct(inner) => {
777            segments.push("oct".into());
778            extract_pipe_source(inner, segments)
779        }
780        ExprKind::Chr(inner) => {
781            segments.push("chr".into());
782            extract_pipe_source(inner, segments)
783        }
784        ExprKind::Ord(inner) => {
785            segments.push("ord".into());
786            extract_pipe_source(inner, segments)
787        }
788        ExprKind::Defined(inner) => {
789            segments.push("defined".into());
790            extract_pipe_source(inner, segments)
791        }
792        ExprKind::Ref(inner) => {
793            segments.push("ref".into());
794            extract_pipe_source(inner, segments)
795        }
796        ExprKind::ScalarContext(inner) => {
797            segments.push("scalar".into());
798            extract_pipe_source(inner, segments)
799        }
800        ExprKind::Keys(inner) => {
801            segments.push("keys".into());
802            extract_pipe_source(inner, segments)
803        }
804        ExprKind::Values(inner) => {
805            segments.push("values".into());
806            extract_pipe_source(inner, segments)
807        }
808        ExprKind::Each(inner) => {
809            segments.push("each".into());
810            extract_pipe_source(inner, segments)
811        }
812        ExprKind::Pop(inner) => {
813            segments.push("pop".into());
814            extract_pipe_source(inner, segments)
815        }
816        ExprKind::Shift(inner) => {
817            segments.push("shift".into());
818            extract_pipe_source(inner, segments)
819        }
820        ExprKind::ReverseExpr(inner) => {
821            segments.push("reverse".into());
822            extract_pipe_source(inner, segments)
823        }
824        ExprKind::Slurp(inner) => {
825            segments.push("slurp".into());
826            extract_pipe_source(inner, segments)
827        }
828        ExprKind::Chdir(inner) => {
829            segments.push("chdir".into());
830            extract_pipe_source(inner, segments)
831        }
832        ExprKind::Stat(inner) => {
833            segments.push("stat".into());
834            extract_pipe_source(inner, segments)
835        }
836        ExprKind::Lstat(inner) => {
837            segments.push("lstat".into());
838            extract_pipe_source(inner, segments)
839        }
840        ExprKind::Readlink(inner) => {
841            segments.push("readlink".into());
842            extract_pipe_source(inner, segments)
843        }
844        ExprKind::Study(inner) => {
845            segments.push("study".into());
846            extract_pipe_source(inner, segments)
847        }
848        ExprKind::Close(inner) => {
849            segments.push("close".into());
850            extract_pipe_source(inner, segments)
851        }
852        ExprKind::Readdir(inner) => {
853            segments.push("readdir".into());
854            extract_pipe_source(inner, segments)
855        }
856        ExprKind::Eval(inner) => {
857            segments.push("eval".into());
858            extract_pipe_source(inner, segments)
859        }
860        ExprKind::Require(inner) => {
861            segments.push("require".into());
862            extract_pipe_source(inner, segments)
863        }
864
865        // ── List-taking higher-order builtins ────────────────────────────
866        ExprKind::MapExpr {
867            block,
868            list,
869            flatten_array_refs,
870            stream,
871        } => {
872            let kw = match (*flatten_array_refs, *stream) {
873                (true, true) => "flat_maps",
874                (true, false) => "flat_map",
875                (false, true) => "maps",
876                (false, false) => "map",
877            };
878            segments.push(format!("{} {{\n{}\n}}", kw, convert_block(block, 0)));
879            extract_pipe_source(list, segments)
880        }
881        ExprKind::MapExprComma {
882            expr,
883            list,
884            flatten_array_refs,
885            stream,
886        } => {
887            let kw = match (*flatten_array_refs, *stream) {
888                (true, true) => "flat_maps",
889                (true, false) => "flat_map",
890                (false, true) => "maps",
891                (false, false) => "map",
892            };
893            // Convert comma form to block form for cleaner pipe syntax.
894            segments.push(format!("{} {{ {} }}", kw, convert_expr_top(expr)));
895            extract_pipe_source(list, segments)
896        }
897        ExprKind::GrepExpr {
898            block,
899            list,
900            keyword,
901        } => {
902            segments.push(format!(
903                "{} {{\n{}\n}}",
904                keyword.as_str(),
905                convert_block(block, 0)
906            ));
907            extract_pipe_source(list, segments)
908        }
909        ExprKind::GrepExprComma {
910            expr,
911            list,
912            keyword,
913        } => {
914            segments.push(format!(
915                "{} {{ {} }}",
916                keyword.as_str(),
917                convert_expr_top(expr)
918            ));
919            extract_pipe_source(list, segments)
920        }
921        ExprKind::SortExpr { cmp, list } => {
922            let seg = match cmp {
923                Some(SortComparator::Block(b)) => {
924                    format!("sort {{\n{}\n}}", convert_block(b, 0))
925                }
926                Some(SortComparator::Code(e)) => {
927                    format!("sort {}", convert_expr(e))
928                }
929                None => "sort".to_string(),
930            };
931            segments.push(seg);
932            extract_pipe_source(list, segments)
933        }
934        ExprKind::JoinExpr { separator, list } => {
935            segments.push(format!("join {}", convert_expr(separator)));
936            extract_pipe_source(list, segments)
937        }
938        ExprKind::ReduceExpr { block, list } => {
939            segments.push(format!("reduce {{\n{}\n}}", convert_block(block, 0)));
940            extract_pipe_source(list, segments)
941        }
942        ExprKind::ForEachExpr { block, list } => {
943            segments.push(format!("fore {{\n{}\n}}", convert_block(block, 0)));
944            extract_pipe_source(list, segments)
945        }
946
947        // ── Parallel higher-order builtins ───────────────────────────────
948        ExprKind::PMapExpr {
949            block,
950            list,
951            progress,
952            flat_outputs,
953            on_cluster,
954            stream: _,
955        } if progress.is_none() && on_cluster.is_none() => {
956            let kw = if *flat_outputs { "pflat_map" } else { "pmap" };
957            segments.push(format!("{} {{\n{}\n}}", kw, convert_block(block, 0)));
958            extract_pipe_source(list, segments)
959        }
960        ExprKind::PGrepExpr {
961            block,
962            list,
963            progress,
964            stream: _,
965        } if progress.is_none() => {
966            segments.push(format!("pgrep {{\n{}\n}}", convert_block(block, 0)));
967            extract_pipe_source(list, segments)
968        }
969        ExprKind::PSortExpr {
970            cmp,
971            list,
972            progress,
973        } if progress.is_none() => {
974            let seg = match cmp {
975                Some(b) => format!("psort {{\n{}\n}}", convert_block(b, 0)),
976                None => "psort".to_string(),
977            };
978            segments.push(seg);
979            extract_pipe_source(list, segments)
980        }
981
982        // ── Print / say with single arg → pipe ───────────────────────────
983        // say adds newline → p; print does not → print
984        ExprKind::Say { handle: None, args } if args.len() == 1 => {
985            segments.push("p".into());
986            extract_pipe_source(&args[0], segments)
987        }
988        ExprKind::Print { handle: None, args } if args.len() == 1 => {
989            segments.push("print".into());
990            extract_pipe_source(&args[0], segments)
991        }
992
993        // ── Generic function calls ───────────────────────────────────────
994        ExprKind::FuncCall { name, args } if !args.is_empty() => {
995            let seg = if args.len() == 1 {
996                name.clone()
997            } else {
998                let rest = args[1..]
999                    .iter()
1000                    .map(convert_expr)
1001                    .collect::<Vec<_>>()
1002                    .join(", ");
1003                format!("{} {}", name, rest)
1004            };
1005            segments.push(seg);
1006            extract_pipe_source(&args[0], segments)
1007        }
1008
1009        // ── Substitution with /r flag (value-returning) ──────────────────
1010        ExprKind::Substitution {
1011            expr,
1012            pattern,
1013            replacement,
1014            flags,
1015            delim,
1016        } if flags.contains('r') => {
1017            // `$str =~ s/old/new/r` → `$str |> s/old/new/r`
1018            // In pipe context the parser auto-injects `r`, but keeping it
1019            // is harmless and explicit.
1020            let d = choose_delim(*delim);
1021            segments.push(format!(
1022                "s{}{}{}{}{}{}",
1023                d,
1024                fmt::escape_regex_part(pattern),
1025                d,
1026                fmt::escape_regex_part(replacement),
1027                d,
1028                flags
1029            ));
1030            extract_pipe_source(expr, segments)
1031        }
1032
1033        // ── Transliterate with /r flag ───────────────────────────────────
1034        ExprKind::Transliterate {
1035            expr,
1036            from,
1037            to,
1038            flags,
1039            delim,
1040        } if flags.contains('r') => {
1041            let d = choose_delim(*delim);
1042            segments.push(format!(
1043                "tr{}{}{}{}{}{}",
1044                d,
1045                fmt::escape_regex_part(from),
1046                d,
1047                fmt::escape_regex_part(to),
1048                d,
1049                flags
1050            ));
1051            extract_pipe_source(expr, segments)
1052        }
1053
1054        // ── Single-element list: unwrap and continue extraction ──────────
1055        ExprKind::List(elems) if elems.len() == 1 => extract_pipe_source(&elems[0], segments),
1056
1057        // ── Base case: not pipeable ──────────────────────────────────────
1058        _ => convert_expr_direct(e, false),
1059    }
1060}
1061
1062// ── Direct expression formatting (no pipe extraction) ───────────────────────
1063//
1064// Handles the common expression types with recursive `convert_expr` calls
1065// for sub-expressions.  Rare / complex variants delegate to `fmt::format_expr`.
1066
1067fn convert_expr_direct(e: &Expr, top: bool) -> String {
1068    match &e.kind {
1069        // ── Leaf / simple (delegate to fmt) ──────────────────────────────
1070        ExprKind::Integer(_)
1071        | ExprKind::Float(_)
1072        | ExprKind::String(_)
1073        | ExprKind::Bareword(_)
1074        | ExprKind::Regex(..)
1075        | ExprKind::QW(_)
1076        | ExprKind::Undef
1077        | ExprKind::MagicConst(_)
1078        | ExprKind::ScalarVar(_)
1079        | ExprKind::ArrayVar(_)
1080        | ExprKind::HashVar(_)
1081        | ExprKind::Typeglob(_)
1082        | ExprKind::Wantarray
1083        | ExprKind::SubroutineRef(_)
1084        | ExprKind::SubroutineCodeRef(_) => fmt::format_expr(e),
1085
1086        // ── Interpolated strings — parts may embed expressions ───────────
1087        ExprKind::InterpolatedString(parts) => {
1088            format!(
1089                "\"{}\"",
1090                parts.iter().map(convert_string_part).collect::<String>()
1091            )
1092        }
1093
1094        // ── Binary operations ────────────────────────────────────────────
1095        ExprKind::BinOp { left, op, right } => {
1096            format!(
1097                "{} {} {}",
1098                convert_expr(left),
1099                fmt::format_binop(*op),
1100                convert_expr(right)
1101            )
1102        }
1103
1104        // ── Unary / postfix ──────────────────────────────────────────────
1105        ExprKind::UnaryOp { op, expr } => {
1106            format!("{}{}", fmt::format_unary(*op), convert_expr(expr))
1107        }
1108        ExprKind::PostfixOp { expr, op } => {
1109            format!("{}{}", convert_expr(expr), fmt::format_postfix(*op))
1110        }
1111
1112        // ── Assignment ───────────────────────────────────────────────────
1113        ExprKind::Assign { target, value } => {
1114            format!("{} = {}", convert_expr(target), convert_expr_top(value))
1115        }
1116        ExprKind::CompoundAssign { target, op, value } => format!(
1117            "{} {}= {}",
1118            convert_expr(target),
1119            fmt::format_binop(*op),
1120            convert_expr_top(value)
1121        ),
1122
1123        // ── Ternary ──────────────────────────────────────────────────────
1124        ExprKind::Ternary {
1125            condition,
1126            then_expr,
1127            else_expr,
1128        } => format!(
1129            "{} ? {} : {}",
1130            convert_expr(condition),
1131            convert_expr(then_expr),
1132            convert_expr(else_expr)
1133        ),
1134
1135        // ── Range / repeat ───────────────────────────────────────────────
1136        ExprKind::SliceRange { from, to, step } => {
1137            let f = from.as_ref().map(|e| convert_expr(e)).unwrap_or_default();
1138            let t = to.as_ref().map(|e| convert_expr(e)).unwrap_or_default();
1139            match step {
1140                Some(s) => format!("{}:{}:{}", f, t, convert_expr(s)),
1141                None => format!("{}:{}", f, t),
1142            }
1143        }
1144        ExprKind::Range {
1145            from,
1146            to,
1147            exclusive,
1148            step,
1149        } => {
1150            let op = if *exclusive { "..." } else { ".." };
1151            if let Some(s) = step {
1152                format!(
1153                    "{} {} {}:{}",
1154                    convert_expr(from),
1155                    op,
1156                    convert_expr(to),
1157                    convert_expr(s)
1158                )
1159            } else {
1160                format!("{} {} {}", convert_expr(from), op, convert_expr(to))
1161            }
1162        }
1163        ExprKind::Repeat {
1164            expr,
1165            count,
1166            list_repeat,
1167        } => {
1168            // Re-emit the parens for list-repeat so `(0) x 5` round-trips as
1169            // list-repeat rather than collapsing to scalar `0 x 5`.
1170            if *list_repeat && !matches!(expr.kind, ExprKind::List(_) | ExprKind::QW(_)) {
1171                format!("({}) x {}", convert_expr(expr), convert_expr(count))
1172            } else {
1173                format!("{} x {}", convert_expr(expr), convert_expr(count))
1174            }
1175        }
1176
1177        // ── Calls ────────────────────────────────────────────────────────
1178        ExprKind::FuncCall { name, args } => format!("{}({})", name, convert_expr_list(args)),
1179        ExprKind::MethodCall {
1180            object,
1181            method,
1182            args,
1183            super_call,
1184        } => {
1185            let m = if *super_call {
1186                format!("SUPER::{}", method)
1187            } else {
1188                method.clone()
1189            };
1190            format!(
1191                "{}->{}({})",
1192                convert_expr(object),
1193                m,
1194                convert_expr_list(args)
1195            )
1196        }
1197        ExprKind::IndirectCall {
1198            target,
1199            args,
1200            ampersand,
1201            pass_caller_arglist,
1202        } => {
1203            if *pass_caller_arglist && args.is_empty() {
1204                format!("&{}", convert_expr(target))
1205            } else {
1206                let inner = format!("{}({})", convert_expr(target), convert_expr_list(args));
1207                if *ampersand {
1208                    format!("&{}", inner)
1209                } else {
1210                    inner
1211                }
1212            }
1213        }
1214
1215        // ── Data structures ──────────────────────────────────────────────
1216        ExprKind::List(exprs) => format!("({})", convert_expr_list(exprs)),
1217        ExprKind::ArrayRef(elems) => format!("[{}]", convert_expr_list(elems)),
1218        ExprKind::HashRef(pairs) => {
1219            let inner = pairs
1220                .iter()
1221                .map(|(k, v)| format!("{} => {}", convert_expr(k), convert_expr(v)))
1222                .collect::<Vec<_>>()
1223                .join(", ");
1224            format!("{{{}}}", inner)
1225        }
1226        ExprKind::CodeRef { params, body } => {
1227            if params.is_empty() {
1228                format!("fn {{\n{}\n}}", convert_block(body, 0))
1229            } else {
1230                let sig = params
1231                    .iter()
1232                    .map(fmt::format_sub_sig_param)
1233                    .collect::<Vec<_>>()
1234                    .join(", ");
1235                format!("fn ({}) {{\n{}\n}}", sig, convert_block(body, 0))
1236            }
1237        }
1238
1239        // ── Access / deref ───────────────────────────────────────────────
1240        ExprKind::ArrayElement { array, index } => {
1241            format!("${}[{}]", array, convert_expr(index))
1242        }
1243        ExprKind::HashElement { hash, key } => {
1244            format!("${}{{{}}}", hash, convert_expr(key))
1245        }
1246        ExprKind::ScalarRef(inner) => format!("\\{}", convert_expr(inner)),
1247        ExprKind::ArrowDeref { expr, index, kind } => match kind {
1248            DerefKind::Array => {
1249                format!("({})->[{}]", convert_expr(expr), convert_expr(index))
1250            }
1251            DerefKind::Hash => {
1252                format!("({})->{{{}}}", convert_expr(expr), convert_expr(index))
1253            }
1254            DerefKind::Call => {
1255                format!("({})->({})", convert_expr(expr), convert_expr(index))
1256            }
1257        },
1258        ExprKind::Deref { expr, kind } => match kind {
1259            Sigil::Scalar => format!("${{{}}}", convert_expr(expr)),
1260            Sigil::Array => format!("@{{${}}}", convert_expr(expr)),
1261            Sigil::Hash => format!("%{{${}}}", convert_expr(expr)),
1262            Sigil::Typeglob => format!("*{{${}}}", convert_expr(expr)),
1263        },
1264
1265        // ── Print / say / die / warn ─────────────────────────────────────
1266        // print has no newline; say/p adds newline
1267        ExprKind::Print { handle, args } => {
1268            let h = handle
1269                .as_ref()
1270                .map(|h| format!("{} ", h))
1271                .unwrap_or_default();
1272            format!("print {}{}", h, convert_expr_list(args))
1273        }
1274        ExprKind::Say { handle, args } => {
1275            if let Some(h) = handle {
1276                format!("say {} {}", h, convert_expr_list(args))
1277            } else {
1278                format!("p {}", convert_expr_list(args))
1279            }
1280        }
1281        ExprKind::Printf { handle, args } => {
1282            let h = handle
1283                .as_ref()
1284                .map(|h| format!("{} ", h))
1285                .unwrap_or_default();
1286            format!("printf {}{}", h, convert_expr_list(args))
1287        }
1288        ExprKind::Die(args) => {
1289            if args.is_empty() {
1290                "die".to_string()
1291            } else {
1292                format!("die {}", convert_expr_list(args))
1293            }
1294        }
1295        ExprKind::Warn(args) => {
1296            if args.is_empty() {
1297                "warn".to_string()
1298            } else {
1299                format!("warn {}", convert_expr_list(args))
1300            }
1301        }
1302
1303        // ── Regex (non-piped) ────────────────────────────────────────────
1304        ExprKind::Match {
1305            expr,
1306            pattern,
1307            flags,
1308            delim,
1309            ..
1310        } => {
1311            let d = choose_delim(*delim);
1312            format!(
1313                "{} =~ {}{}{}{}",
1314                convert_expr(expr),
1315                d,
1316                fmt::escape_regex_part(pattern),
1317                d,
1318                flags
1319            )
1320        }
1321        ExprKind::Substitution {
1322            expr,
1323            pattern,
1324            replacement,
1325            flags,
1326            delim,
1327        } => {
1328            let d = choose_delim(*delim);
1329            format!(
1330                "{} =~ s{}{}{}{}{}{}",
1331                convert_expr(expr),
1332                d,
1333                fmt::escape_regex_part(pattern),
1334                d,
1335                fmt::escape_regex_part(replacement),
1336                d,
1337                flags
1338            )
1339        }
1340        ExprKind::Transliterate {
1341            expr,
1342            from,
1343            to,
1344            flags,
1345            delim,
1346        } => {
1347            let d = choose_delim(*delim);
1348            format!(
1349                "{} =~ tr{}{}{}{}{}{}",
1350                convert_expr(expr),
1351                d,
1352                fmt::escape_regex_part(from),
1353                d,
1354                fmt::escape_regex_part(to),
1355                d,
1356                flags
1357            )
1358        }
1359
1360        // ── Postfix modifiers ────────────────────────────────────────────
1361        ExprKind::PostfixIf { expr, condition } => {
1362            format!("{} if {}", convert_expr_top(expr), convert_expr(condition))
1363        }
1364        ExprKind::PostfixUnless { expr, condition } => {
1365            format!(
1366                "{} unless {}",
1367                convert_expr_top(expr),
1368                convert_expr(condition)
1369            )
1370        }
1371        ExprKind::PostfixWhile { expr, condition } => {
1372            format!(
1373                "{} while {}",
1374                convert_expr_top(expr),
1375                convert_expr(condition)
1376            )
1377        }
1378        ExprKind::PostfixUntil { expr, condition } => {
1379            format!(
1380                "{} until {}",
1381                convert_expr_top(expr),
1382                convert_expr(condition)
1383            )
1384        }
1385        ExprKind::PostfixForeach { expr, list } => {
1386            format!("{} for {}", convert_expr_top(expr), convert_expr(list))
1387        }
1388
1389        // ── Higher-order forms (fallback when not piped — e.g. empty list) ─
1390        ExprKind::MapExpr {
1391            block,
1392            list,
1393            flatten_array_refs,
1394            stream,
1395        } => {
1396            let kw = match (*flatten_array_refs, *stream) {
1397                (true, true) => "flat_maps",
1398                (true, false) => "flat_map",
1399                (false, true) => "maps",
1400                (false, false) => "map",
1401            };
1402            format!(
1403                "{} {{\n{}\n}} {}",
1404                kw,
1405                convert_block(block, 0),
1406                convert_expr(list)
1407            )
1408        }
1409        ExprKind::GrepExpr {
1410            block,
1411            list,
1412            keyword,
1413        } => {
1414            format!(
1415                "{} {{\n{}\n}} {}",
1416                keyword.as_str(),
1417                convert_block(block, 0),
1418                convert_expr(list)
1419            )
1420        }
1421        ExprKind::SortExpr { cmp, list } => match cmp {
1422            Some(SortComparator::Block(b)) => {
1423                format!(
1424                    "sort {{\n{}\n}} {}",
1425                    convert_block(b, 0),
1426                    convert_expr(list)
1427                )
1428            }
1429            Some(SortComparator::Code(e)) => {
1430                format!("sort {} {}", convert_expr(e), convert_expr(list))
1431            }
1432            None => format!("sort {}", convert_expr(list)),
1433        },
1434        ExprKind::JoinExpr { separator, list } => {
1435            format!("join({}, {})", convert_expr(separator), convert_expr(list))
1436        }
1437        ExprKind::SplitExpr {
1438            pattern,
1439            string,
1440            limit,
1441        } => match limit {
1442            Some(l) => format!(
1443                "split({}, {}, {})",
1444                convert_expr(pattern),
1445                convert_expr(string),
1446                convert_expr(l)
1447            ),
1448            None => format!("split({}, {})", convert_expr(pattern), convert_expr(string)),
1449        },
1450
1451        // ── Bless ────────────────────────────────────────────────────────
1452        ExprKind::Bless { ref_expr, class } => match class {
1453            Some(c) => format!("bless({}, {})", convert_expr(ref_expr), convert_expr(c)),
1454            None => format!("bless({})", convert_expr(ref_expr)),
1455        },
1456
1457        // ── Push / unshift / splice ──────────────────────────────────────
1458        ExprKind::Push { array, values } => {
1459            format!(
1460                "push({}, {})",
1461                convert_expr(array),
1462                convert_expr_list(values)
1463            )
1464        }
1465        ExprKind::Unshift { array, values } => {
1466            format!(
1467                "unshift({}, {})",
1468                convert_expr(array),
1469                convert_expr_list(values)
1470            )
1471        }
1472
1473        // ── Algebraic match ──────────────────────────────────────────────
1474        ExprKind::AlgebraicMatch { subject, arms } => {
1475            let arms_s = arms
1476                .iter()
1477                .map(|a| {
1478                    let guard_s = a
1479                        .guard
1480                        .as_ref()
1481                        .map(|g| format!(" if {}", convert_expr(g)))
1482                        .unwrap_or_default();
1483                    format!(
1484                        "{}{} => {}",
1485                        fmt::format_match_pattern(&a.pattern),
1486                        guard_s,
1487                        convert_expr(&a.body)
1488                    )
1489                })
1490                .collect::<Vec<_>>()
1491                .join(", ");
1492            format!("match ({}) {{ {} }}", convert_expr(subject), arms_s)
1493        }
1494
1495        // ── Everything else: delegate to fmt ─────────────────────────────
1496        _ => fmt::format_expr(e),
1497    }
1498}
1499
1500// ── Tests ───────────────────────────────────────────────────────────────────
1501
1502#[cfg(test)]
1503mod tests {
1504    use super::*;
1505    use crate::parse;
1506
1507    /// Helper: convert code and strip the shebang line for easier assertions.
1508    fn convert(code: &str) -> String {
1509        let p = parse(code).expect("parse failed");
1510        let out = convert_program(&p);
1511        // Strip shebang line for test comparisons
1512        out.strip_prefix("#!/usr/bin/env stryke\n")
1513            .unwrap_or(&out)
1514            .to_string()
1515    }
1516
1517    #[test]
1518    fn unary_builtin_direct() {
1519        // Single-stage: direct call syntax
1520        assert_eq!(convert("uc($x)"), "uc $x");
1521        assert_eq!(convert("length($str)"), "length $str");
1522    }
1523
1524    #[test]
1525    fn nested_unary_direct() {
1526        // 2-stage: direct call syntax (inner-to-outer order)
1527        let out = convert("uc(lc($x))");
1528        assert_eq!(out, "lc uc $x");
1529    }
1530
1531    #[test]
1532    fn nested_builtin_chain_thread() {
1533        let out = convert("chomp(lc(uc($x)))");
1534        assert_eq!(out, "t $x uc lc chomp");
1535    }
1536
1537    #[test]
1538    fn deeply_nested_thread() {
1539        let out = convert("length(chomp(lc(uc($x))))");
1540        assert_eq!(out, "t $x uc lc chomp length");
1541    }
1542
1543    #[test]
1544    fn map_grep_sort_thread() {
1545        let out = convert("sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @numbers");
1546        assert!(out.contains("t @numbers grep"));
1547        assert!(out.contains(" map"));
1548        assert!(out.contains(" sort"));
1549    }
1550
1551    #[test]
1552    fn join_direct() {
1553        let out = convert(r#"join(",", sort(@arr))"#);
1554        // 2-stage: direct call (inner-to-outer)
1555        assert!(out.contains("sort join \",\" @arr"));
1556    }
1557
1558    #[test]
1559    fn no_semicolons() {
1560        let out = convert("my $x = 1;\nmy $y = 2");
1561        assert!(!out.contains(';'));
1562        assert!(out.contains("my $x = 1"));
1563        assert!(out.contains("my $y = 2"));
1564    }
1565
1566    #[test]
1567    fn assignment_rhs_direct() {
1568        let out = convert("my $x = uc(lc($str))");
1569        // 2-stage: direct call
1570        assert_eq!(out, "my $x = lc uc $str");
1571    }
1572
1573    #[test]
1574    fn chain_in_subexpression_parenthesized() {
1575        let out = convert("$x + uc(lc($str))");
1576        // 2-stage chain should be parenthesized inside the binary op.
1577        assert!(out.contains("(lc uc $str)"));
1578    }
1579
1580    #[test]
1581    fn fn_body_indented() {
1582        let out = convert("fn foo { return uc(lc($x)); }");
1583        assert!(out.contains("fn foo"));
1584        // 2-stage: direct call
1585        assert!(out.contains("lc uc $x"));
1586        // Body should be indented
1587        assert!(out.contains("    return"));
1588    }
1589
1590    #[test]
1591    fn if_condition_converted() {
1592        let out = convert("if (defined(length($x))) { 1; }");
1593        // 2-stage: direct call
1594        assert!(out.contains("length defined $x"));
1595    }
1596
1597    #[test]
1598    fn method_call_preserved() {
1599        let out = convert("$obj->method($x)");
1600        assert!(out.contains("->method"));
1601    }
1602
1603    #[test]
1604    fn substitution_r_flag_direct() {
1605        // Single stage: direct syntax
1606        let out = convert(r#"($str =~ s/old/new/r)"#);
1607        assert!(out.contains("s/old/new/r $str"));
1608    }
1609
1610    #[test]
1611    fn user_func_call_direct() {
1612        let out = convert("fn Str::trim { } Str::trim(uc($x))");
1613        assert!(out.contains("fn Str::trim"));
1614        // 2-stage: direct call (inner-to-outer)
1615        assert!(out.contains("uc Str::trim $x"));
1616    }
1617
1618    #[test]
1619    fn user_func_extra_args_direct() {
1620        let out = convert("fn process { } process(uc($x), 42)");
1621        assert!(out.contains("fn process"));
1622        // Direct call (inner-to-outer): uc process 42 $x
1623        assert!(out.contains("uc process 42 $x"));
1624    }
1625
1626    #[test]
1627    fn map_grep_sort_chain_thread() {
1628        let out = convert("join(',', sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @nums)");
1629        assert!(out.contains("t @nums grep"));
1630        assert!(out.contains(" map"));
1631        assert!(out.contains(" sort"));
1632        assert!(out.contains(" join"));
1633    }
1634
1635    #[test]
1636    fn reduce_direct() {
1637        // Single stage with block: direct syntax
1638        let out = convert("reduce { $a + $b } @nums");
1639        assert!(out.contains("reduce {\n$a + $b\n} @nums"));
1640    }
1641
1642    #[test]
1643    fn shebang_prepended() {
1644        let p = parse("print 1").expect("parse failed");
1645        let out = convert_program(&p);
1646        assert!(out.starts_with("#!/usr/bin/env stryke\n"));
1647    }
1648
1649    #[test]
1650    fn indentation_in_blocks() {
1651        let out = convert("if ($x) { print 1; print 2; }");
1652        // Single stage: direct call syntax
1653        assert!(out.contains("\n    print 1\n    print 2\n"));
1654    }
1655
1656    #[test]
1657    fn binop_no_parens_at_top() {
1658        let out = convert("my $x = $a + $b");
1659        // At top level / assignment RHS, no parens around binop
1660        assert!(out.contains("= $a + $b"));
1661        assert!(!out.contains("= ($a + $b)"));
1662    }
1663
1664    fn convert_with_delim(code: &str, delim: char) -> String {
1665        let p = parse(code).expect("parse failed");
1666        let opts = ConvertOptions {
1667            output_delim: Some(delim),
1668        };
1669        let out = convert_program_with_options(&p, &opts);
1670        out.strip_prefix("#!/usr/bin/env stryke\n")
1671            .unwrap_or(&out)
1672            .to_string()
1673    }
1674
1675    #[test]
1676    fn output_delim_substitution() {
1677        let out = convert_with_delim("$x =~ s/foo/bar/g;", '|');
1678        assert_eq!(out, "$x =~ s|foo|bar|g");
1679    }
1680
1681    #[test]
1682    fn output_delim_transliterate() {
1683        let out = convert_with_delim("$y =~ tr/a-z/A-Z/;", '#');
1684        assert_eq!(out, "$y =~ tr#a-z#A-Z#");
1685    }
1686
1687    #[test]
1688    fn output_delim_match() {
1689        let out = convert_with_delim("$z =~ m/pattern/i;", '!');
1690        assert_eq!(out, "$z =~ !pattern!i");
1691    }
1692
1693    #[test]
1694    fn output_delim_preserves_original_when_none() {
1695        let out = convert("$x =~ s#old#new#g");
1696        assert_eq!(out, "$x =~ s#old#new#g");
1697    }
1698}