Skip to main content

xpile_ruchy_codegen/
lib.rs

1//! Ruchy backend.
2//!
3//! Lowers meta-HIR to Ruchy source. v0.1.0 emits the same arithmetic
4//! subset as `xpile-rust-codegen` — the surface difference is
5//! `fun ... -> T { ... }` instead of Rust's `pub fn ... -> T { ... }`,
6//! and floor-div / modulo still go through Euclidean semantics
7//! (`div_euclid` / `rem_euclid`).
8//!
9//! Future scope (tracked by `Profile::RuchyOut`): reconstruct the
10//! pipeline operator `|>` and DataFrame-flavored sugar from meta-HIR
11//! patterns. See `docs/specifications/sub/bidirectional-ruchy.md`.
12
13use std::fmt::Write;
14use xpile_backend::{Artifact, Backend, BackendConfig, BackendError, QuorumStatus, Target};
15use xpile_meta_hir::{
16    BinOp, Block, Expr, FloatOp, Function, Item, Module, NumBuiltinOp, Param, Stmt, StrMethodOp,
17    Type, UnOp,
18};
19
20/// PMAT-477 (R8): Ruchy → Rust infix symbol for a float arithmetic op.
21fn float_op_sym(op: FloatOp) -> &'static str {
22    match op {
23        FloatOp::Add => "+",
24        FloatOp::Sub => "-",
25        FloatOp::Mul => "*",
26        FloatOp::Div => "/",
27    }
28}
29
30#[derive(Debug, thiserror::Error)]
31pub enum RuchyCodegenError {
32    #[error("unsupported item: {0}")]
33    Unsupported(String),
34    #[error("formatting error: {0}")]
35    Format(#[from] std::fmt::Error),
36}
37
38pub fn emit_module(module: &Module) -> Result<String, RuchyCodegenError> {
39    let mut out = String::new();
40    writeln!(
41        out,
42        "// xpile-generated from {:?} module {}",
43        module.source_lang, module.name
44    )?;
45    writeln!(out)?;
46    for item in &module.items {
47        match item {
48            Item::Function(f) => emit_function(&mut out, f)?,
49        }
50    }
51    Ok(out)
52}
53
54fn emit_function(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
55    emit_contract_citations(out, f)?;
56    // Ruchy: `fun name(params) -> ret { body }`. No `pub`.
57    write!(out, "fun {}(", f.name)?;
58    for (i, p) in f.params.iter().enumerate() {
59        if i > 0 {
60            write!(out, ", ")?;
61        }
62        emit_param(out, p)?;
63    }
64    write!(out, ") -> ")?;
65    emit_type(out, &f.return_type)?;
66    writeln!(out, " {{")?;
67    let mode = function_bigint_mode(f);
68    emit_block(out, &f.body, mode)?;
69    writeln!(out, "}}")?;
70    Ok(())
71}
72
73/// PMAT-012-FOLLOWUP / PMAT-025: a function is in BigInt mode if any
74/// param is BigInt, the return type is BigInt, OR any pre-bound Let
75/// is BigInt. In BigInt mode, the Ruchy backend emits the same shape
76/// as the Rust backend (since Ruchy compiles to Rust):
77/// `xpile_bigint::BigInt::from(<n>i64)` literals + plain infix
78/// arithmetic + `.clone()` on Ident references (BigInt isn't `Copy`).
79fn function_bigint_mode(f: &Function) -> bool {
80    if matches!(f.return_type, Type::BigInt) {
81        return true;
82    }
83    if f.params.iter().any(|p| matches!(p.ty, Type::BigInt)) {
84        return true;
85    }
86    fn stmt_has_bigint(s: &Stmt) -> bool {
87        match s {
88            Stmt::Let { ty, .. } => matches!(ty, Type::BigInt),
89            // PMAT-479 (R10): early return introduces no BigInt binding.
90            // PMAT-494b: tuple unpacking introduces no BigInt binding.
91            // PMAT-503a: a raise introduces no BigInt binding.
92            Stmt::Assign { .. }
93            | Stmt::Assert { .. }
94            | Stmt::Return(_)
95            | Stmt::LetTuple { .. }
96            | Stmt::Raise { .. } => false,
97            Stmt::While { body, .. }
98            | Stmt::ForEach { body, .. }
99            | Stmt::ForEachPair { body, .. } => body.iter().any(stmt_has_bigint),
100            // PMAT-478 (R9): recurse both branches of an if/else.
101            Stmt::If {
102                then_body,
103                else_body,
104                ..
105            } => then_body.iter().any(stmt_has_bigint) || else_body.iter().any(stmt_has_bigint),
106            // PMAT-460: list.append() — same disposition.
107            Stmt::ListAppend { .. } | Stmt::SetAdd { .. } => false,
108            // PMAT-461: indexed assignment same disposition.
109            Stmt::IndexAssign { .. } => false,
110            // PMAT-466: dict keyed assignment same disposition.
111            Stmt::DictSet { .. } => false,
112            // PMAT-039: see rust-codegen's twin arm — shell commands
113            // carry no BigInt operands.
114            Stmt::Cmd { .. } => false,
115            // PMAT-041: see rust-codegen's twin arm.
116            Stmt::Pipeline { .. } => false,
117            // PMAT-048: see rust-codegen's twin arm.
118            Stmt::ShellLoop { .. } => false,
119            // PMAT-051: see rust-codegen's twin arm.
120            Stmt::ShellAssign { .. } => false,
121        }
122    }
123    f.body.stmts.iter().any(stmt_has_bigint)
124}
125
126/// PMAT-011: same `// xpile-contract: <ID>` form as the Rust backend.
127/// Ruchy compiles to Rust, so it shares the comment-citation convention.
128fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
129    for id in f.applicable_contracts() {
130        writeln!(out, "// xpile-contract: {id}")?;
131    }
132    Ok(())
133}
134
135fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), RuchyCodegenError> {
136    for stmt in &block.stmts {
137        emit_stmt(out, stmt, mode)?;
138    }
139    write!(out, "    ")?;
140    emit_expr(out, &block.trailing_return, mode)?;
141    writeln!(out)?;
142    Ok(())
143}
144
145fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), RuchyCodegenError> {
146    emit_stmt_indented(out, stmt, "    ", mode)
147}
148
149fn emit_stmt_indented(
150    out: &mut String,
151    stmt: &Stmt,
152    indent: &str,
153    mode: bool,
154) -> Result<(), RuchyCodegenError> {
155    match stmt {
156        Stmt::Let {
157            name,
158            ty,
159            value,
160            mutable,
161        } => {
162            let kw = if *mutable { "let mut" } else { "let" };
163            write!(out, "{indent}{kw} {name}: ")?;
164            emit_type(out, ty)?;
165            write!(out, " = ")?;
166            emit_expr(out, value, mode)?;
167            writeln!(out, ";")?;
168            Ok(())
169        }
170        // PMAT-494b: tuple unpacking → `let (a, b, ...) = <value>;`.
171        Stmt::LetTuple { names, value } => {
172            write!(out, "{indent}let ({}) = ", names.join(", "))?;
173            emit_expr(out, value, mode)?;
174            writeln!(out, ";")?;
175            Ok(())
176        }
177        // PMAT-479 (R10): early `return <expr>;` (guard clause).
178        Stmt::Return(e) => {
179            write!(out, "{indent}return ")?;
180            emit_expr(out, e, mode)?;
181            writeln!(out, ";")?;
182            Ok(())
183        }
184        // PMAT-478 (R9): if/else statement → `if c { … } else { … }`.
185        Stmt::If {
186            cond,
187            then_body,
188            else_body,
189        } => {
190            write!(out, "{indent}if ")?;
191            emit_expr(out, cond, mode)?;
192            writeln!(out, " {{")?;
193            let inner = format!("{indent}    ");
194            for s in then_body {
195                emit_stmt_indented(out, s, &inner, mode)?;
196            }
197            if else_body.is_empty() {
198                writeln!(out, "{indent}}}")?;
199            } else {
200                writeln!(out, "{indent}}} else {{")?;
201                for s in else_body {
202                    emit_stmt_indented(out, s, &inner, mode)?;
203                }
204                writeln!(out, "{indent}}}")?;
205            }
206            Ok(())
207        }
208        Stmt::Assign { name, value } => {
209            write!(out, "{indent}{name} = ")?;
210            emit_expr(out, value, mode)?;
211            writeln!(out, ";")?;
212            Ok(())
213        }
214        Stmt::While { cond, body } => {
215            write!(out, "{indent}while ")?;
216            emit_expr(out, cond, mode)?;
217            writeln!(out, " {{")?;
218            let inner = format!("{indent}    ");
219            for s in body {
220                emit_stmt_indented(out, s, &inner, mode)?;
221            }
222            writeln!(out, "{indent}}}")?;
223            Ok(())
224        }
225        // PMAT-458 (v0.2.0 Track 1.B): Ruchy → Rust → for-each with
226        // .iter().cloned() for owned-value bindings.
227        Stmt::ForEach {
228            var,
229            iter,
230            body,
231            over_keys,
232            ..
233        } => {
234            // PMAT-472 (R3): dict iterates keys via `.keys().cloned()`.
235            let method = if *over_keys { "keys" } else { "iter" };
236            write!(out, "{indent}for {var} in ")?;
237            emit_expr(out, iter, mode)?;
238            writeln!(out, ".{method}().cloned() {{")?;
239            let inner = format!("{indent}    ");
240            for s in body {
241                emit_stmt_indented(out, s, &inner, mode)?;
242            }
243            writeln!(out, "{indent}}}")?;
244            Ok(())
245        }
246        // PMAT-495: paired for-loop (enumerate / zip), Ruchy → Rust.
247        Stmt::ForEachPair {
248            first,
249            second,
250            iter,
251            kind,
252            body,
253        } => {
254            write!(out, "{indent}for ({first}, {second}) in ")?;
255            emit_expr(out, iter, mode)?;
256            match kind {
257                xpile_meta_hir::PairIterKind::Enumerate => {
258                    out.push_str(
259                        ".iter().cloned().enumerate().map(|(__i, __e)| (__i as i64, __e))",
260                    );
261                }
262                xpile_meta_hir::PairIterKind::Zip(other) => {
263                    out.push_str(".iter().cloned().zip(");
264                    emit_expr(out, other, mode)?;
265                    out.push_str(".iter().cloned())");
266                }
267            }
268            writeln!(out, " {{")?;
269            let inner = format!("{indent}    ");
270            for s in body {
271                emit_stmt_indented(out, s, &inner, mode)?;
272            }
273            writeln!(out, "{indent}}}")?;
274            Ok(())
275        }
276        // PMAT-460 (v0.2.0 Track 1.B): Ruchy → Rust → `.push(...)`.
277        Stmt::ListAppend { list_name, elem } => {
278            write!(out, "{indent}{list_name}.push(")?;
279            emit_expr(out, elem, mode)?;
280            writeln!(out, ");")?;
281            Ok(())
282        }
283        // PMAT-500b: Ruchy → Rust `s.insert(x);`.
284        Stmt::SetAdd { set_name, elem } => {
285            write!(out, "{indent}{set_name}.insert(")?;
286            emit_expr(out, elem, mode)?;
287            writeln!(out, ");")?;
288            Ok(())
289        }
290        // PMAT-461 (v0.2.0 Track 1.B): Ruchy → Rust →
291        // `xs[i as usize] = v;`, matching the Rust backend.
292        Stmt::IndexAssign {
293            list_name,
294            index,
295            value,
296        } => {
297            write!(out, "{indent}{list_name}[")?;
298            emit_expr(out, index, mode)?;
299            out.push_str(" as usize] = ");
300            emit_expr(out, value, mode)?;
301            writeln!(out, ";")?;
302            Ok(())
303        }
304        // PMAT-466 (v0.2.0 Track 1.C): Ruchy → Rust
305        // `{ let __v = v; d.insert(k.clone(), __v); }`, matching the Rust
306        // backend — value bound to a temp before insert, and the key
307        // cloned so a non-Copy str key survives a later read (see the
308        // Rust twin arm for the full move-then-borrow rationale).
309        Stmt::DictSet {
310            dict_name,
311            key,
312            value,
313        } => {
314            write!(out, "{indent}{{ let __xpile_dict_val = ")?;
315            emit_expr(out, value, mode)?;
316            write!(out, "; {dict_name}.insert(")?;
317            emit_expr(out, key, mode)?;
318            writeln!(out, ".clone(), __xpile_dict_val); }}")?;
319            Ok(())
320        }
321        Stmt::Assert { cond } => {
322            write!(out, "{indent}assert!(")?;
323            emit_expr(out, cond, mode)?;
324            writeln!(out, ");")?;
325            Ok(())
326        }
327        // PMAT-503a: `raise Exc("msg")` → `panic!("{}", <message>);` (Ruchy
328        // compiles to Rust and inherits the diverging-panic disposition).
329        Stmt::Raise { message } => {
330            write!(out, "{indent}panic!(\"{{}}\", ")?;
331            emit_expr(out, message, mode)?;
332            writeln!(out, ");")?;
333            Ok(())
334        }
335        // PMAT-039 / XPILE-BASHRS-MERGER-001 Layer B: see rust-codegen's
336        // matching arm. Ruchy compiles to Rust and inherits Rust's
337        // disposition — no Ruchy-level translation of `Stmt::Cmd`
338        // exists.
339        Stmt::Cmd { program, args } => Err(RuchyCodegenError::Unsupported(format!(
340            "Ruchy backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
341             contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
342             use `--target shell` to emit POSIX sh via bashrs-backend",
343            args.len()
344        ))),
345        // PMAT-041: same disposition as Cmd.
346        Stmt::Pipeline { stages } => Err(RuchyCodegenError::Unsupported(format!(
347            "Ruchy backend does not lower Stmt::Pipeline ({} stages) — \
348             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
349             use `--target shell`",
350            stages.len()
351        ))),
352        // PMAT-048: same disposition.
353        Stmt::ShellLoop { .. } => Err(RuchyCodegenError::Unsupported(
354            "Ruchy backend does not lower Stmt::ShellLoop — \
355             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
356             use `--target shell`"
357                .into(),
358        )),
359        // PMAT-051: same disposition.
360        Stmt::ShellAssign { name, .. } => Err(RuchyCodegenError::Unsupported(format!(
361            "Ruchy backend does not lower Stmt::ShellAssign (`{name}=…`) — \
362             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
363             use `--target shell`"
364        ))),
365    }
366}
367
368fn emit_param(out: &mut String, p: &Param) -> Result<(), RuchyCodegenError> {
369    // PMAT-460: same posture as the Rust backend.
370    if p.mutable {
371        write!(out, "mut ")?;
372    }
373    write!(out, "{}: ", p.name)?;
374    emit_type(out, &p.ty)?;
375    Ok(())
376}
377
378/// Escape a string for emission inside a Ruchy `"..."` literal.
379/// PMAT-449 — Ruchy compiles to Rust, so identical escape semantics.
380fn escape_ruchy_str(s: &str) -> String {
381    let mut out = String::with_capacity(s.len());
382    for c in s.chars() {
383        match c {
384            '\\' => out.push_str("\\\\"),
385            '"' => out.push_str("\\\""),
386            other => out.push(other),
387        }
388    }
389    out
390}
391
392fn emit_type(out: &mut String, t: &Type) -> Result<(), RuchyCodegenError> {
393    match t {
394        Type::I64 => out.push_str("i64"),
395        // PMAT-477 (R8): Ruchy → Rust `f64`.
396        Type::F64 => out.push_str("f64"),
397        Type::Bool => out.push_str("bool"),
398        // Ruchy compiles to Rust → same BigInt re-export. PMAT-012.
399        Type::BigInt => out.push_str("xpile_bigint::BigInt"),
400        // PMAT-449 (v0.2.0 Track 1.A): Ruchy → Rust → owned `String`,
401        // mirrors xpile-rust-codegen's lowering.
402        Type::Str => out.push_str("String"),
403        // PMAT-455 (v0.2.0 Track 1.B): Ruchy → Rust Vec<T>.
404        Type::List(elem_ty) => {
405            out.push_str("Vec<");
406            emit_type(out, elem_ty)?;
407            out.push('>');
408        }
409        // PMAT-462 (v0.2.0 Track 1.C): Ruchy → Rust HashMap<K, V>.
410        Type::Dict(k_ty, v_ty) => {
411            out.push_str("std::collections::HashMap<");
412            emit_type(out, k_ty)?;
413            out.push_str(", ");
414            emit_type(out, v_ty)?;
415            out.push('>');
416        }
417        // PMAT-500: Ruchy → Rust `HashSet<T>`.
418        Type::Set(elem_ty) => {
419            out.push_str("std::collections::HashSet<");
420            emit_type(out, elem_ty)?;
421            out.push('>');
422        }
423        // PMAT-494: Ruchy → Rust `(T0, T1, ...)`.
424        Type::Tuple(elems) => {
425            out.push('(');
426            for (i, t) in elems.iter().enumerate() {
427                if i > 0 {
428                    out.push_str(", ");
429                }
430                emit_type(out, t)?;
431            }
432            out.push(')');
433        }
434        // PMAT-046: same disposition as the Rust backend.
435        Type::ShellString | Type::ExitCode => {
436            return Err(RuchyCodegenError::Unsupported(format!(
437                "Ruchy backend does not lower {t:?} — \
438                 contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
439                 use `--target shell`"
440            )));
441        }
442    }
443    Ok(())
444}
445
446fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), RuchyCodegenError> {
447    match e {
448        Expr::Ident(name) => {
449            // PMAT-025: in BigInt mode, append `.clone()` to every
450            // Ident reference. BigInt isn't `Copy` (it's
451            // heap-allocated), so a name referenced in cond +
452            // branches + recursive call would move-on-first-use.
453            // Mirrors the Rust backend's PMAT-013 emission.
454            if mode {
455                write!(out, "{}.clone()", name)?;
456            } else {
457                write!(out, "{}", name)?;
458            }
459        }
460        Expr::LitInt(v) => {
461            if mode {
462                write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
463            } else {
464                write!(out, "{}i64", v)?;
465            }
466        }
467        // PMAT-477 (R8): float literal + plain-infix float arithmetic.
468        Expr::LitFloat(v) => write!(out, "{}f64", v)?,
469        Expr::FloatBinOp { op, lhs, rhs } => {
470            out.push('(');
471            emit_expr(out, lhs, mode)?;
472            write!(out, " {} ", float_op_sym(*op))?;
473            emit_expr(out, rhs, mode)?;
474            out.push(')');
475        }
476        // PMAT-456 (v0.2.0 Track 1.B): Ruchy → Rust → lowercase
477        // `true` / `false`.
478        Expr::LitBool(b) => write!(out, "{}", b)?,
479        Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
480        // PMAT-451 (v0.2.0 Track 1.A): same str-concat shape as the
481        // Rust backend — Ruchy compiles to Rust, so `format!()` works
482        // identically.
483        Expr::Concat { lhs, rhs } => {
484            out.push_str("format!(\"{}{}\", ");
485            emit_expr(out, lhs, mode)?;
486            out.push_str(", ");
487            emit_expr(out, rhs, mode)?;
488            out.push(')');
489        }
490        // PMAT-492/493b: Python string methods (Ruchy → Rust). No-arg
491        // transforms emit a suffix; startswith/endswith emit
492        // `.starts_with(&(<pat>)[..])` (the reslice yields `&str`).
493        Expr::StrMethod { recv, op, args } => {
494            // PMAT-492d: `join` inverts receiver/arg (sep.join(xs) → xs.join(sep)).
495            if matches!(op, StrMethodOp::Join) {
496                emit_expr(out, &args[0], mode)?;
497                out.push_str(".join(&(");
498                emit_expr(out, recv, mode)?;
499                out.push_str(")[..])");
500                return Ok(());
501            }
502            emit_expr(out, recv, mode)?;
503            match op {
504                StrMethodOp::Upper => out.push_str(".to_uppercase()"),
505                StrMethodOp::Lower => out.push_str(".to_lowercase()"),
506                StrMethodOp::Strip => out.push_str(".trim().to_string()"),
507                StrMethodOp::StartsWith | StrMethodOp::EndsWith => {
508                    out.push_str(if matches!(op, StrMethodOp::StartsWith) {
509                        ".starts_with(&("
510                    } else {
511                        ".ends_with(&("
512                    });
513                    emit_expr(out, &args[0], mode)?;
514                    out.push_str(")[..])");
515                }
516                // PMAT-492c: `.split(sep)` → Vec<String>.
517                StrMethodOp::Split => {
518                    out.push_str(".split(&(");
519                    emit_expr(out, &args[0], mode)?;
520                    out.push_str(")[..]).map(|__c| __c.to_string()).collect::<Vec<String>>()");
521                }
522                // PMAT-502b: `.replace(old, new)`.
523                StrMethodOp::Replace => {
524                    out.push_str(".replace(&(");
525                    emit_expr(out, &args[0], mode)?;
526                    out.push_str(")[..], &(");
527                    emit_expr(out, &args[1], mode)?;
528                    out.push_str(")[..])");
529                }
530                StrMethodOp::Join => unreachable!("Join handled above"),
531            }
532        }
533        // PMAT-455 (v0.2.0 Track 1.B): Ruchy → Rust → `vec![...]`.
534        Expr::ListLit(elems) => {
535            out.push_str("vec![");
536            for (i, e) in elems.iter().enumerate() {
537                if i > 0 {
538                    out.push_str(", ");
539                }
540                emit_expr(out, e, mode)?;
541            }
542            out.push(']');
543        }
544        // PMAT-494: Python tuple literal → Ruchy → Rust `(e0, e1, ...)`.
545        Expr::TupleLit(elems) => {
546            out.push('(');
547            for (i, e) in elems.iter().enumerate() {
548                if i > 0 {
549                    out.push_str(", ");
550                }
551                emit_expr(out, e, mode)?;
552            }
553            out.push(')');
554        }
555        // PMAT-496: Ruchy → Rust slice (`.to_vec()` / `.to_string()`).
556        Expr::Slice {
557            collection,
558            lo,
559            hi,
560            of_str,
561        } => {
562            emit_expr(out, collection, mode)?;
563            out.push_str("[(");
564            emit_expr(out, lo, mode)?;
565            out.push_str(") as usize..(");
566            emit_expr(out, hi, mode)?;
567            out.push_str(") as usize]");
568            out.push_str(if *of_str { ".to_string()" } else { ".to_vec()" });
569        }
570        // PMAT-498: scalar numeric builtins → receiver-method form.
571        Expr::NumBuiltin { op, args } => {
572            out.push('(');
573            emit_expr(out, &args[0], mode)?;
574            out.push(')');
575            match op {
576                NumBuiltinOp::Abs => out.push_str(".abs()"),
577                NumBuiltinOp::Min | NumBuiltinOp::Max => {
578                    out.push_str(if matches!(op, NumBuiltinOp::Min) {
579                        ".min("
580                    } else {
581                        ".max("
582                    });
583                    emit_expr(out, &args[1], mode)?;
584                    out.push(')');
585                }
586            }
587        }
588        // PMAT-498b: `sum(xs)` → `<list>.iter().sum::<T>()`.
589        Expr::Sum { list, of_float } => {
590            emit_expr(out, list, mode)?;
591            out.push_str(if *of_float {
592                ".iter().sum::<f64>()"
593            } else {
594                ".iter().sum::<i64>()"
595            });
596        }
597        // PMAT-502e: 1-arg `min(xs)`/`max(xs)` reduction over an int list.
598        Expr::ListMinMax { list, is_max } => {
599            emit_expr(out, list, mode)?;
600            out.push_str(if *is_max {
601                ".iter().copied().max().unwrap()"
602            } else {
603                ".iter().copied().min().unwrap()"
604            });
605        }
606        // PMAT-502c: `sorted(xs)` → clone+sort block.
607        Expr::Sorted { list } => {
608            out.push_str("{ let mut __xv = ");
609            emit_expr(out, list, mode)?;
610            out.push_str(".clone(); __xv.sort(); __xv }");
611        }
612        // PMAT-502d: `reversed(xs)` → a new reversed Vec.
613        Expr::Reversed { list } => {
614            out.push_str("{ let mut __xv = ");
615            emit_expr(out, list, mode)?;
616            out.push_str(".clone(); __xv.reverse(); __xv }");
617        }
618        // PMAT-462 (v0.2.0 Track 1.C): Ruchy → Rust HashMap-init block.
619        // PMAT-466: empty literal → bare `HashMap::new()` (see the Rust
620        // backend's twin arm — avoids clippy `unused_mut`).
621        Expr::DictLit(pairs) => {
622            if pairs.is_empty() {
623                out.push_str("std::collections::HashMap::new()");
624            } else {
625                out.push_str("{ let mut m = std::collections::HashMap::new(); ");
626                for (k, v) in pairs {
627                    out.push_str("m.insert(");
628                    emit_expr(out, k, mode)?;
629                    out.push_str(", ");
630                    emit_expr(out, v, mode)?;
631                    out.push_str("); ");
632                }
633                out.push_str("m }");
634            }
635        }
636        // PMAT-457 (v0.2.0 Track 1.B): Ruchy → Rust →
637        // `xs[i as usize].clone()`, matching the Rust backend.
638        Expr::Index { collection, index } => {
639            emit_expr(out, collection, mode)?;
640            out.push('[');
641            emit_expr(out, index, mode)?;
642            out.push_str(" as usize].clone()");
643        }
644        // PMAT-466 (v0.2.0 Track 1.C): dict ops → Rust, matching the
645        // Rust backend exactly (Ruchy compiles to Rust).
646        Expr::DictGet { dict, key } => {
647            emit_expr(out, dict, mode)?;
648            out.push_str("[&(");
649            emit_expr(out, key, mode)?;
650            out.push_str(")].clone()");
651        }
652        Expr::DictGetOr { dict, key, default } => {
653            emit_expr(out, dict, mode)?;
654            out.push_str(".get(&(");
655            emit_expr(out, key, mode)?;
656            out.push_str(")).cloned().unwrap_or(");
657            emit_expr(out, default, mode)?;
658            out.push(')');
659        }
660        Expr::DictContains { dict, key } => {
661            emit_expr(out, dict, mode)?;
662            out.push_str(".contains_key(&(");
663            emit_expr(out, key, mode)?;
664            out.push_str("))");
665        }
666        // PMAT-500/501b: Ruchy → Rust set literal (empty → bare new()).
667        Expr::SetLit(elems) => {
668            if elems.is_empty() {
669                out.push_str("std::collections::HashSet::new()");
670            } else {
671                out.push_str("{ let mut __xset = std::collections::HashSet::new(); ");
672                for e in elems {
673                    out.push_str("__xset.insert(");
674                    emit_expr(out, e, mode)?;
675                    out.push_str("); ");
676                }
677                out.push_str("__xset }");
678            }
679        }
680        Expr::SetContains { set, elem } => {
681            emit_expr(out, set, mode)?;
682            out.push_str(".contains(&(");
683            emit_expr(out, elem, mode)?;
684            out.push_str("))");
685        }
686        // PMAT-459 (v0.2.0 Track 1.B): Ruchy → Rust → `.len() as i64`.
687        Expr::Len(inner) => {
688            emit_expr(out, inner, mode)?;
689            out.push_str(".len() as i64");
690        }
691        Expr::IfExpr {
692            cond,
693            then_expr,
694            else_expr,
695        } => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
696        Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
697        Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
698        // PMAT-449 (v0.2.0 Track 1.A): Python `str` literal → Ruchy
699        // owned `String::from("...")`. Same escape semantics as the
700        // Rust backend.
701        Expr::LitStr(s) => {
702            write!(out, "String::from(\"{}\")", escape_ruchy_str(s))?;
703        }
704        // PMAT-042: `QuotedString` carries explicit shell quoting and
705        // stays bashrs-only.
706        Expr::QuotedString { .. } => {
707            return Err(RuchyCodegenError::Unsupported(
708                "Ruchy backend does not lower Expr::QuotedString — \
709                 contract C-BASHRS-POSIX-IDEMPOTENCE governs quoted shell strings; \
710                 use `--target shell`"
711                    .into(),
712            ));
713        }
714        // PMAT-045: see rust-codegen's matching arm.
715        Expr::ShellVar(name) => {
716            return Err(RuchyCodegenError::Unsupported(format!(
717                "Ruchy backend does not lower Expr::ShellVar (${name}) — \
718                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable refs; \
719                 use `--target shell`"
720            )));
721        }
722        // PMAT-047: see rust-codegen.
723        Expr::CommandSubstitution(_) => {
724            return Err(RuchyCodegenError::Unsupported(
725                "Ruchy backend does not lower Expr::CommandSubstitution — \
726                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
727                 use `--target shell`"
728                    .into(),
729            ));
730        }
731        // PMAT-055: see rust-codegen.
732        Expr::ShellSpecial(name) => {
733            return Err(RuchyCodegenError::Unsupported(format!(
734                "Ruchy backend does not lower Expr::ShellSpecial (${name}) — \
735                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
736                 use `--target shell`"
737            )));
738        }
739    }
740    Ok(())
741}
742
743fn emit_unop(
744    out: &mut String,
745    op: UnOp,
746    operand: &Expr,
747    mode: bool,
748) -> Result<(), RuchyCodegenError> {
749    match op {
750        UnOp::Neg => {
751            if mode {
752                // BigInt::neg is total — no overflow.
753                write!(out, "(-")?;
754                emit_expr(out, operand, mode)?;
755                write!(out, ")")?;
756            } else {
757                // Python: `-x` on int never overflows mathematically.
758                // Rust i64::MIN.checked_neg() == None — use checked_neg
759                // + panic pointing at C-PY-INT-ARITH slow path.
760                write!(out, "(")?;
761                emit_expr(out, operand, mode)?;
762                write!(
763                    out,
764                    ").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
765                )?;
766            }
767        }
768        UnOp::Not => {
769            write!(out, "(!")?;
770            emit_expr(out, operand, mode)?;
771            write!(out, ")")?;
772        }
773    }
774    Ok(())
775}
776
777fn emit_call(
778    out: &mut String,
779    callee: &str,
780    args: &[Expr],
781    mode: bool,
782) -> Result<(), RuchyCodegenError> {
783    write!(out, "{}(", callee)?;
784    for (i, a) in args.iter().enumerate() {
785        if i > 0 {
786            write!(out, ", ")?;
787        }
788        emit_expr(out, a, mode)?;
789    }
790    write!(out, ")")?;
791    Ok(())
792}
793
794/// Ruchy uses Rust-like `if cond { then } else { else_ }` as an expression.
795/// Flattens nested `else if` for readability (same pattern as the Rust backend).
796fn emit_if_expr(
797    out: &mut String,
798    cond: &Expr,
799    then_expr: &Expr,
800    else_expr: &Expr,
801    mode: bool,
802) -> Result<(), RuchyCodegenError> {
803    write!(out, "if ")?;
804    emit_expr(out, cond, mode)?;
805    write!(out, " {{ ")?;
806    emit_expr(out, then_expr, mode)?;
807    write!(out, " }} else ")?;
808    match else_expr {
809        Expr::IfExpr {
810            cond: c2,
811            then_expr: t2,
812            else_expr: e2,
813        } => emit_if_expr(out, c2, t2, e2, mode),
814        _ => {
815            write!(out, "{{ ")?;
816            emit_expr(out, else_expr, mode)?;
817            write!(out, " }}")?;
818            Ok(())
819        }
820    }
821}
822
823/// Arithmetic emits two shapes per the C-PY-INT-ARITH contract:
824///
825/// * i64 fast path: `.checked_*().expect("...")` with the slow-path
826///   panic message (no overflow → no panic).
827/// * BigInt slow path (mode=true): plain infix on BigInt operands
828///   (BigInt overloads `+ - * <= ...`); FloorDiv / Mod use
829///   `xpile_bigint::div_floor / mod_floor`; bitwise / shift / pow
830///   deferred (same scope as the Rust backend).
831///
832/// Mirrors the Rust backend's emission shape — Ruchy compiles to Rust
833/// so they share semantics. PMAT-025.
834fn emit_binop(
835    out: &mut String,
836    op: BinOp,
837    lhs: &Expr,
838    rhs: &Expr,
839    mode: bool,
840) -> Result<(), RuchyCodegenError> {
841    match op {
842        BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
843        BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
844        BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
845        BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
846        BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
847        // PMAT-026 / PMAT-013-FOLLOWUP — mirror of the Rust backend.
848        // See `xpile-rust-codegen/src/lib.rs` for the design rationale.
849        BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
850        BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
851        BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
852        BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
853        BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
854        BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
855        BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
856        BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
857        BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
858        BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
859        BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
860        BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
861        BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
862        BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
863        BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
864        BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
865        BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
866        BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
867        BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
868        BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
869        BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
870        BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
871        BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
872        BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
873        BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
874    }
875}
876
877/// BigInt-mode floor-div / mod via the helpers in xpile-bigint
878/// (num-bigint requires `Integer` trait + reference operands).
879/// PMAT-025; mirrors Rust backend.
880fn emit_bigint_floor_call(
881    out: &mut String,
882    method: &str,
883    lhs: &Expr,
884    rhs: &Expr,
885    mode: bool,
886) -> Result<(), RuchyCodegenError> {
887    write!(out, "xpile_bigint::{method}(&")?;
888    emit_expr(out, lhs, mode)?;
889    write!(out, ", &")?;
890    emit_expr(out, rhs, mode)?;
891    write!(out, ")")?;
892    Ok(())
893}
894
895fn emit_checked_pow(
896    out: &mut String,
897    lhs: &Expr,
898    rhs: &Expr,
899    mode: bool,
900) -> Result<(), RuchyCodegenError> {
901    write!(out, "(")?;
902    emit_expr(out, lhs, mode)?;
903    write!(out, ").checked_pow(u32::try_from(")?;
904    emit_expr(out, rhs, mode)?;
905    write!(
906        out,
907        ").expect(\"xpile: exponent out of range for u32 — Python returns Float for negative exponents which v0.1.0 cannot represent (contract C-PY-INT-ARITH)\")).expect(\"xpile: i64 power overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
908    )?;
909    Ok(())
910}
911
912fn emit_checked_shift(
913    out: &mut String,
914    lhs: &Expr,
915    method: &str,
916    rhs: &Expr,
917    op_name: &str,
918    mode: bool,
919) -> Result<(), RuchyCodegenError> {
920    write!(out, "(")?;
921    emit_expr(out, lhs, mode)?;
922    write!(out, ").{method}(u32::try_from(")?;
923    emit_expr(out, rhs, mode)?;
924    write!(
925        out,
926        ").expect(\"xpile: shift amount out of range for u32 (contract C-PY-INT-ARITH)\")).expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
927    )?;
928    Ok(())
929}
930
931fn emit_checked(
932    out: &mut String,
933    lhs: &Expr,
934    method: &str,
935    rhs: &Expr,
936    op_name: &str,
937    mode: bool,
938) -> Result<(), RuchyCodegenError> {
939    write!(out, "(")?;
940    emit_expr(out, lhs, mode)?;
941    write!(out, ").{method}(")?;
942    emit_expr(out, rhs, mode)?;
943    write!(
944        out,
945        ").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
946    )?;
947    Ok(())
948}
949
950fn emit_infix(
951    out: &mut String,
952    lhs: &Expr,
953    op: &str,
954    rhs: &Expr,
955    mode: bool,
956) -> Result<(), RuchyCodegenError> {
957    write!(out, "(")?;
958    emit_expr(out, lhs, mode)?;
959    out.push_str(op);
960    emit_expr(out, rhs, mode)?;
961    write!(out, ")")?;
962    Ok(())
963}
964
965pub struct RuchyBackend;
966
967impl Backend for RuchyBackend {
968    fn name(&self) -> &'static str {
969        "ruchy"
970    }
971
972    fn targets(&self) -> &[Target] {
973        &[Target::Ruchy]
974    }
975
976    fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
977        let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
978        Ok(Artifact {
979            primary,
980            sidecars: Vec::new(),
981            citations: Vec::new(),
982            quorum_status: QuorumStatus::Single {
983                emitter: "xpile-ruchy-codegen".to_string(),
984            },
985        })
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992    use xpile_meta_hir::{Module, SourceLang};
993
994    fn module_with(name: &str, items: Vec<Item>) -> Module {
995        Module {
996            name: name.into(),
997            source_lang: SourceLang::Python,
998            items,
999            ffi_boundaries: Vec::new(),
1000        }
1001    }
1002
1003    fn add_fn() -> Function {
1004        Function {
1005            name: "add".into(),
1006            params: vec![
1007                Param {
1008                    name: "a".into(),
1009                    ty: Type::I64,
1010                    mutable: false,
1011                },
1012                Param {
1013                    name: "b".into(),
1014                    ty: Type::I64,
1015                    mutable: false,
1016                },
1017            ],
1018            return_type: Type::I64,
1019            body: Block {
1020                stmts: vec![],
1021                trailing_return: Expr::BinOp {
1022                    op: BinOp::Add,
1023                    lhs: Box::new(Expr::Ident("a".into())),
1024                    rhs: Box::new(Expr::Ident("b".into())),
1025                },
1026            },
1027        }
1028    }
1029
1030    #[test]
1031    fn emits_fun_keyword_not_pub_fn() {
1032        let m = module_with("fixture", vec![Item::Function(add_fn())]);
1033        let ruchy = emit_module(&m).expect("emit ok");
1034        assert!(
1035            ruchy.contains("fun add("),
1036            "Ruchy uses `fun`, not `fn` or `pub fn`: got\n{}",
1037            ruchy
1038        );
1039        assert!(
1040            !ruchy.contains("pub fn"),
1041            "Ruchy emission must not produce `pub fn` (that's Rust)"
1042        );
1043        // Post PMAT-002: addition lowers to checked_add (Ruchy compiles
1044        // to Rust, so it shares Rust's overflow semantics + contract
1045        // C-PY-INT-ARITH).
1046        assert!(
1047            ruchy.contains("checked_add"),
1048            "expected checked_add: {ruchy}"
1049        );
1050        assert!(ruchy.contains("C-PY-INT-ARITH"));
1051    }
1052
1053    #[test]
1054    fn ruchy_floordiv_also_uses_div_euclid() {
1055        let f = Function {
1056            name: "fdiv".into(),
1057            params: vec![
1058                Param {
1059                    name: "a".into(),
1060                    ty: Type::I64,
1061                    mutable: false,
1062                },
1063                Param {
1064                    name: "b".into(),
1065                    ty: Type::I64,
1066                    mutable: false,
1067                },
1068            ],
1069            return_type: Type::I64,
1070            body: Block {
1071                stmts: vec![],
1072                trailing_return: Expr::BinOp {
1073                    op: BinOp::FloorDiv,
1074                    lhs: Box::new(Expr::Ident("a".into())),
1075                    rhs: Box::new(Expr::Ident("b".into())),
1076                },
1077            },
1078        };
1079        let m = module_with("fixture", vec![Item::Function(f)]);
1080        let ruchy = emit_module(&m).expect("emit ok");
1081        assert!(ruchy.contains("div_euclid"));
1082        assert!(!ruchy.contains(" / "));
1083    }
1084}