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