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            step,
1122        } => {
1123            let op = if *exclusive { "..." } else { ".." };
1124            if let Some(s) = step {
1125                format!(
1126                    "{} {} {}:{}",
1127                    convert_expr(from),
1128                    op,
1129                    convert_expr(to),
1130                    convert_expr(s)
1131                )
1132            } else {
1133                format!("{} {} {}", convert_expr(from), op, convert_expr(to))
1134            }
1135        }
1136        ExprKind::Repeat { expr, count } => {
1137            format!("{} x {}", convert_expr(expr), convert_expr(count))
1138        }
1139
1140        // ── Calls ────────────────────────────────────────────────────────
1141        ExprKind::FuncCall { name, args } => format!("{}({})", name, convert_expr_list(args)),
1142        ExprKind::MethodCall {
1143            object,
1144            method,
1145            args,
1146            super_call,
1147        } => {
1148            let m = if *super_call {
1149                format!("SUPER::{}", method)
1150            } else {
1151                method.clone()
1152            };
1153            format!(
1154                "{}->{}({})",
1155                convert_expr(object),
1156                m,
1157                convert_expr_list(args)
1158            )
1159        }
1160        ExprKind::IndirectCall {
1161            target,
1162            args,
1163            ampersand,
1164            pass_caller_arglist,
1165        } => {
1166            if *pass_caller_arglist && args.is_empty() {
1167                format!("&{}", convert_expr(target))
1168            } else {
1169                let inner = format!("{}({})", convert_expr(target), convert_expr_list(args));
1170                if *ampersand {
1171                    format!("&{}", inner)
1172                } else {
1173                    inner
1174                }
1175            }
1176        }
1177
1178        // ── Data structures ──────────────────────────────────────────────
1179        ExprKind::List(exprs) => format!("({})", convert_expr_list(exprs)),
1180        ExprKind::ArrayRef(elems) => format!("[{}]", convert_expr_list(elems)),
1181        ExprKind::HashRef(pairs) => {
1182            let inner = pairs
1183                .iter()
1184                .map(|(k, v)| format!("{} => {}", convert_expr(k), convert_expr(v)))
1185                .collect::<Vec<_>>()
1186                .join(", ");
1187            format!("{{{}}}", inner)
1188        }
1189        ExprKind::CodeRef { params, body } => {
1190            if params.is_empty() {
1191                format!("fn {{\n{}\n}}", convert_block(body, 0))
1192            } else {
1193                let sig = params
1194                    .iter()
1195                    .map(fmt::format_sub_sig_param)
1196                    .collect::<Vec<_>>()
1197                    .join(", ");
1198                format!("fn ({}) {{\n{}\n}}", sig, convert_block(body, 0))
1199            }
1200        }
1201
1202        // ── Access / deref ───────────────────────────────────────────────
1203        ExprKind::ArrayElement { array, index } => {
1204            format!("${}[{}]", array, convert_expr(index))
1205        }
1206        ExprKind::HashElement { hash, key } => {
1207            format!("${}{{{}}}", hash, convert_expr(key))
1208        }
1209        ExprKind::ScalarRef(inner) => format!("\\{}", convert_expr(inner)),
1210        ExprKind::ArrowDeref { expr, index, kind } => match kind {
1211            DerefKind::Array => {
1212                format!("({})->[{}]", convert_expr(expr), convert_expr(index))
1213            }
1214            DerefKind::Hash => {
1215                format!("({})->{{{}}}", convert_expr(expr), convert_expr(index))
1216            }
1217            DerefKind::Call => {
1218                format!("({})->({})", convert_expr(expr), convert_expr(index))
1219            }
1220        },
1221        ExprKind::Deref { expr, kind } => match kind {
1222            Sigil::Scalar => format!("${{{}}}", convert_expr(expr)),
1223            Sigil::Array => format!("@{{${}}}", convert_expr(expr)),
1224            Sigil::Hash => format!("%{{${}}}", convert_expr(expr)),
1225            Sigil::Typeglob => format!("*{{${}}}", convert_expr(expr)),
1226        },
1227
1228        // ── Print / say / die / warn ─────────────────────────────────────
1229        // print has no newline; say/p adds newline
1230        ExprKind::Print { handle, args } => {
1231            let h = handle
1232                .as_ref()
1233                .map(|h| format!("{} ", h))
1234                .unwrap_or_default();
1235            format!("print {}{}", h, convert_expr_list(args))
1236        }
1237        ExprKind::Say { handle, args } => {
1238            if let Some(h) = handle {
1239                format!("say {} {}", h, convert_expr_list(args))
1240            } else {
1241                format!("p {}", convert_expr_list(args))
1242            }
1243        }
1244        ExprKind::Printf { handle, args } => {
1245            let h = handle
1246                .as_ref()
1247                .map(|h| format!("{} ", h))
1248                .unwrap_or_default();
1249            format!("printf {}{}", h, convert_expr_list(args))
1250        }
1251        ExprKind::Die(args) => {
1252            if args.is_empty() {
1253                "die".to_string()
1254            } else {
1255                format!("die {}", convert_expr_list(args))
1256            }
1257        }
1258        ExprKind::Warn(args) => {
1259            if args.is_empty() {
1260                "warn".to_string()
1261            } else {
1262                format!("warn {}", convert_expr_list(args))
1263            }
1264        }
1265
1266        // ── Regex (non-piped) ────────────────────────────────────────────
1267        ExprKind::Match {
1268            expr,
1269            pattern,
1270            flags,
1271            delim,
1272            ..
1273        } => {
1274            let d = choose_delim(*delim);
1275            format!(
1276                "{} =~ {}{}{}{}",
1277                convert_expr(expr),
1278                d,
1279                fmt::escape_regex_part(pattern),
1280                d,
1281                flags
1282            )
1283        }
1284        ExprKind::Substitution {
1285            expr,
1286            pattern,
1287            replacement,
1288            flags,
1289            delim,
1290        } => {
1291            let d = choose_delim(*delim);
1292            format!(
1293                "{} =~ s{}{}{}{}{}{}",
1294                convert_expr(expr),
1295                d,
1296                fmt::escape_regex_part(pattern),
1297                d,
1298                fmt::escape_regex_part(replacement),
1299                d,
1300                flags
1301            )
1302        }
1303        ExprKind::Transliterate {
1304            expr,
1305            from,
1306            to,
1307            flags,
1308            delim,
1309        } => {
1310            let d = choose_delim(*delim);
1311            format!(
1312                "{} =~ tr{}{}{}{}{}{}",
1313                convert_expr(expr),
1314                d,
1315                fmt::escape_regex_part(from),
1316                d,
1317                fmt::escape_regex_part(to),
1318                d,
1319                flags
1320            )
1321        }
1322
1323        // ── Postfix modifiers ────────────────────────────────────────────
1324        ExprKind::PostfixIf { expr, condition } => {
1325            format!("{} if {}", convert_expr_top(expr), convert_expr(condition))
1326        }
1327        ExprKind::PostfixUnless { expr, condition } => {
1328            format!(
1329                "{} unless {}",
1330                convert_expr_top(expr),
1331                convert_expr(condition)
1332            )
1333        }
1334        ExprKind::PostfixWhile { expr, condition } => {
1335            format!(
1336                "{} while {}",
1337                convert_expr_top(expr),
1338                convert_expr(condition)
1339            )
1340        }
1341        ExprKind::PostfixUntil { expr, condition } => {
1342            format!(
1343                "{} until {}",
1344                convert_expr_top(expr),
1345                convert_expr(condition)
1346            )
1347        }
1348        ExprKind::PostfixForeach { expr, list } => {
1349            format!("{} for {}", convert_expr_top(expr), convert_expr(list))
1350        }
1351
1352        // ── Higher-order forms (fallback when not piped — e.g. empty list) ─
1353        ExprKind::MapExpr {
1354            block,
1355            list,
1356            flatten_array_refs,
1357            stream,
1358        } => {
1359            let kw = match (*flatten_array_refs, *stream) {
1360                (true, true) => "flat_maps",
1361                (true, false) => "flat_map",
1362                (false, true) => "maps",
1363                (false, false) => "map",
1364            };
1365            format!(
1366                "{} {{\n{}\n}} {}",
1367                kw,
1368                convert_block(block, 0),
1369                convert_expr(list)
1370            )
1371        }
1372        ExprKind::GrepExpr {
1373            block,
1374            list,
1375            keyword,
1376        } => {
1377            format!(
1378                "{} {{\n{}\n}} {}",
1379                keyword.as_str(),
1380                convert_block(block, 0),
1381                convert_expr(list)
1382            )
1383        }
1384        ExprKind::SortExpr { cmp, list } => match cmp {
1385            Some(SortComparator::Block(b)) => {
1386                format!(
1387                    "sort {{\n{}\n}} {}",
1388                    convert_block(b, 0),
1389                    convert_expr(list)
1390                )
1391            }
1392            Some(SortComparator::Code(e)) => {
1393                format!("sort {} {}", convert_expr(e), convert_expr(list))
1394            }
1395            None => format!("sort {}", convert_expr(list)),
1396        },
1397        ExprKind::JoinExpr { separator, list } => {
1398            format!("join({}, {})", convert_expr(separator), convert_expr(list))
1399        }
1400        ExprKind::SplitExpr {
1401            pattern,
1402            string,
1403            limit,
1404        } => match limit {
1405            Some(l) => format!(
1406                "split({}, {}, {})",
1407                convert_expr(pattern),
1408                convert_expr(string),
1409                convert_expr(l)
1410            ),
1411            None => format!("split({}, {})", convert_expr(pattern), convert_expr(string)),
1412        },
1413
1414        // ── Bless ────────────────────────────────────────────────────────
1415        ExprKind::Bless { ref_expr, class } => match class {
1416            Some(c) => format!("bless({}, {})", convert_expr(ref_expr), convert_expr(c)),
1417            None => format!("bless({})", convert_expr(ref_expr)),
1418        },
1419
1420        // ── Push / unshift / splice ──────────────────────────────────────
1421        ExprKind::Push { array, values } => {
1422            format!(
1423                "push({}, {})",
1424                convert_expr(array),
1425                convert_expr_list(values)
1426            )
1427        }
1428        ExprKind::Unshift { array, values } => {
1429            format!(
1430                "unshift({}, {})",
1431                convert_expr(array),
1432                convert_expr_list(values)
1433            )
1434        }
1435
1436        // ── Algebraic match ──────────────────────────────────────────────
1437        ExprKind::AlgebraicMatch { subject, arms } => {
1438            let arms_s = arms
1439                .iter()
1440                .map(|a| {
1441                    let guard_s = a
1442                        .guard
1443                        .as_ref()
1444                        .map(|g| format!(" if {}", convert_expr(g)))
1445                        .unwrap_or_default();
1446                    format!(
1447                        "{}{} => {}",
1448                        fmt::format_match_pattern(&a.pattern),
1449                        guard_s,
1450                        convert_expr(&a.body)
1451                    )
1452                })
1453                .collect::<Vec<_>>()
1454                .join(", ");
1455            format!("match ({}) {{ {} }}", convert_expr(subject), arms_s)
1456        }
1457
1458        // ── Everything else: delegate to fmt ─────────────────────────────
1459        _ => fmt::format_expr(e),
1460    }
1461}
1462
1463// ── Tests ───────────────────────────────────────────────────────────────────
1464
1465#[cfg(test)]
1466mod tests {
1467    use super::*;
1468    use crate::parse;
1469
1470    /// Helper: convert code and strip the shebang line for easier assertions.
1471    fn convert(code: &str) -> String {
1472        let p = parse(code).expect("parse failed");
1473        let out = convert_program(&p);
1474        // Strip shebang line for test comparisons
1475        out.strip_prefix("#!/usr/bin/env stryke\n")
1476            .unwrap_or(&out)
1477            .to_string()
1478    }
1479
1480    #[test]
1481    fn unary_builtin_direct() {
1482        // Single-stage: direct call syntax
1483        assert_eq!(convert("uc($x)"), "uc $x");
1484        assert_eq!(convert("length($str)"), "length $str");
1485    }
1486
1487    #[test]
1488    fn nested_unary_direct() {
1489        // 2-stage: direct call syntax (inner-to-outer order)
1490        let out = convert("uc(lc($x))");
1491        assert_eq!(out, "lc uc $x");
1492    }
1493
1494    #[test]
1495    fn nested_builtin_chain_thread() {
1496        let out = convert("chomp(lc(uc($x)))");
1497        assert_eq!(out, "t $x uc lc chomp");
1498    }
1499
1500    #[test]
1501    fn deeply_nested_thread() {
1502        let out = convert("length(chomp(lc(uc($x))))");
1503        assert_eq!(out, "t $x uc lc chomp length");
1504    }
1505
1506    #[test]
1507    fn map_grep_sort_thread() {
1508        let out = convert("sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @numbers");
1509        assert!(out.contains("t @numbers grep"));
1510        assert!(out.contains(" map"));
1511        assert!(out.contains(" sort"));
1512    }
1513
1514    #[test]
1515    fn join_direct() {
1516        let out = convert(r#"join(",", sort(@arr))"#);
1517        // 2-stage: direct call (inner-to-outer)
1518        assert!(out.contains("sort join \",\" @arr"));
1519    }
1520
1521    #[test]
1522    fn no_semicolons() {
1523        let out = convert("my $x = 1;\nmy $y = 2");
1524        assert!(!out.contains(';'));
1525        assert!(out.contains("my $x = 1"));
1526        assert!(out.contains("my $y = 2"));
1527    }
1528
1529    #[test]
1530    fn assignment_rhs_direct() {
1531        let out = convert("my $x = uc(lc($str))");
1532        // 2-stage: direct call
1533        assert_eq!(out, "my $x = lc uc $str");
1534    }
1535
1536    #[test]
1537    fn chain_in_subexpression_parenthesized() {
1538        let out = convert("$x + uc(lc($str))");
1539        // 2-stage chain should be parenthesized inside the binary op.
1540        assert!(out.contains("(lc uc $str)"));
1541    }
1542
1543    #[test]
1544    fn fn_body_indented() {
1545        let out = convert("fn foo { return uc(lc($x)); }");
1546        assert!(out.contains("fn foo"));
1547        // 2-stage: direct call
1548        assert!(out.contains("lc uc $x"));
1549        // Body should be indented
1550        assert!(out.contains("    return"));
1551    }
1552
1553    #[test]
1554    fn if_condition_converted() {
1555        let out = convert("if (defined(length($x))) { 1; }");
1556        // 2-stage: direct call
1557        assert!(out.contains("length defined $x"));
1558    }
1559
1560    #[test]
1561    fn method_call_preserved() {
1562        let out = convert("$obj->method($x)");
1563        assert!(out.contains("->method"));
1564    }
1565
1566    #[test]
1567    fn substitution_r_flag_direct() {
1568        // Single stage: direct syntax
1569        let out = convert(r#"($str =~ s/old/new/r)"#);
1570        assert!(out.contains("s/old/new/r $str"));
1571    }
1572
1573    #[test]
1574    fn user_func_call_direct() {
1575        let out = convert("fn my_trim { } my_trim(uc($x))");
1576        assert!(out.contains("fn my_trim"));
1577        // 2-stage: direct call (inner-to-outer)
1578        assert!(out.contains("uc my_trim $x"));
1579    }
1580
1581    #[test]
1582    fn user_func_extra_args_direct() {
1583        let out = convert("fn process { } process(uc($x), 42)");
1584        assert!(out.contains("fn process"));
1585        // Direct call (inner-to-outer): uc process 42 $x
1586        assert!(out.contains("uc process 42 $x"));
1587    }
1588
1589    #[test]
1590    fn map_grep_sort_chain_thread() {
1591        let out = convert("join(',', sort { $a <=> $b } map { $_ * 2 } grep { $_ > 0 } @nums)");
1592        assert!(out.contains("t @nums grep"));
1593        assert!(out.contains(" map"));
1594        assert!(out.contains(" sort"));
1595        assert!(out.contains(" join"));
1596    }
1597
1598    #[test]
1599    fn reduce_direct() {
1600        // Single stage with block: direct syntax
1601        let out = convert("use List::Util 'reduce';\nreduce { $a + $b } @nums");
1602        assert!(out.contains("reduce {\n$a + $b\n} @nums"));
1603    }
1604
1605    #[test]
1606    fn shebang_prepended() {
1607        let p = parse("print 1").expect("parse failed");
1608        let out = convert_program(&p);
1609        assert!(out.starts_with("#!/usr/bin/env stryke\n"));
1610    }
1611
1612    #[test]
1613    fn indentation_in_blocks() {
1614        let out = convert("if ($x) { print 1; print 2; }");
1615        // Single stage: direct call syntax
1616        assert!(out.contains("\n    print 1\n    print 2\n"));
1617    }
1618
1619    #[test]
1620    fn binop_no_parens_at_top() {
1621        let out = convert("my $x = $a + $b");
1622        // At top level / assignment RHS, no parens around binop
1623        assert!(out.contains("= $a + $b"));
1624        assert!(!out.contains("= ($a + $b)"));
1625    }
1626
1627    fn convert_with_delim(code: &str, delim: char) -> String {
1628        let p = parse(code).expect("parse failed");
1629        let opts = ConvertOptions {
1630            output_delim: Some(delim),
1631        };
1632        let out = convert_program_with_options(&p, &opts);
1633        out.strip_prefix("#!/usr/bin/env stryke\n")
1634            .unwrap_or(&out)
1635            .to_string()
1636    }
1637
1638    #[test]
1639    fn output_delim_substitution() {
1640        let out = convert_with_delim("$x =~ s/foo/bar/g;", '|');
1641        assert_eq!(out, "$x =~ s|foo|bar|g");
1642    }
1643
1644    #[test]
1645    fn output_delim_transliterate() {
1646        let out = convert_with_delim("$y =~ tr/a-z/A-Z/;", '#');
1647        assert_eq!(out, "$y =~ tr#a-z#A-Z#");
1648    }
1649
1650    #[test]
1651    fn output_delim_match() {
1652        let out = convert_with_delim("$z =~ m/pattern/i;", '!');
1653        assert_eq!(out, "$z =~ !pattern!i");
1654    }
1655
1656    #[test]
1657    fn output_delim_preserves_original_when_none() {
1658        let out = convert("$x =~ s#old#new#g");
1659        assert_eq!(out, "$x =~ s#old#new#g");
1660    }
1661}