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::StmtGroup(b) => convert_block(b, depth),
403        StmtKind::Block(b) => format!("{{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
404        StmtKind::Begin(b) => format!("BEGIN {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
405        StmtKind::UnitCheck(b) => {
406            format!("UNITCHECK {{\n{}\n{}}}", convert_block(b, depth + 1), pfx)
407        }
408        StmtKind::Check(b) => format!("CHECK {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
409        StmtKind::Init(b) => format!("INIT {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
410        StmtKind::End(b) => format!("END {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
411        StmtKind::Empty => String::new(),
412        StmtKind::Goto { target } => format!("goto {}", convert_expr(target)),
413        StmtKind::Continue(b) => format!("continue {{\n{}\n{}}}", convert_block(b, depth + 1), pfx),
414        StmtKind::StructDecl { def } => {
415            let fields = def
416                .fields
417                .iter()
418                .map(|f| format!("{} => {}", f.name, f.ty.display_name()))
419                .collect::<Vec<_>>()
420                .join(", ");
421            format!("struct {} {{ {} }}", def.name, fields)
422        }
423        StmtKind::EnumDecl { def } => {
424            let variants = def
425                .variants
426                .iter()
427                .map(|v| {
428                    if let Some(ty) = &v.ty {
429                        format!("{} => {}", v.name, ty.display_name())
430                    } else {
431                        v.name.clone()
432                    }
433                })
434                .collect::<Vec<_>>()
435                .join(", ");
436            format!("enum {} {{ {} }}", def.name, variants)
437        }
438        StmtKind::ClassDecl { def } => {
439            let prefix = if def.is_abstract {
440                "abstract "
441            } else if def.is_final {
442                "final "
443            } else {
444                ""
445            };
446            let mut parts = vec![format!("{}class {}", prefix, def.name)];
447            if !def.extends.is_empty() {
448                parts.push(format!("extends {}", def.extends.join(", ")));
449            }
450            if !def.implements.is_empty() {
451                parts.push(format!("impl {}", def.implements.join(", ")));
452            }
453            let fields = def
454                .fields
455                .iter()
456                .map(|f| {
457                    let vis = match f.visibility {
458                        crate::ast::Visibility::Private => "priv ",
459                        crate::ast::Visibility::Protected => "prot ",
460                        crate::ast::Visibility::Public => "",
461                    };
462                    format!("{}{}: {}", vis, f.name, f.ty.display_name())
463                })
464                .collect::<Vec<_>>()
465                .join("; ");
466            format!("{} {{ {} }}", parts.join(" "), fields)
467        }
468        StmtKind::TraitDecl { def } => {
469            let methods = def
470                .methods
471                .iter()
472                .map(|m| format!("fn {}", m.name))
473                .collect::<Vec<_>>()
474                .join("; ");
475            format!("trait {} {{ {} }}", def.name, methods)
476        }
477        StmtKind::EvalTimeout { timeout, body } => {
478            format!(
479                "eval_timeout {} {{\n{}\n{}}}",
480                convert_expr(timeout),
481                convert_block(body, depth + 1),
482                pfx
483            )
484        }
485        StmtKind::TryCatch {
486            try_block,
487            catch_var,
488            catch_block,
489            finally_block,
490        } => {
491            let fin = finally_block
492                .as_ref()
493                .map(|b| {
494                    format!(
495                        "\n{}finally {{\n{}\n{}}}",
496                        pfx,
497                        convert_block(b, depth + 1),
498                        pfx
499                    )
500                })
501                .unwrap_or_default();
502            format!(
503                "try {{\n{}\n{}}} catch (${}) {{\n{}\n{}}}{}",
504                convert_block(try_block, depth + 1),
505                pfx,
506                catch_var,
507                convert_block(catch_block, depth + 1),
508                pfx,
509                fin
510            )
511        }
512        StmtKind::Given { topic, body } => {
513            format!(
514                "given ({}) {{\n{}\n{}}}",
515                convert_expr(topic),
516                convert_block(body, depth + 1),
517                pfx
518            )
519        }
520        StmtKind::When { cond, body } => {
521            format!(
522                "when ({}) {{\n{}\n{}}}",
523                convert_expr(cond),
524                convert_block(body, depth + 1),
525                pfx
526            )
527        }
528        StmtKind::DefaultCase { body } => {
529            format!("default {{\n{}\n{}}}", convert_block(body, depth + 1), pfx)
530        }
531        StmtKind::FormatDecl { name, lines } => {
532            let mut s = format!("format {} =\n", name);
533            for ln in lines {
534                s.push_str(ln);
535                s.push('\n');
536            }
537            s.push('.');
538            s
539        }
540        StmtKind::Tie {
541            target,
542            class,
543            args,
544        } => {
545            let target_s = match target {
546                crate::ast::TieTarget::Hash(h) => format!("%{}", h),
547                crate::ast::TieTarget::Array(a) => format!("@{}", a),
548                crate::ast::TieTarget::Scalar(s) => format!("${}", s),
549            };
550            let mut s = format!("tie {} {}", target_s, convert_expr(class));
551            for a in args {
552                s.push_str(&format!(", {}", convert_expr(a)));
553            }
554            s
555        }
556    };
557    format!("{}{}{}", pfx, lab, body)
558}
559
560/// Convert a statement body without indentation prefix (for C-style for init).
561fn convert_statement_body(s: &Statement) -> String {
562    let lab = s
563        .label
564        .as_ref()
565        .map(|l| format!("{}: ", l))
566        .unwrap_or_default();
567    let body = match &s.kind {
568        StmtKind::Expression(e) => convert_expr_top(e),
569        StmtKind::My(decls) => format!("my {}", convert_var_decls(decls)),
570        _ => convert_statement(s, 0).trim().to_string(),
571    };
572    format!("{}{}", lab, body)
573}
574
575// ── Variable declarations ───────────────────────────────────────────────────
576
577fn convert_var_decls(decls: &[VarDecl]) -> String {
578    decls
579        .iter()
580        .map(|d| {
581            let sig = match d.sigil {
582                Sigil::Scalar => "$",
583                Sigil::Array => "@",
584                Sigil::Hash => "%",
585                Sigil::Typeglob => "*",
586            };
587            let mut s = format!("{}{}", sig, d.name);
588            if let Some(ref t) = d.type_annotation {
589                s.push_str(&format!(" : {}", t.display_name()));
590            }
591            if let Some(ref init) = d.initializer {
592                s.push_str(&format!(" = {}", convert_expr_top(init)));
593            }
594            s
595        })
596        .collect::<Vec<_>>()
597        .join(", ")
598}
599
600// ── Expression conversion ───────────────────────────────────────────────────
601
602fn convert_expr_list(es: &[Expr]) -> String {
603    es.iter().map(convert_expr).collect::<Vec<_>>().join(", ")
604}
605
606/// Format a string part for converted output.
607/// Uses simple `$name` when possible, `${name}` only when needed.
608fn convert_string_part(p: &StringPart) -> String {
609    match p {
610        StringPart::Literal(s) => fmt::escape_interpolated_literal(s),
611        StringPart::ScalarVar(n) => {
612            // Use ${} only if name has special chars or would be ambiguous
613            if needs_braces(n) {
614                format!("${{{}}}", n)
615            } else {
616                format!("${}", n)
617            }
618        }
619        StringPart::ArrayVar(n) => {
620            if needs_braces(n) {
621                format!("@{{{}}}", n)
622            } else {
623                format!("@{}", n)
624            }
625        }
626        StringPart::Expr(e) => fmt::format_expr(e),
627    }
628}
629
630/// Check if a variable name needs braces in interpolation.
631fn needs_braces(name: &str) -> bool {
632    // Empty or starts with digit needs braces
633    if name.is_empty() || name.chars().next().is_some_and(|c| c.is_ascii_digit()) {
634        return true;
635    }
636    // Contains non-identifier chars
637    !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
638}
639
640/// Convert an expression at statement level or assignment RHS — pipe chains
641/// are emitted without outer parentheses.
642fn convert_expr_top(e: &Expr) -> String {
643    convert_expr_impl(e, true)
644}
645
646/// Convert an expression in a sub-expression context — pipe chains are wrapped
647/// in parentheses to preserve precedence.
648fn convert_expr(e: &Expr) -> String {
649    convert_expr_impl(e, false)
650}
651
652fn convert_expr_impl(e: &Expr, top: bool) -> String {
653    let mut segments: Vec<String> = Vec::new();
654    let source = extract_pipe_source(e, &mut segments);
655    if !segments.is_empty() {
656        segments.reverse();
657        // 1-2 stages: direct call syntax (e.g., `print "$x"`, `uc lc $x`)
658        // 3+ stages: thread macro (e.g., `t $x lc uc print`)
659        if segments.len() <= 2 {
660            let result = format!("{} {}", segments.join(" "), source);
661            if !top {
662                return format!("({})", result);
663            }
664            return result;
665        }
666        // 3+ stages: use thread macro
667        let stages = segments.join(" ");
668        // Strip outer parens from source if it's a parenthesized list/thread
669        let source = if source.starts_with("(t ") || source.starts_with("((") {
670            source[1..source.len() - 1].to_string()
671        } else {
672            source
673        };
674        let result = format!("t {} {}", source, stages);
675        if !top {
676            return format!("({})", result);
677        }
678        return result;
679    }
680    // No pipe chain — format with recursive sub-expression conversion.
681    convert_expr_direct(e, top)
682}
683
684// ── Pipe chain extraction ───────────────────────────────────────────────────
685//
686// Walks the expression tree from the outermost call inward, peeling off
687// each pipeable layer as a segment string.  Segments are pushed in
688// outer-to-inner order; the caller reverses before joining with `|>`.
689
690fn extract_pipe_source(e: &Expr, segments: &mut Vec<String>) -> String {
691    match &e.kind {
692        // ── Unary builtins ──────────────────────────────────────────────
693        ExprKind::Uc(inner) => {
694            segments.push("uc".into());
695            extract_pipe_source(inner, segments)
696        }
697        ExprKind::Lc(inner) => {
698            segments.push("lc".into());
699            extract_pipe_source(inner, segments)
700        }
701        ExprKind::Ucfirst(inner) => {
702            segments.push("ucfirst".into());
703            extract_pipe_source(inner, segments)
704        }
705        ExprKind::Lcfirst(inner) => {
706            segments.push("lcfirst".into());
707            extract_pipe_source(inner, segments)
708        }
709        ExprKind::Fc(inner) => {
710            segments.push("fc".into());
711            extract_pipe_source(inner, segments)
712        }
713        ExprKind::Chomp(inner) => {
714            segments.push("chomp".into());
715            extract_pipe_source(inner, segments)
716        }
717        ExprKind::Chop(inner) => {
718            segments.push("chop".into());
719            extract_pipe_source(inner, segments)
720        }
721        ExprKind::Length(inner) => {
722            segments.push("length".into());
723            extract_pipe_source(inner, segments)
724        }
725        ExprKind::Abs(inner) => {
726            segments.push("abs".into());
727            extract_pipe_source(inner, segments)
728        }
729        ExprKind::Int(inner) => {
730            segments.push("int".into());
731            extract_pipe_source(inner, segments)
732        }
733        ExprKind::Sqrt(inner) => {
734            segments.push("sqrt".into());
735            extract_pipe_source(inner, segments)
736        }
737        ExprKind::Sin(inner) => {
738            segments.push("sin".into());
739            extract_pipe_source(inner, segments)
740        }
741        ExprKind::Cos(inner) => {
742            segments.push("cos".into());
743            extract_pipe_source(inner, segments)
744        }
745        ExprKind::Exp(inner) => {
746            segments.push("exp".into());
747            extract_pipe_source(inner, segments)
748        }
749        ExprKind::Log(inner) => {
750            segments.push("log".into());
751            extract_pipe_source(inner, segments)
752        }
753        ExprKind::Hex(inner) => {
754            segments.push("hex".into());
755            extract_pipe_source(inner, segments)
756        }
757        ExprKind::Oct(inner) => {
758            segments.push("oct".into());
759            extract_pipe_source(inner, segments)
760        }
761        ExprKind::Chr(inner) => {
762            segments.push("chr".into());
763            extract_pipe_source(inner, segments)
764        }
765        ExprKind::Ord(inner) => {
766            segments.push("ord".into());
767            extract_pipe_source(inner, segments)
768        }
769        ExprKind::Defined(inner) => {
770            segments.push("defined".into());
771            extract_pipe_source(inner, segments)
772        }
773        ExprKind::Ref(inner) => {
774            segments.push("ref".into());
775            extract_pipe_source(inner, segments)
776        }
777        ExprKind::ScalarContext(inner) => {
778            segments.push("scalar".into());
779            extract_pipe_source(inner, segments)
780        }
781        ExprKind::Keys(inner) => {
782            segments.push("keys".into());
783            extract_pipe_source(inner, segments)
784        }
785        ExprKind::Values(inner) => {
786            segments.push("values".into());
787            extract_pipe_source(inner, segments)
788        }
789        ExprKind::Each(inner) => {
790            segments.push("each".into());
791            extract_pipe_source(inner, segments)
792        }
793        ExprKind::Pop(inner) => {
794            segments.push("pop".into());
795            extract_pipe_source(inner, segments)
796        }
797        ExprKind::Shift(inner) => {
798            segments.push("shift".into());
799            extract_pipe_source(inner, segments)
800        }
801        ExprKind::ReverseExpr(inner) => {
802            segments.push("reverse".into());
803            extract_pipe_source(inner, segments)
804        }
805        ExprKind::Slurp(inner) => {
806            segments.push("slurp".into());
807            extract_pipe_source(inner, segments)
808        }
809        ExprKind::Chdir(inner) => {
810            segments.push("chdir".into());
811            extract_pipe_source(inner, segments)
812        }
813        ExprKind::Stat(inner) => {
814            segments.push("stat".into());
815            extract_pipe_source(inner, segments)
816        }
817        ExprKind::Lstat(inner) => {
818            segments.push("lstat".into());
819            extract_pipe_source(inner, segments)
820        }
821        ExprKind::Readlink(inner) => {
822            segments.push("readlink".into());
823            extract_pipe_source(inner, segments)
824        }
825        ExprKind::Study(inner) => {
826            segments.push("study".into());
827            extract_pipe_source(inner, segments)
828        }
829        ExprKind::Close(inner) => {
830            segments.push("close".into());
831            extract_pipe_source(inner, segments)
832        }
833        ExprKind::Readdir(inner) => {
834            segments.push("readdir".into());
835            extract_pipe_source(inner, segments)
836        }
837        ExprKind::Eval(inner) => {
838            segments.push("eval".into());
839            extract_pipe_source(inner, segments)
840        }
841        ExprKind::Require(inner) => {
842            segments.push("require".into());
843            extract_pipe_source(inner, segments)
844        }
845
846        // ── List-taking higher-order builtins ────────────────────────────
847        ExprKind::MapExpr {
848            block,
849            list,
850            flatten_array_refs,
851            stream,
852        } => {
853            let kw = match (*flatten_array_refs, *stream) {
854                (true, true) => "flat_maps",
855                (true, false) => "flat_map",
856                (false, true) => "maps",
857                (false, false) => "map",
858            };
859            segments.push(format!("{} {{\n{}\n}}", kw, convert_block(block, 0)));
860            extract_pipe_source(list, segments)
861        }
862        ExprKind::MapExprComma {
863            expr,
864            list,
865            flatten_array_refs,
866            stream,
867        } => {
868            let kw = match (*flatten_array_refs, *stream) {
869                (true, true) => "flat_maps",
870                (true, false) => "flat_map",
871                (false, true) => "maps",
872                (false, false) => "map",
873            };
874            // Convert comma form to block form for cleaner pipe syntax.
875            segments.push(format!("{} {{ {} }}", kw, convert_expr_top(expr)));
876            extract_pipe_source(list, segments)
877        }
878        ExprKind::GrepExpr {
879            block,
880            list,
881            keyword,
882        } => {
883            segments.push(format!(
884                "{} {{\n{}\n}}",
885                keyword.as_str(),
886                convert_block(block, 0)
887            ));
888            extract_pipe_source(list, segments)
889        }
890        ExprKind::GrepExprComma {
891            expr,
892            list,
893            keyword,
894        } => {
895            segments.push(format!(
896                "{} {{ {} }}",
897                keyword.as_str(),
898                convert_expr_top(expr)
899            ));
900            extract_pipe_source(list, segments)
901        }
902        ExprKind::SortExpr { cmp, list } => {
903            let seg = match cmp {
904                Some(SortComparator::Block(b)) => {
905                    format!("sort {{\n{}\n}}", convert_block(b, 0))
906                }
907                Some(SortComparator::Code(e)) => {
908                    format!("sort {}", convert_expr(e))
909                }
910                None => "sort".to_string(),
911            };
912            segments.push(seg);
913            extract_pipe_source(list, segments)
914        }
915        ExprKind::JoinExpr { separator, list } => {
916            segments.push(format!("join {}", convert_expr(separator)));
917            extract_pipe_source(list, segments)
918        }
919        ExprKind::ReduceExpr { block, list } => {
920            segments.push(format!("reduce {{\n{}\n}}", convert_block(block, 0)));
921            extract_pipe_source(list, segments)
922        }
923        ExprKind::ForEachExpr { block, list } => {
924            segments.push(format!("fore {{\n{}\n}}", convert_block(block, 0)));
925            extract_pipe_source(list, segments)
926        }
927
928        // ── Parallel higher-order builtins ───────────────────────────────
929        ExprKind::PMapExpr {
930            block,
931            list,
932            progress,
933            flat_outputs,
934            on_cluster,
935            stream: _,
936        } if progress.is_none() && on_cluster.is_none() => {
937            let kw = if *flat_outputs { "pflat_map" } else { "pmap" };
938            segments.push(format!("{} {{\n{}\n}}", kw, convert_block(block, 0)));
939            extract_pipe_source(list, segments)
940        }
941        ExprKind::PGrepExpr {
942            block,
943            list,
944            progress,
945            stream: _,
946        } if progress.is_none() => {
947            segments.push(format!("pgrep {{\n{}\n}}", convert_block(block, 0)));
948            extract_pipe_source(list, segments)
949        }
950        ExprKind::PSortExpr {
951            cmp,
952            list,
953            progress,
954        } if progress.is_none() => {
955            let seg = match cmp {
956                Some(b) => format!("psort {{\n{}\n}}", convert_block(b, 0)),
957                None => "psort".to_string(),
958            };
959            segments.push(seg);
960            extract_pipe_source(list, segments)
961        }
962
963        // ── Print / say with single arg → pipe ───────────────────────────
964        // say adds newline → p; print does not → print
965        ExprKind::Say { handle: None, args } if args.len() == 1 => {
966            segments.push("p".into());
967            extract_pipe_source(&args[0], segments)
968        }
969        ExprKind::Print { handle: None, args } if args.len() == 1 => {
970            segments.push("print".into());
971            extract_pipe_source(&args[0], segments)
972        }
973
974        // ── Generic function calls ───────────────────────────────────────
975        ExprKind::FuncCall { name, args } if !args.is_empty() => {
976            let seg = if args.len() == 1 {
977                name.clone()
978            } else {
979                let rest = args[1..]
980                    .iter()
981                    .map(convert_expr)
982                    .collect::<Vec<_>>()
983                    .join(", ");
984                format!("{} {}", name, rest)
985            };
986            segments.push(seg);
987            extract_pipe_source(&args[0], segments)
988        }
989
990        // ── Substitution with /r flag (value-returning) ──────────────────
991        ExprKind::Substitution {
992            expr,
993            pattern,
994            replacement,
995            flags,
996            delim,
997        } if flags.contains('r') => {
998            // `$str =~ s/old/new/r` → `$str |> s/old/new/r`
999            // In pipe context the parser auto-injects `r`, but keeping it
1000            // is harmless and explicit.
1001            let d = choose_delim(*delim);
1002            segments.push(format!(
1003                "s{}{}{}{}{}{}",
1004                d,
1005                fmt::escape_regex_part(pattern),
1006                d,
1007                fmt::escape_regex_part(replacement),
1008                d,
1009                flags
1010            ));
1011            extract_pipe_source(expr, segments)
1012        }
1013
1014        // ── Transliterate with /r flag ───────────────────────────────────
1015        ExprKind::Transliterate {
1016            expr,
1017            from,
1018            to,
1019            flags,
1020            delim,
1021        } if flags.contains('r') => {
1022            let d = choose_delim(*delim);
1023            segments.push(format!(
1024                "tr{}{}{}{}{}{}",
1025                d,
1026                fmt::escape_regex_part(from),
1027                d,
1028                fmt::escape_regex_part(to),
1029                d,
1030                flags
1031            ));
1032            extract_pipe_source(expr, segments)
1033        }
1034
1035        // ── Single-element list: unwrap and continue extraction ──────────
1036        ExprKind::List(elems) if elems.len() == 1 => extract_pipe_source(&elems[0], segments),
1037
1038        // ── Base case: not pipeable ──────────────────────────────────────
1039        _ => convert_expr_direct(e, false),
1040    }
1041}
1042
1043// ── Direct expression formatting (no pipe extraction) ───────────────────────
1044//
1045// Handles the common expression types with recursive `convert_expr` calls
1046// for sub-expressions.  Rare / complex variants delegate to `fmt::format_expr`.
1047
1048fn convert_expr_direct(e: &Expr, top: bool) -> String {
1049    match &e.kind {
1050        // ── Leaf / simple (delegate to fmt) ──────────────────────────────
1051        ExprKind::Integer(_)
1052        | ExprKind::Float(_)
1053        | ExprKind::String(_)
1054        | ExprKind::Bareword(_)
1055        | ExprKind::Regex(..)
1056        | ExprKind::QW(_)
1057        | ExprKind::Undef
1058        | ExprKind::MagicConst(_)
1059        | ExprKind::ScalarVar(_)
1060        | ExprKind::ArrayVar(_)
1061        | ExprKind::HashVar(_)
1062        | ExprKind::Typeglob(_)
1063        | ExprKind::Wantarray
1064        | ExprKind::SubroutineRef(_)
1065        | ExprKind::SubroutineCodeRef(_) => fmt::format_expr(e),
1066
1067        // ── Interpolated strings — parts may embed expressions ───────────
1068        ExprKind::InterpolatedString(parts) => {
1069            format!(
1070                "\"{}\"",
1071                parts.iter().map(convert_string_part).collect::<String>()
1072            )
1073        }
1074
1075        // ── Binary operations ────────────────────────────────────────────
1076        ExprKind::BinOp { left, op, right } => {
1077            format!(
1078                "{} {} {}",
1079                convert_expr(left),
1080                fmt::format_binop(*op),
1081                convert_expr(right)
1082            )
1083        }
1084
1085        // ── Unary / postfix ──────────────────────────────────────────────
1086        ExprKind::UnaryOp { op, expr } => {
1087            format!("{}{}", fmt::format_unary(*op), convert_expr(expr))
1088        }
1089        ExprKind::PostfixOp { expr, op } => {
1090            format!("{}{}", convert_expr(expr), fmt::format_postfix(*op))
1091        }
1092
1093        // ── Assignment ───────────────────────────────────────────────────
1094        ExprKind::Assign { target, value } => {
1095            format!("{} = {}", convert_expr(target), convert_expr_top(value))
1096        }
1097        ExprKind::CompoundAssign { target, op, value } => format!(
1098            "{} {}= {}",
1099            convert_expr(target),
1100            fmt::format_binop(*op),
1101            convert_expr_top(value)
1102        ),
1103
1104        // ── Ternary ──────────────────────────────────────────────────────
1105        ExprKind::Ternary {
1106            condition,
1107            then_expr,
1108            else_expr,
1109        } => format!(
1110            "{} ? {} : {}",
1111            convert_expr(condition),
1112            convert_expr(then_expr),
1113            convert_expr(else_expr)
1114        ),
1115
1116        // ── Range / repeat ───────────────────────────────────────────────
1117        ExprKind::Range {
1118            from,
1119            to,
1120            exclusive,
1121        } => {
1122            let op = if *exclusive { "..." } else { ".." };
1123            format!("{} {} {}", convert_expr(from), op, convert_expr(to))
1124        }
1125        ExprKind::Repeat { expr, count } => {
1126            format!("{} x {}", convert_expr(expr), convert_expr(count))
1127        }
1128
1129        // ── Calls ────────────────────────────────────────────────────────
1130        ExprKind::FuncCall { name, args } => format!("{}({})", name, convert_expr_list(args)),
1131        ExprKind::MethodCall {
1132            object,
1133            method,
1134            args,
1135            super_call,
1136        } => {
1137            let m = if *super_call {
1138                format!("SUPER::{}", method)
1139            } else {
1140                method.clone()
1141            };
1142            format!(
1143                "{}->{}({})",
1144                convert_expr(object),
1145                m,
1146                convert_expr_list(args)
1147            )
1148        }
1149        ExprKind::IndirectCall {
1150            target,
1151            args,
1152            ampersand,
1153            pass_caller_arglist,
1154        } => {
1155            if *pass_caller_arglist && args.is_empty() {
1156                format!("&{}", convert_expr(target))
1157            } else {
1158                let inner = format!("{}({})", convert_expr(target), convert_expr_list(args));
1159                if *ampersand {
1160                    format!("&{}", inner)
1161                } else {
1162                    inner
1163                }
1164            }
1165        }
1166
1167        // ── Data structures ──────────────────────────────────────────────
1168        ExprKind::List(exprs) => format!("({})", convert_expr_list(exprs)),
1169        ExprKind::ArrayRef(elems) => format!("[{}]", convert_expr_list(elems)),
1170        ExprKind::HashRef(pairs) => {
1171            let inner = pairs
1172                .iter()
1173                .map(|(k, v)| format!("{} => {}", convert_expr(k), convert_expr(v)))
1174                .collect::<Vec<_>>()
1175                .join(", ");
1176            format!("{{{}}}", inner)
1177        }
1178        ExprKind::CodeRef { params, body } => {
1179            if params.is_empty() {
1180                format!("fn {{\n{}\n}}", convert_block(body, 0))
1181            } else {
1182                let sig = params
1183                    .iter()
1184                    .map(fmt::format_sub_sig_param)
1185                    .collect::<Vec<_>>()
1186                    .join(", ");
1187                format!("fn ({}) {{\n{}\n}}", sig, convert_block(body, 0))
1188            }
1189        }
1190
1191        // ── Access / deref ───────────────────────────────────────────────
1192        ExprKind::ArrayElement { array, index } => {
1193            format!("${}[{}]", array, convert_expr(index))
1194        }
1195        ExprKind::HashElement { hash, key } => {
1196            format!("${}{{{}}}", hash, convert_expr(key))
1197        }
1198        ExprKind::ScalarRef(inner) => format!("\\{}", convert_expr(inner)),
1199        ExprKind::ArrowDeref { expr, index, kind } => match kind {
1200            DerefKind::Array => {
1201                format!("({})->[{}]", convert_expr(expr), convert_expr(index))
1202            }
1203            DerefKind::Hash => {
1204                format!("({})->{{{}}}", convert_expr(expr), convert_expr(index))
1205            }
1206            DerefKind::Call => {
1207                format!("({})->({})", convert_expr(expr), convert_expr(index))
1208            }
1209        },
1210        ExprKind::Deref { expr, kind } => match kind {
1211            Sigil::Scalar => format!("${{{}}}", convert_expr(expr)),
1212            Sigil::Array => format!("@{{${}}}", convert_expr(expr)),
1213            Sigil::Hash => format!("%{{${}}}", convert_expr(expr)),
1214            Sigil::Typeglob => format!("*{{${}}}", convert_expr(expr)),
1215        },
1216
1217        // ── Print / say / die / warn ─────────────────────────────────────
1218        // print has no newline; say/p adds newline
1219        ExprKind::Print { handle, args } => {
1220            let h = handle
1221                .as_ref()
1222                .map(|h| format!("{} ", h))
1223                .unwrap_or_default();
1224            format!("print {}{}", h, convert_expr_list(args))
1225        }
1226        ExprKind::Say { handle, args } => {
1227            if let Some(h) = handle {
1228                format!("say {} {}", h, convert_expr_list(args))
1229            } else {
1230                format!("p {}", convert_expr_list(args))
1231            }
1232        }
1233        ExprKind::Printf { handle, args } => {
1234            let h = handle
1235                .as_ref()
1236                .map(|h| format!("{} ", h))
1237                .unwrap_or_default();
1238            format!("printf {}{}", h, convert_expr_list(args))
1239        }
1240        ExprKind::Die(args) => {
1241            if args.is_empty() {
1242                "die".to_string()
1243            } else {
1244                format!("die {}", convert_expr_list(args))
1245            }
1246        }
1247        ExprKind::Warn(args) => {
1248            if args.is_empty() {
1249                "warn".to_string()
1250            } else {
1251                format!("warn {}", convert_expr_list(args))
1252            }
1253        }
1254
1255        // ── Regex (non-piped) ────────────────────────────────────────────
1256        ExprKind::Match {
1257            expr,
1258            pattern,
1259            flags,
1260            delim,
1261            ..
1262        } => {
1263            let d = choose_delim(*delim);
1264            format!(
1265                "{} =~ {}{}{}{}",
1266                convert_expr(expr),
1267                d,
1268                fmt::escape_regex_part(pattern),
1269                d,
1270                flags
1271            )
1272        }
1273        ExprKind::Substitution {
1274            expr,
1275            pattern,
1276            replacement,
1277            flags,
1278            delim,
1279        } => {
1280            let d = choose_delim(*delim);
1281            format!(
1282                "{} =~ s{}{}{}{}{}{}",
1283                convert_expr(expr),
1284                d,
1285                fmt::escape_regex_part(pattern),
1286                d,
1287                fmt::escape_regex_part(replacement),
1288                d,
1289                flags
1290            )
1291        }
1292        ExprKind::Transliterate {
1293            expr,
1294            from,
1295            to,
1296            flags,
1297            delim,
1298        } => {
1299            let d = choose_delim(*delim);
1300            format!(
1301                "{} =~ tr{}{}{}{}{}{}",
1302                convert_expr(expr),
1303                d,
1304                fmt::escape_regex_part(from),
1305                d,
1306                fmt::escape_regex_part(to),
1307                d,
1308                flags
1309            )
1310        }
1311
1312        // ── Postfix modifiers ────────────────────────────────────────────
1313        ExprKind::PostfixIf { expr, condition } => {
1314            format!("{} if {}", convert_expr_top(expr), convert_expr(condition))
1315        }
1316        ExprKind::PostfixUnless { expr, condition } => {
1317            format!(
1318                "{} unless {}",
1319                convert_expr_top(expr),
1320                convert_expr(condition)
1321            )
1322        }
1323        ExprKind::PostfixWhile { expr, condition } => {
1324            format!(
1325                "{} while {}",
1326                convert_expr_top(expr),
1327                convert_expr(condition)
1328            )
1329        }
1330        ExprKind::PostfixUntil { expr, condition } => {
1331            format!(
1332                "{} until {}",
1333                convert_expr_top(expr),
1334                convert_expr(condition)
1335            )
1336        }
1337        ExprKind::PostfixForeach { expr, list } => {
1338            format!("{} for {}", convert_expr_top(expr), convert_expr(list))
1339        }
1340
1341        // ── Higher-order forms (fallback when not piped — e.g. empty list) ─
1342        ExprKind::MapExpr {
1343            block,
1344            list,
1345            flatten_array_refs,
1346            stream,
1347        } => {
1348            let kw = match (*flatten_array_refs, *stream) {
1349                (true, true) => "flat_maps",
1350                (true, false) => "flat_map",
1351                (false, true) => "maps",
1352                (false, false) => "map",
1353            };
1354            format!(
1355                "{} {{\n{}\n}} {}",
1356                kw,
1357                convert_block(block, 0),
1358                convert_expr(list)
1359            )
1360        }
1361        ExprKind::GrepExpr {
1362            block,
1363            list,
1364            keyword,
1365        } => {
1366            format!(
1367                "{} {{\n{}\n}} {}",
1368                keyword.as_str(),
1369                convert_block(block, 0),
1370                convert_expr(list)
1371            )
1372        }
1373        ExprKind::SortExpr { cmp, list } => match cmp {
1374            Some(SortComparator::Block(b)) => {
1375                format!(
1376                    "sort {{\n{}\n}} {}",
1377                    convert_block(b, 0),
1378                    convert_expr(list)
1379                )
1380            }
1381            Some(SortComparator::Code(e)) => {
1382                format!("sort {} {}", convert_expr(e), convert_expr(list))
1383            }
1384            None => format!("sort {}", convert_expr(list)),
1385        },
1386        ExprKind::JoinExpr { separator, list } => {
1387            format!("join({}, {})", convert_expr(separator), convert_expr(list))
1388        }
1389        ExprKind::SplitExpr {
1390            pattern,
1391            string,
1392            limit,
1393        } => match limit {
1394            Some(l) => format!(
1395                "split({}, {}, {})",
1396                convert_expr(pattern),
1397                convert_expr(string),
1398                convert_expr(l)
1399            ),
1400            None => format!("split({}, {})", convert_expr(pattern), convert_expr(string)),
1401        },
1402
1403        // ── Bless ────────────────────────────────────────────────────────
1404        ExprKind::Bless { ref_expr, class } => match class {
1405            Some(c) => format!("bless({}, {})", convert_expr(ref_expr), convert_expr(c)),
1406            None => format!("bless({})", convert_expr(ref_expr)),
1407        },
1408
1409        // ── Push / unshift / splice ──────────────────────────────────────
1410        ExprKind::Push { array, values } => {
1411            format!(
1412                "push({}, {})",
1413                convert_expr(array),
1414                convert_expr_list(values)
1415            )
1416        }
1417        ExprKind::Unshift { array, values } => {
1418            format!(
1419                "unshift({}, {})",
1420                convert_expr(array),
1421                convert_expr_list(values)
1422            )
1423        }
1424
1425        // ── Algebraic match ──────────────────────────────────────────────
1426        ExprKind::AlgebraicMatch { subject, arms } => {
1427            let arms_s = arms
1428                .iter()
1429                .map(|a| {
1430                    let guard_s = a
1431                        .guard
1432                        .as_ref()
1433                        .map(|g| format!(" if {}", convert_expr(g)))
1434                        .unwrap_or_default();
1435                    format!(
1436                        "{}{} => {}",
1437                        fmt::format_match_pattern(&a.pattern),
1438                        guard_s,
1439                        convert_expr(&a.body)
1440                    )
1441                })
1442                .collect::<Vec<_>>()
1443                .join(", ");
1444            format!("match ({}) {{ {} }}", convert_expr(subject), arms_s)
1445        }
1446
1447        // ── Everything else: delegate to fmt ─────────────────────────────
1448        _ => fmt::format_expr(e),
1449    }
1450}
1451
1452// ── Tests ───────────────────────────────────────────────────────────────────
1453
1454#[cfg(test)]
1455mod tests {
1456    use super::*;
1457    use crate::parse;
1458
1459    /// Helper: convert code and strip the shebang line for easier assertions.
1460    fn convert(code: &str) -> String {
1461        let p = parse(code).expect("parse failed");
1462        let out = convert_program(&p);
1463        // Strip shebang line for test comparisons
1464        out.strip_prefix("#!/usr/bin/env stryke\n")
1465            .unwrap_or(&out)
1466            .to_string()
1467    }
1468
1469    #[test]
1470    fn unary_builtin_direct() {
1471        // Single-stage: direct call syntax
1472        assert_eq!(convert("uc($x)"), "uc $x");
1473        assert_eq!(convert("length($str)"), "length $str");
1474    }
1475
1476    #[test]
1477    fn nested_unary_direct() {
1478        // 2-stage: direct call syntax (inner-to-outer order)
1479        let out = convert("uc(lc($x))");
1480        assert_eq!(out, "lc uc $x");
1481    }
1482
1483    #[test]
1484    fn nested_builtin_chain_thread() {
1485        let out = convert("chomp(lc(uc($x)))");
1486        assert_eq!(out, "t $x uc lc chomp");
1487    }
1488
1489    #[test]
1490    fn deeply_nested_thread() {
1491        let out = convert("length(chomp(lc(uc($x))))");
1492        assert_eq!(out, "t $x uc lc chomp length");
1493    }
1494
1495    #[test]
1496    fn map_grep_sort_thread() {
1497        let out = convert("sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @numbers");
1498        assert!(out.contains("t @numbers grep"));
1499        assert!(out.contains(" map"));
1500        assert!(out.contains(" sort"));
1501    }
1502
1503    #[test]
1504    fn join_direct() {
1505        let out = convert(r#"join(",", sort(@arr))"#);
1506        // 2-stage: direct call (inner-to-outer)
1507        assert!(out.contains("sort join \",\" @arr"));
1508    }
1509
1510    #[test]
1511    fn no_semicolons() {
1512        let out = convert("my $x = 1;\nmy $y = 2");
1513        assert!(!out.contains(';'));
1514        assert!(out.contains("my $x = 1"));
1515        assert!(out.contains("my $y = 2"));
1516    }
1517
1518    #[test]
1519    fn assignment_rhs_direct() {
1520        let out = convert("my $x = uc(lc($str))");
1521        // 2-stage: direct call
1522        assert_eq!(out, "my $x = lc uc $str");
1523    }
1524
1525    #[test]
1526    fn chain_in_subexpression_parenthesized() {
1527        let out = convert("$x + uc(lc($str))");
1528        // 2-stage chain should be parenthesized inside the binary op.
1529        assert!(out.contains("(lc uc $str)"));
1530    }
1531
1532    #[test]
1533    fn fn_body_indented() {
1534        let out = convert("sub foo { return uc(lc($x)); }");
1535        assert!(out.contains("fn foo"));
1536        // 2-stage: direct call
1537        assert!(out.contains("lc uc $x"));
1538        // Body should be indented
1539        assert!(out.contains("    return"));
1540    }
1541
1542    #[test]
1543    fn if_condition_converted() {
1544        let out = convert("if (defined(length($x))) { 1; }");
1545        // 2-stage: direct call
1546        assert!(out.contains("length defined $x"));
1547    }
1548
1549    #[test]
1550    fn method_call_preserved() {
1551        let out = convert("$obj->method($x)");
1552        assert!(out.contains("->method"));
1553    }
1554
1555    #[test]
1556    fn substitution_r_flag_direct() {
1557        // Single stage: direct syntax
1558        let out = convert(r#"($str =~ s/old/new/r)"#);
1559        assert!(out.contains("s/old/new/r $str"));
1560    }
1561
1562    #[test]
1563    fn user_func_call_direct() {
1564        let out = convert("sub trim { } trim(uc($x))");
1565        assert!(out.contains("fn trim"));
1566        // 2-stage: direct call (inner-to-outer)
1567        assert!(out.contains("uc trim $x"));
1568    }
1569
1570    #[test]
1571    fn user_func_extra_args_direct() {
1572        let out = convert("sub process { } process(uc($x), 42)");
1573        assert!(out.contains("fn process"));
1574        // Direct call (inner-to-outer): uc process 42 $x
1575        assert!(out.contains("uc process 42 $x"));
1576    }
1577
1578    #[test]
1579    fn map_grep_sort_chain_thread() {
1580        let out = convert("join(',', sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @nums)");
1581        assert!(out.contains("t @nums grep"));
1582        assert!(out.contains(" map"));
1583        assert!(out.contains(" sort"));
1584        assert!(out.contains(" join"));
1585    }
1586
1587    #[test]
1588    fn reduce_direct() {
1589        // Single stage with block: direct syntax
1590        let out = convert("use List::Util 'reduce';\nreduce { $a + $b } @nums");
1591        assert!(out.contains("reduce {\n$a + $b\n} @nums"));
1592    }
1593
1594    #[test]
1595    fn shebang_prepended() {
1596        let p = parse("print 1").expect("parse failed");
1597        let out = convert_program(&p);
1598        assert!(out.starts_with("#!/usr/bin/env stryke\n"));
1599    }
1600
1601    #[test]
1602    fn indentation_in_blocks() {
1603        let out = convert("if ($x) { print 1; print 2; }");
1604        // Single stage: direct call syntax
1605        assert!(out.contains("\n    print 1\n    print 2\n"));
1606    }
1607
1608    #[test]
1609    fn binop_no_parens_at_top() {
1610        let out = convert("my $x = $a + $b");
1611        // At top level / assignment RHS, no parens around binop
1612        assert!(out.contains("= $a + $b"));
1613        assert!(!out.contains("= ($a + $b)"));
1614    }
1615
1616    fn convert_with_delim(code: &str, delim: char) -> String {
1617        let p = parse(code).expect("parse failed");
1618        let opts = ConvertOptions {
1619            output_delim: Some(delim),
1620        };
1621        let out = convert_program_with_options(&p, &opts);
1622        out.strip_prefix("#!/usr/bin/env stryke\n")
1623            .unwrap_or(&out)
1624            .to_string()
1625    }
1626
1627    #[test]
1628    fn output_delim_substitution() {
1629        let out = convert_with_delim("$x =~ s/foo/bar/g;", '|');
1630        assert_eq!(out, "$x =~ s|foo|bar|g");
1631    }
1632
1633    #[test]
1634    fn output_delim_transliterate() {
1635        let out = convert_with_delim("$y =~ tr/a-z/A-Z/;", '#');
1636        assert_eq!(out, "$y =~ tr#a-z#A-Z#");
1637    }
1638
1639    #[test]
1640    fn output_delim_match() {
1641        let out = convert_with_delim("$z =~ m/pattern/i;", '!');
1642        assert_eq!(out, "$z =~ !pattern!i");
1643    }
1644
1645    #[test]
1646    fn output_delim_preserves_original_when_none() {
1647        let out = convert("$x =~ s#old#new#g");
1648        assert_eq!(out, "$x =~ s#old#new#g");
1649    }
1650}