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::{BinOp, Block, Expr, Function, Item, Module, Param, Stmt, Type, UnOp};
16
17#[derive(Debug, thiserror::Error)]
18pub enum RuchyCodegenError {
19    #[error("unsupported item: {0}")]
20    Unsupported(String),
21    #[error("formatting error: {0}")]
22    Format(#[from] std::fmt::Error),
23}
24
25pub fn emit_module(module: &Module) -> Result<String, RuchyCodegenError> {
26    let mut out = String::new();
27    writeln!(
28        out,
29        "// xpile-generated from {:?} module {}",
30        module.source_lang, module.name
31    )?;
32    writeln!(out)?;
33    for item in &module.items {
34        match item {
35            Item::Function(f) => emit_function(&mut out, f)?,
36        }
37    }
38    Ok(out)
39}
40
41fn emit_function(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
42    emit_contract_citations(out, f)?;
43    // Ruchy: `fun name(params) -> ret { body }`. No `pub`.
44    write!(out, "fun {}(", f.name)?;
45    for (i, p) in f.params.iter().enumerate() {
46        if i > 0 {
47            write!(out, ", ")?;
48        }
49        emit_param(out, p)?;
50    }
51    write!(out, ") -> ")?;
52    emit_type(out, &f.return_type)?;
53    writeln!(out, " {{")?;
54    let mode = function_bigint_mode(f);
55    emit_block(out, &f.body, mode)?;
56    writeln!(out, "}}")?;
57    Ok(())
58}
59
60/// PMAT-012-FOLLOWUP / PMAT-025: a function is in BigInt mode if any
61/// param is BigInt, the return type is BigInt, OR any pre-bound Let
62/// is BigInt. In BigInt mode, the Ruchy backend emits the same shape
63/// as the Rust backend (since Ruchy compiles to Rust):
64/// `xpile_bigint::BigInt::from(<n>i64)` literals + plain infix
65/// arithmetic + `.clone()` on Ident references (BigInt isn't `Copy`).
66fn function_bigint_mode(f: &Function) -> bool {
67    if matches!(f.return_type, Type::BigInt) {
68        return true;
69    }
70    if f.params.iter().any(|p| matches!(p.ty, Type::BigInt)) {
71        return true;
72    }
73    fn stmt_has_bigint(s: &Stmt) -> bool {
74        match s {
75            Stmt::Let { ty, .. } => matches!(ty, Type::BigInt),
76            Stmt::Assign { .. } | Stmt::Assert { .. } => false,
77            Stmt::While { body, .. } | Stmt::ForEach { body, .. } => {
78                body.iter().any(stmt_has_bigint)
79            }
80            // PMAT-460: list.append() — same disposition.
81            Stmt::ListAppend { .. } => false,
82            // PMAT-461: indexed assignment same disposition.
83            Stmt::IndexAssign { .. } => false,
84            // PMAT-039: see rust-codegen's twin arm — shell commands
85            // carry no BigInt operands.
86            Stmt::Cmd { .. } => false,
87            // PMAT-041: see rust-codegen's twin arm.
88            Stmt::Pipeline { .. } => false,
89            // PMAT-048: see rust-codegen's twin arm.
90            Stmt::ShellLoop { .. } => false,
91            // PMAT-051: see rust-codegen's twin arm.
92            Stmt::ShellAssign { .. } => false,
93        }
94    }
95    f.body.stmts.iter().any(stmt_has_bigint)
96}
97
98/// PMAT-011: same `// xpile-contract: <ID>` form as the Rust backend.
99/// Ruchy compiles to Rust, so it shares the comment-citation convention.
100fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
101    for id in f.applicable_contracts() {
102        writeln!(out, "// xpile-contract: {id}")?;
103    }
104    Ok(())
105}
106
107fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), RuchyCodegenError> {
108    for stmt in &block.stmts {
109        emit_stmt(out, stmt, mode)?;
110    }
111    write!(out, "    ")?;
112    emit_expr(out, &block.trailing_return, mode)?;
113    writeln!(out)?;
114    Ok(())
115}
116
117fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), RuchyCodegenError> {
118    emit_stmt_indented(out, stmt, "    ", mode)
119}
120
121fn emit_stmt_indented(
122    out: &mut String,
123    stmt: &Stmt,
124    indent: &str,
125    mode: bool,
126) -> Result<(), RuchyCodegenError> {
127    match stmt {
128        Stmt::Let {
129            name,
130            ty,
131            value,
132            mutable,
133        } => {
134            let kw = if *mutable { "let mut" } else { "let" };
135            write!(out, "{indent}{kw} {name}: ")?;
136            emit_type(out, ty)?;
137            write!(out, " = ")?;
138            emit_expr(out, value, mode)?;
139            writeln!(out, ";")?;
140            Ok(())
141        }
142        Stmt::Assign { name, value } => {
143            write!(out, "{indent}{name} = ")?;
144            emit_expr(out, value, mode)?;
145            writeln!(out, ";")?;
146            Ok(())
147        }
148        Stmt::While { cond, body } => {
149            write!(out, "{indent}while ")?;
150            emit_expr(out, cond, mode)?;
151            writeln!(out, " {{")?;
152            let inner = format!("{indent}    ");
153            for s in body {
154                emit_stmt_indented(out, s, &inner, mode)?;
155            }
156            writeln!(out, "{indent}}}")?;
157            Ok(())
158        }
159        // PMAT-458 (v0.2.0 Track 1.B): Ruchy → Rust → for-each with
160        // .iter().cloned() for owned-value bindings.
161        Stmt::ForEach {
162            var, iter, body, ..
163        } => {
164            write!(out, "{indent}for {var} in ")?;
165            emit_expr(out, iter, mode)?;
166            writeln!(out, ".iter().cloned() {{")?;
167            let inner = format!("{indent}    ");
168            for s in body {
169                emit_stmt_indented(out, s, &inner, mode)?;
170            }
171            writeln!(out, "{indent}}}")?;
172            Ok(())
173        }
174        // PMAT-460 (v0.2.0 Track 1.B): Ruchy → Rust → `.push(...)`.
175        Stmt::ListAppend { list_name, elem } => {
176            write!(out, "{indent}{list_name}.push(")?;
177            emit_expr(out, elem, mode)?;
178            writeln!(out, ");")?;
179            Ok(())
180        }
181        // PMAT-461 (v0.2.0 Track 1.B): Ruchy → Rust →
182        // `xs[i as usize] = v;`, matching the Rust backend.
183        Stmt::IndexAssign {
184            list_name,
185            index,
186            value,
187        } => {
188            write!(out, "{indent}{list_name}[")?;
189            emit_expr(out, index, mode)?;
190            out.push_str(" as usize] = ");
191            emit_expr(out, value, mode)?;
192            writeln!(out, ";")?;
193            Ok(())
194        }
195        Stmt::Assert { cond } => {
196            write!(out, "{indent}assert!(")?;
197            emit_expr(out, cond, mode)?;
198            writeln!(out, ");")?;
199            Ok(())
200        }
201        // PMAT-039 / XPILE-BASHRS-MERGER-001 Layer B: see rust-codegen's
202        // matching arm. Ruchy compiles to Rust and inherits Rust's
203        // disposition — no Ruchy-level translation of `Stmt::Cmd`
204        // exists.
205        Stmt::Cmd { program, args } => Err(RuchyCodegenError::Unsupported(format!(
206            "Ruchy backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
207             contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
208             use `--target shell` to emit POSIX sh via bashrs-backend",
209            args.len()
210        ))),
211        // PMAT-041: same disposition as Cmd.
212        Stmt::Pipeline { stages } => Err(RuchyCodegenError::Unsupported(format!(
213            "Ruchy backend does not lower Stmt::Pipeline ({} stages) — \
214             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
215             use `--target shell`",
216            stages.len()
217        ))),
218        // PMAT-048: same disposition.
219        Stmt::ShellLoop { .. } => Err(RuchyCodegenError::Unsupported(
220            "Ruchy backend does not lower Stmt::ShellLoop — \
221             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
222             use `--target shell`"
223                .into(),
224        )),
225        // PMAT-051: same disposition.
226        Stmt::ShellAssign { name, .. } => Err(RuchyCodegenError::Unsupported(format!(
227            "Ruchy backend does not lower Stmt::ShellAssign (`{name}=…`) — \
228             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
229             use `--target shell`"
230        ))),
231    }
232}
233
234fn emit_param(out: &mut String, p: &Param) -> Result<(), RuchyCodegenError> {
235    // PMAT-460: same posture as the Rust backend.
236    if p.mutable {
237        write!(out, "mut ")?;
238    }
239    write!(out, "{}: ", p.name)?;
240    emit_type(out, &p.ty)?;
241    Ok(())
242}
243
244/// Escape a string for emission inside a Ruchy `"..."` literal.
245/// PMAT-449 — Ruchy compiles to Rust, so identical escape semantics.
246fn escape_ruchy_str(s: &str) -> String {
247    let mut out = String::with_capacity(s.len());
248    for c in s.chars() {
249        match c {
250            '\\' => out.push_str("\\\\"),
251            '"' => out.push_str("\\\""),
252            other => out.push(other),
253        }
254    }
255    out
256}
257
258fn emit_type(out: &mut String, t: &Type) -> Result<(), RuchyCodegenError> {
259    match t {
260        Type::I64 => out.push_str("i64"),
261        Type::Bool => out.push_str("bool"),
262        // Ruchy compiles to Rust → same BigInt re-export. PMAT-012.
263        Type::BigInt => out.push_str("xpile_bigint::BigInt"),
264        // PMAT-449 (v0.2.0 Track 1.A): Ruchy → Rust → owned `String`,
265        // mirrors xpile-rust-codegen's lowering.
266        Type::Str => out.push_str("String"),
267        // PMAT-455 (v0.2.0 Track 1.B): Ruchy → Rust Vec<T>.
268        Type::List(elem_ty) => {
269            out.push_str("Vec<");
270            emit_type(out, elem_ty)?;
271            out.push('>');
272        }
273        // PMAT-462 (v0.2.0 Track 1.C): Ruchy → Rust HashMap<K, V>.
274        Type::Dict(k_ty, v_ty) => {
275            out.push_str("std::collections::HashMap<");
276            emit_type(out, k_ty)?;
277            out.push_str(", ");
278            emit_type(out, v_ty)?;
279            out.push('>');
280        }
281        // PMAT-046: same disposition as the Rust backend.
282        Type::ShellString | Type::ExitCode => {
283            return Err(RuchyCodegenError::Unsupported(format!(
284                "Ruchy backend does not lower {t:?} — \
285                 contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
286                 use `--target shell`"
287            )));
288        }
289    }
290    Ok(())
291}
292
293fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), RuchyCodegenError> {
294    match e {
295        Expr::Ident(name) => {
296            // PMAT-025: in BigInt mode, append `.clone()` to every
297            // Ident reference. BigInt isn't `Copy` (it's
298            // heap-allocated), so a name referenced in cond +
299            // branches + recursive call would move-on-first-use.
300            // Mirrors the Rust backend's PMAT-013 emission.
301            if mode {
302                write!(out, "{}.clone()", name)?;
303            } else {
304                write!(out, "{}", name)?;
305            }
306        }
307        Expr::LitInt(v) => {
308            if mode {
309                write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
310            } else {
311                write!(out, "{}i64", v)?;
312            }
313        }
314        // PMAT-456 (v0.2.0 Track 1.B): Ruchy → Rust → lowercase
315        // `true` / `false`.
316        Expr::LitBool(b) => write!(out, "{}", b)?,
317        Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
318        // PMAT-451 (v0.2.0 Track 1.A): same str-concat shape as the
319        // Rust backend — Ruchy compiles to Rust, so `format!()` works
320        // identically.
321        Expr::Concat { lhs, rhs } => {
322            out.push_str("format!(\"{}{}\", ");
323            emit_expr(out, lhs, mode)?;
324            out.push_str(", ");
325            emit_expr(out, rhs, mode)?;
326            out.push(')');
327        }
328        // PMAT-455 (v0.2.0 Track 1.B): Ruchy → Rust → `vec![...]`.
329        Expr::ListLit(elems) => {
330            out.push_str("vec![");
331            for (i, e) in elems.iter().enumerate() {
332                if i > 0 {
333                    out.push_str(", ");
334                }
335                emit_expr(out, e, mode)?;
336            }
337            out.push(']');
338        }
339        // PMAT-462 (v0.2.0 Track 1.C): Ruchy → Rust HashMap-init block.
340        Expr::DictLit(pairs) => {
341            out.push_str("{ let mut m = std::collections::HashMap::new(); ");
342            for (k, v) in pairs {
343                out.push_str("m.insert(");
344                emit_expr(out, k, mode)?;
345                out.push_str(", ");
346                emit_expr(out, v, mode)?;
347                out.push_str("); ");
348            }
349            out.push_str("m }");
350        }
351        // PMAT-457 (v0.2.0 Track 1.B): Ruchy → Rust →
352        // `xs[i as usize].clone()`, matching the Rust backend.
353        Expr::Index { collection, index } => {
354            emit_expr(out, collection, mode)?;
355            out.push('[');
356            emit_expr(out, index, mode)?;
357            out.push_str(" as usize].clone()");
358        }
359        // PMAT-459 (v0.2.0 Track 1.B): Ruchy → Rust → `.len() as i64`.
360        Expr::Len(inner) => {
361            emit_expr(out, inner, mode)?;
362            out.push_str(".len() as i64");
363        }
364        Expr::IfExpr {
365            cond,
366            then_expr,
367            else_expr,
368        } => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
369        Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
370        Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
371        // PMAT-449 (v0.2.0 Track 1.A): Python `str` literal → Ruchy
372        // owned `String::from("...")`. Same escape semantics as the
373        // Rust backend.
374        Expr::LitStr(s) => {
375            write!(out, "String::from(\"{}\")", escape_ruchy_str(s))?;
376        }
377        // PMAT-042: `QuotedString` carries explicit shell quoting and
378        // stays bashrs-only.
379        Expr::QuotedString { .. } => {
380            return Err(RuchyCodegenError::Unsupported(
381                "Ruchy backend does not lower Expr::QuotedString — \
382                 contract C-BASHRS-POSIX-IDEMPOTENCE governs quoted shell strings; \
383                 use `--target shell`"
384                    .into(),
385            ));
386        }
387        // PMAT-045: see rust-codegen's matching arm.
388        Expr::ShellVar(name) => {
389            return Err(RuchyCodegenError::Unsupported(format!(
390                "Ruchy backend does not lower Expr::ShellVar (${name}) — \
391                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable refs; \
392                 use `--target shell`"
393            )));
394        }
395        // PMAT-047: see rust-codegen.
396        Expr::CommandSubstitution(_) => {
397            return Err(RuchyCodegenError::Unsupported(
398                "Ruchy backend does not lower Expr::CommandSubstitution — \
399                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
400                 use `--target shell`"
401                    .into(),
402            ));
403        }
404        // PMAT-055: see rust-codegen.
405        Expr::ShellSpecial(name) => {
406            return Err(RuchyCodegenError::Unsupported(format!(
407                "Ruchy backend does not lower Expr::ShellSpecial (${name}) — \
408                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
409                 use `--target shell`"
410            )));
411        }
412    }
413    Ok(())
414}
415
416fn emit_unop(
417    out: &mut String,
418    op: UnOp,
419    operand: &Expr,
420    mode: bool,
421) -> Result<(), RuchyCodegenError> {
422    match op {
423        UnOp::Neg => {
424            if mode {
425                // BigInt::neg is total — no overflow.
426                write!(out, "(-")?;
427                emit_expr(out, operand, mode)?;
428                write!(out, ")")?;
429            } else {
430                // Python: `-x` on int never overflows mathematically.
431                // Rust i64::MIN.checked_neg() == None — use checked_neg
432                // + panic pointing at C-PY-INT-ARITH slow path.
433                write!(out, "(")?;
434                emit_expr(out, operand, mode)?;
435                write!(
436                    out,
437                    ").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
438                )?;
439            }
440        }
441        UnOp::Not => {
442            write!(out, "(!")?;
443            emit_expr(out, operand, mode)?;
444            write!(out, ")")?;
445        }
446    }
447    Ok(())
448}
449
450fn emit_call(
451    out: &mut String,
452    callee: &str,
453    args: &[Expr],
454    mode: bool,
455) -> Result<(), RuchyCodegenError> {
456    write!(out, "{}(", callee)?;
457    for (i, a) in args.iter().enumerate() {
458        if i > 0 {
459            write!(out, ", ")?;
460        }
461        emit_expr(out, a, mode)?;
462    }
463    write!(out, ")")?;
464    Ok(())
465}
466
467/// Ruchy uses Rust-like `if cond { then } else { else_ }` as an expression.
468/// Flattens nested `else if` for readability (same pattern as the Rust backend).
469fn emit_if_expr(
470    out: &mut String,
471    cond: &Expr,
472    then_expr: &Expr,
473    else_expr: &Expr,
474    mode: bool,
475) -> Result<(), RuchyCodegenError> {
476    write!(out, "if ")?;
477    emit_expr(out, cond, mode)?;
478    write!(out, " {{ ")?;
479    emit_expr(out, then_expr, mode)?;
480    write!(out, " }} else ")?;
481    match else_expr {
482        Expr::IfExpr {
483            cond: c2,
484            then_expr: t2,
485            else_expr: e2,
486        } => emit_if_expr(out, c2, t2, e2, mode),
487        _ => {
488            write!(out, "{{ ")?;
489            emit_expr(out, else_expr, mode)?;
490            write!(out, " }}")?;
491            Ok(())
492        }
493    }
494}
495
496/// Arithmetic emits two shapes per the C-PY-INT-ARITH contract:
497///
498/// * i64 fast path: `.checked_*().expect("...")` with the slow-path
499///   panic message (no overflow → no panic).
500/// * BigInt slow path (mode=true): plain infix on BigInt operands
501///   (BigInt overloads `+ - * <= ...`); FloorDiv / Mod use
502///   `xpile_bigint::div_floor / mod_floor`; bitwise / shift / pow
503///   deferred (same scope as the Rust backend).
504///
505/// Mirrors the Rust backend's emission shape — Ruchy compiles to Rust
506/// so they share semantics. PMAT-025.
507fn emit_binop(
508    out: &mut String,
509    op: BinOp,
510    lhs: &Expr,
511    rhs: &Expr,
512    mode: bool,
513) -> Result<(), RuchyCodegenError> {
514    match op {
515        BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
516        BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
517        BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
518        BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
519        BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
520        // PMAT-026 / PMAT-013-FOLLOWUP — mirror of the Rust backend.
521        // See `xpile-rust-codegen/src/lib.rs` for the design rationale.
522        BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
523        BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
524        BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
525        BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
526        BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
527        BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
528        BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
529        BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
530        BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
531        BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
532        BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
533        BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
534        BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
535        BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
536        BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
537        BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
538        BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
539        BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
540        BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
541        BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
542        BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
543        BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
544        BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
545        BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
546        BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
547    }
548}
549
550/// BigInt-mode floor-div / mod via the helpers in xpile-bigint
551/// (num-bigint requires `Integer` trait + reference operands).
552/// PMAT-025; mirrors Rust backend.
553fn emit_bigint_floor_call(
554    out: &mut String,
555    method: &str,
556    lhs: &Expr,
557    rhs: &Expr,
558    mode: bool,
559) -> Result<(), RuchyCodegenError> {
560    write!(out, "xpile_bigint::{method}(&")?;
561    emit_expr(out, lhs, mode)?;
562    write!(out, ", &")?;
563    emit_expr(out, rhs, mode)?;
564    write!(out, ")")?;
565    Ok(())
566}
567
568fn emit_checked_pow(
569    out: &mut String,
570    lhs: &Expr,
571    rhs: &Expr,
572    mode: bool,
573) -> Result<(), RuchyCodegenError> {
574    write!(out, "(")?;
575    emit_expr(out, lhs, mode)?;
576    write!(out, ").checked_pow(u32::try_from(")?;
577    emit_expr(out, rhs, mode)?;
578    write!(
579        out,
580        ").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\")"
581    )?;
582    Ok(())
583}
584
585fn emit_checked_shift(
586    out: &mut String,
587    lhs: &Expr,
588    method: &str,
589    rhs: &Expr,
590    op_name: &str,
591    mode: bool,
592) -> Result<(), RuchyCodegenError> {
593    write!(out, "(")?;
594    emit_expr(out, lhs, mode)?;
595    write!(out, ").{method}(u32::try_from(")?;
596    emit_expr(out, rhs, mode)?;
597    write!(
598        out,
599        ").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\")"
600    )?;
601    Ok(())
602}
603
604fn emit_checked(
605    out: &mut String,
606    lhs: &Expr,
607    method: &str,
608    rhs: &Expr,
609    op_name: &str,
610    mode: bool,
611) -> Result<(), RuchyCodegenError> {
612    write!(out, "(")?;
613    emit_expr(out, lhs, mode)?;
614    write!(out, ").{method}(")?;
615    emit_expr(out, rhs, mode)?;
616    write!(
617        out,
618        ").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
619    )?;
620    Ok(())
621}
622
623fn emit_infix(
624    out: &mut String,
625    lhs: &Expr,
626    op: &str,
627    rhs: &Expr,
628    mode: bool,
629) -> Result<(), RuchyCodegenError> {
630    write!(out, "(")?;
631    emit_expr(out, lhs, mode)?;
632    out.push_str(op);
633    emit_expr(out, rhs, mode)?;
634    write!(out, ")")?;
635    Ok(())
636}
637
638pub struct RuchyBackend;
639
640impl Backend for RuchyBackend {
641    fn name(&self) -> &'static str {
642        "ruchy"
643    }
644
645    fn targets(&self) -> &[Target] {
646        &[Target::Ruchy]
647    }
648
649    fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
650        let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
651        Ok(Artifact {
652            primary,
653            sidecars: Vec::new(),
654            citations: Vec::new(),
655            quorum_status: QuorumStatus::Single {
656                emitter: "xpile-ruchy-codegen".to_string(),
657            },
658        })
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use xpile_meta_hir::{Module, SourceLang};
666
667    fn module_with(name: &str, items: Vec<Item>) -> Module {
668        Module {
669            name: name.into(),
670            source_lang: SourceLang::Python,
671            items,
672            ffi_boundaries: Vec::new(),
673        }
674    }
675
676    fn add_fn() -> Function {
677        Function {
678            name: "add".into(),
679            params: vec![
680                Param {
681                    name: "a".into(),
682                    ty: Type::I64,
683                    mutable: false,
684                },
685                Param {
686                    name: "b".into(),
687                    ty: Type::I64,
688                    mutable: false,
689                },
690            ],
691            return_type: Type::I64,
692            body: Block {
693                stmts: vec![],
694                trailing_return: Expr::BinOp {
695                    op: BinOp::Add,
696                    lhs: Box::new(Expr::Ident("a".into())),
697                    rhs: Box::new(Expr::Ident("b".into())),
698                },
699            },
700        }
701    }
702
703    #[test]
704    fn emits_fun_keyword_not_pub_fn() {
705        let m = module_with("fixture", vec![Item::Function(add_fn())]);
706        let ruchy = emit_module(&m).expect("emit ok");
707        assert!(
708            ruchy.contains("fun add("),
709            "Ruchy uses `fun`, not `fn` or `pub fn`: got\n{}",
710            ruchy
711        );
712        assert!(
713            !ruchy.contains("pub fn"),
714            "Ruchy emission must not produce `pub fn` (that's Rust)"
715        );
716        // Post PMAT-002: addition lowers to checked_add (Ruchy compiles
717        // to Rust, so it shares Rust's overflow semantics + contract
718        // C-PY-INT-ARITH).
719        assert!(
720            ruchy.contains("checked_add"),
721            "expected checked_add: {ruchy}"
722        );
723        assert!(ruchy.contains("C-PY-INT-ARITH"));
724    }
725
726    #[test]
727    fn ruchy_floordiv_also_uses_div_euclid() {
728        let f = Function {
729            name: "fdiv".into(),
730            params: vec![
731                Param {
732                    name: "a".into(),
733                    ty: Type::I64,
734                    mutable: false,
735                },
736                Param {
737                    name: "b".into(),
738                    ty: Type::I64,
739                    mutable: false,
740                },
741            ],
742            return_type: Type::I64,
743            body: Block {
744                stmts: vec![],
745                trailing_return: Expr::BinOp {
746                    op: BinOp::FloorDiv,
747                    lhs: Box::new(Expr::Ident("a".into())),
748                    rhs: Box::new(Expr::Ident("b".into())),
749                },
750            },
751        };
752        let m = module_with("fixture", vec![Item::Function(f)]);
753        let ruchy = emit_module(&m).expect("emit ok");
754        assert!(ruchy.contains("div_euclid"));
755        assert!(!ruchy.contains(" / "));
756    }
757}