Skip to main content

xpile_rust_codegen/
lib.rs

1//! Shared Rust emission.
2//!
3//! Takes meta-HIR as input, emits idiomatic Rust. Language-neutral by
4//! design — language-specific quirks (Python's int promotion, C's
5//! pointer arithmetic, Ruchy's pipeline operator) are normalized in
6//! each frontend before reaching codegen.
7//!
8//! Exposes both:
9//!   * [`emit_module`] — free function, kept stable for callers that
10//!     don't want to go through the [`Backend`] trait.
11//!   * [`RustBackend`] — a [`Backend`] impl that wraps [`emit_module`]
12//!     so Rust dispatches through the same trait as PTX / WGSL / Lean.
13
14use std::fmt::Write;
15use xpile_backend::{Artifact, Backend, BackendConfig, BackendError, QuorumStatus, Target};
16use xpile_meta_hir::{
17    BinOp, Block, Expr, FloatOp, Function, Item, Module, NumBuiltinOp, Param, SourceLang, Stmt,
18    StrMethodOp, Type, UnOp,
19};
20
21/// PMAT-477 (R8): the Rust infix symbol for a float arithmetic op.
22fn float_op_sym(op: FloatOp) -> &'static str {
23    match op {
24        FloatOp::Add => "+",
25        FloatOp::Sub => "-",
26        FloatOp::Mul => "*",
27        FloatOp::Div => "/",
28    }
29}
30
31#[derive(Debug, thiserror::Error)]
32pub enum CodegenError {
33    #[error("unsupported item: {0}")]
34    Unsupported(String),
35    #[error("formatting error: {0}")]
36    Format(#[from] std::fmt::Error),
37}
38
39pub fn emit_module(module: &Module) -> Result<String, CodegenError> {
40    let mut out = String::new();
41    writeln!(
42        out,
43        "// xpile-generated from {:?} module {}",
44        module.source_lang, module.name
45    )?;
46    writeln!(out)?;
47    // PMAT-467 (v0.2.0 Track 2.A): C sources lower with C arithmetic
48    // semantics (fixed-width `i32`, wrapping overflow) via an isolated
49    // emit path, keeping the Python/Ruchy codegen (i64 + checked /
50    // bigint) untouched. Governed by `C-C-INT-ARITH` (substrate queued).
51    let is_c = matches!(module.source_lang, SourceLang::C);
52    for item in &module.items {
53        match item {
54            Item::Function(f) => {
55                if is_c {
56                    emit_c_function(&mut out, f)?;
57                } else {
58                    emit_function(&mut out, f)?;
59                }
60            }
61        }
62    }
63    Ok(out)
64}
65
66fn emit_function(out: &mut String, f: &Function) -> Result<(), CodegenError> {
67    emit_contract_citations(out, f)?;
68    write!(out, "pub fn {}(", f.name)?;
69    for (i, p) in f.params.iter().enumerate() {
70        if i > 0 {
71            write!(out, ", ")?;
72        }
73        emit_param(out, p)?;
74    }
75    write!(out, ") -> ")?;
76    emit_type(out, &f.return_type)?;
77    writeln!(out, " {{")?;
78    let mode = function_bigint_mode(f);
79    emit_block(out, &f.body, mode)?;
80    writeln!(out, "}}")?;
81    Ok(())
82}
83
84/// PMAT-012: a function is in BigInt mode if any param is BigInt OR
85/// any pre-bound Let has type BigInt OR the return type is BigInt. In
86/// BigInt mode, the Rust backend emits `xpile_bigint::BigInt::from(...)`
87/// for integer literals and plain infix `+ - * <= ...` for arithmetic
88/// (BigInt never overflows, so no `.checked_*().expect(...)`).
89fn function_bigint_mode(f: &Function) -> bool {
90    if matches!(f.return_type, Type::BigInt) {
91        return true;
92    }
93    if f.params.iter().any(|p| matches!(p.ty, Type::BigInt)) {
94        return true;
95    }
96    fn stmt_has_bigint(s: &Stmt) -> bool {
97        match s {
98            Stmt::Let { ty, .. } => matches!(ty, Type::BigInt),
99            // PMAT-494b: tuple unpacking introduces no BigInt binding
100            // (tuples aren't BigInt-typed at first cut).
101            Stmt::LetTuple { .. } => false,
102            // PMAT-479 (R10): an early return introduces no BigInt
103            // binding (bigint mode is set by params/lets/return type).
104            // PMAT-503a: a raise introduces no BigInt binding.
105            Stmt::Assign { .. } | Stmt::Assert { .. } | Stmt::Return(_) | Stmt::Raise { .. } => {
106                false
107            }
108            Stmt::While { body, .. }
109            | Stmt::ForEach { body, .. }
110            | Stmt::ForEachPair { body, .. } => body.iter().any(stmt_has_bigint),
111            // PMAT-478 (R9): recurse both branches of an if/else.
112            Stmt::If {
113                then_body,
114                else_body,
115                ..
116            } => then_body.iter().any(stmt_has_bigint) || else_body.iter().any(stmt_has_bigint),
117            // PMAT-460: list.append() carries no Type::Let, so no
118            // BigInt-mode trigger of its own.
119            Stmt::ListAppend { .. } | Stmt::SetAdd { .. } => false,
120            // PMAT-461: indexed assignment same disposition.
121            Stmt::IndexAssign { .. } => false,
122            // PMAT-466: dict keyed assignment carries no Type::Let;
123            // dict values are int/bool/str at v0.2.0, never BigInt.
124            Stmt::DictSet { .. } => false,
125            // PMAT-039: shell commands carry no BigInt operands. They
126            // also never reach this Rust-codegen scan in practice
127            // (bashrs-frontend produces Shell modules that the Rust
128            // backend declines at emit_stmt), but exhaustive match
129            // keeps the dispatch boundary explicit.
130            Stmt::Cmd { .. } => false,
131            // PMAT-041: same disposition as Cmd — Pipeline composes
132            // Cmd stages; no BigInt operand reachable.
133            Stmt::Pipeline { .. } => false,
134            // PMAT-048: ShellLoop is bashrs-domain — no BigInt
135            // operand reachable through it.
136            Stmt::ShellLoop { .. } => false,
137            // PMAT-051: ShellAssign same disposition.
138            Stmt::ShellAssign { .. } => false,
139        }
140    }
141    f.body.stmts.iter().any(stmt_has_bigint)
142}
143
144/// PMAT-011: emit one `// xpile-contract: <ID>` comment line per
145/// contract that governs this function. Matches the mdBook convention
146/// from `sub/contract-frontend-trait.md`'s citation grid — same prefix
147/// across all text-comment hosts, so a single regex finds them all.
148/// Lean uses `@[xpile_contract "<ID>"]` (proper structured attribute);
149/// LaTeX uses `\xpileContract{<ID>}{...}`; mdBook + Rust + Ruchy share
150/// the comment form.
151fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), CodegenError> {
152    for id in f.applicable_contracts() {
153        writeln!(out, "// xpile-contract: {id}")?;
154    }
155    Ok(())
156}
157
158fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), CodegenError> {
159    for stmt in &block.stmts {
160        emit_stmt(out, stmt, mode)?;
161    }
162    write!(out, "    ")?;
163    emit_expr(out, &block.trailing_return, mode)?;
164    writeln!(out)?;
165    Ok(())
166}
167
168fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), CodegenError> {
169    emit_stmt_indented(out, stmt, "    ", mode)
170}
171
172fn emit_stmt_indented(
173    out: &mut String,
174    stmt: &Stmt,
175    indent: &str,
176    mode: bool,
177) -> Result<(), CodegenError> {
178    match stmt {
179        Stmt::Let {
180            name,
181            ty,
182            value,
183            mutable,
184        } => {
185            let kw = if *mutable { "let mut" } else { "let" };
186            write!(out, "{indent}{kw} {name}: ")?;
187            emit_type(out, ty)?;
188            write!(out, " = ")?;
189            emit_expr(out, value, mode)?;
190            writeln!(out, ";")?;
191            Ok(())
192        }
193        Stmt::Assign { name, value } => {
194            write!(out, "{indent}{name} = ")?;
195            emit_expr(out, value, mode)?;
196            writeln!(out, ";")?;
197            Ok(())
198        }
199        // PMAT-494b: tuple unpacking → `let (a, b, ...) = <value>;`.
200        Stmt::LetTuple { names, value } => {
201            write!(out, "{indent}let ({}) = ", names.join(", "))?;
202            emit_expr(out, value, mode)?;
203            writeln!(out, ";")?;
204            Ok(())
205        }
206        // PMAT-479 (R10): early `return <expr>;` (e.g. a guard clause).
207        Stmt::Return(e) => {
208            write!(out, "{indent}return ")?;
209            emit_expr(out, e, mode)?;
210            writeln!(out, ";")?;
211            Ok(())
212        }
213        // PMAT-478 (R9): if/else statement → Rust `if c { … } else { … }`.
214        // The `else` block is omitted when `else_body` is empty.
215        Stmt::If {
216            cond,
217            then_body,
218            else_body,
219        } => {
220            write!(out, "{indent}if ")?;
221            emit_expr(out, cond, mode)?;
222            writeln!(out, " {{")?;
223            let inner = format!("{indent}    ");
224            for s in then_body {
225                emit_stmt_indented(out, s, &inner, mode)?;
226            }
227            if else_body.is_empty() {
228                writeln!(out, "{indent}}}")?;
229            } else {
230                writeln!(out, "{indent}}} else {{")?;
231                for s in else_body {
232                    emit_stmt_indented(out, s, &inner, mode)?;
233                }
234                writeln!(out, "{indent}}}")?;
235            }
236            Ok(())
237        }
238        Stmt::While { cond, body } => {
239            write!(out, "{indent}while ")?;
240            emit_expr(out, cond, mode)?;
241            writeln!(out, " {{")?;
242            let inner = format!("{indent}    ");
243            for s in body {
244                emit_stmt_indented(out, s, &inner, mode)?;
245            }
246            writeln!(out, "{indent}}}")?;
247            Ok(())
248        }
249        // PMAT-458 (v0.2.0 Track 1.B): for-each over a collection.
250        // Emit `for var in iter.iter().cloned() { body }` — the
251        // .iter().cloned() produces owned elements matching the
252        // v0.2.0 owned-value posture (Index already returns .clone(),
253        // so the body sees owned values consistently).
254        Stmt::ForEach {
255            var,
256            iter,
257            body,
258            over_keys,
259            ..
260        } => {
261            // PMAT-472 (R3): a dict iterates keys (`for k in d:`) via
262            // `.keys().cloned()`; a list iterates elements via
263            // `.iter().cloned()`. Both yield owned values.
264            let method = if *over_keys { "keys" } else { "iter" };
265            write!(out, "{indent}for {var} in ")?;
266            emit_expr(out, iter, mode)?;
267            writeln!(out, ".{method}().cloned() {{")?;
268            let inner = format!("{indent}    ");
269            for s in body {
270                emit_stmt_indented(out, s, &inner, mode)?;
271            }
272            writeln!(out, "{indent}}}")?;
273            Ok(())
274        }
275        // PMAT-495: paired for-loop. enumerate → `(i as i64, e)`; zip →
276        // both iterators `.iter().cloned()`.
277        Stmt::ForEachPair {
278            first,
279            second,
280            iter,
281            kind,
282            body,
283        } => {
284            write!(out, "{indent}for ({first}, {second}) in ")?;
285            emit_expr(out, iter, mode)?;
286            match kind {
287                xpile_meta_hir::PairIterKind::Enumerate => {
288                    out.push_str(
289                        ".iter().cloned().enumerate().map(|(__i, __e)| (__i as i64, __e))",
290                    );
291                }
292                xpile_meta_hir::PairIterKind::Zip(other) => {
293                    out.push_str(".iter().cloned().zip(");
294                    emit_expr(out, other, mode)?;
295                    out.push_str(".iter().cloned())");
296                }
297            }
298            writeln!(out, " {{")?;
299            let inner = format!("{indent}    ");
300            for s in body {
301                emit_stmt_indented(out, s, &inner, mode)?;
302            }
303            writeln!(out, "{indent}}}")?;
304            Ok(())
305        }
306        // PMAT-460 (v0.2.0 Track 1.B): Python `xs.append(v)` → Rust
307        // `xs.push(v);`. The frontend has already marked `xs` as
308        // mutable so the emission type-checks.
309        Stmt::ListAppend { list_name, elem } => {
310            write!(out, "{indent}{list_name}.push(")?;
311            emit_expr(out, elem, mode)?;
312            writeln!(out, ");")?;
313            Ok(())
314        }
315        // PMAT-500b: Python `s.add(x)` → Rust `s.insert(x);`.
316        Stmt::SetAdd { set_name, elem } => {
317            write!(out, "{indent}{set_name}.insert(")?;
318            emit_expr(out, elem, mode)?;
319            writeln!(out, ");")?;
320            Ok(())
321        }
322        // PMAT-461 (v0.2.0 Track 1.B): Python `xs[i] = v` → Rust
323        // `xs[i as usize] = v;`. Same `as usize` coercion as
324        // Expr::Index; same param-mut threading as ListAppend.
325        Stmt::IndexAssign {
326            list_name,
327            index,
328            value,
329        } => {
330            write!(out, "{indent}{list_name}[")?;
331            emit_expr(out, index, mode)?;
332            out.push_str(" as usize] = ");
333            emit_expr(out, value, mode)?;
334            writeln!(out, ";")?;
335            Ok(())
336        }
337        // PMAT-466 (v0.2.0 Track 1.C): Python `d[k] = v` → Rust
338        // `{ let __v = v; d.insert(k.clone(), __v); }`. Present-key
339        // overwrite / absent-key insert matches Python dict assignment.
340        //
341        // Two subtleties, both about the move-then-borrow hazard of a
342        // non-Copy (`String`) key:
343        //   1. The value is bound to a temp BEFORE `.insert`, so the
344        //      canonical `d[k] = d.get(k, 0) + 1` idiom (value borrows
345        //      the key) doesn't move the key out from under its own
346        //      value expression (E0382). Binding the value first also
347        //      ends the immutable `.get` borrow before the mutable
348        //      `.insert` borrow (NLL).
349        //   2. The key is `.clone()`d into `.insert` so the caller's key
350        //      binding survives a *later* read of the same key (e.g.
351        //      `d[k] = …; return d[k]`). For Copy keys (int/bool) the
352        //      clone is a no-op move; `rustc` accepts it (the
353        //      `clone_on_copy` lint is clippy-only and xpile does not
354        //      clippy emitted output).
355        Stmt::DictSet {
356            dict_name,
357            key,
358            value,
359        } => {
360            write!(out, "{indent}{{ let __xpile_dict_val = ")?;
361            emit_expr(out, value, mode)?;
362            write!(out, "; {dict_name}.insert(")?;
363            emit_expr(out, key, mode)?;
364            writeln!(out, ".clone(), __xpile_dict_val); }}")?;
365            Ok(())
366        }
367        Stmt::Assert { cond } => {
368            write!(out, "{indent}assert!(")?;
369            emit_expr(out, cond, mode)?;
370            writeln!(out, ");")?;
371            Ok(())
372        }
373        // PMAT-503a: `raise Exc("msg")` → `panic!("{}", <message>);`. The
374        // diverging `!` type unifies with any function return, so a `raise`
375        // in a guard clause type-checks without a phantom value.
376        Stmt::Raise { message } => {
377            write!(out, "{indent}panic!(\"{{}}\", ")?;
378            emit_expr(out, message, mode)?;
379            writeln!(out, ");")?;
380            Ok(())
381        }
382        // PMAT-039 / XPILE-BASHRS-MERGER-001 Layer B: shell-command
383        // statements are produced exclusively by bashrs-frontend and
384        // consumed exclusively by bashrs-backend. The Rust backend
385        // refuses them — there is no meaningful Rust translation of an
386        // anonymous shell-line invocation that respects
387        // `C-BASHRS-POSIX-IDEMPOTENCE`. (A future cross-domain
388        // refinement of `subprocess.run([...])` into a typed
389        // `Stmt::Cmd` would still be lowered via Rust's
390        // `std::process::Command` API — that's separate machinery, not
391        // a generic Cmd-to-Rust translation.)
392        Stmt::Cmd { program, args } => Err(CodegenError::Unsupported(format!(
393            "Rust backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
394             contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
395             use `--target shell` to emit POSIX sh via bashrs-backend",
396            args.len()
397        ))),
398        // PMAT-041: see Cmd arm above. Pipelines have the same
399        // cross-domain disposition.
400        Stmt::Pipeline { stages } => Err(CodegenError::Unsupported(format!(
401            "Rust backend does not lower Stmt::Pipeline ({} stages) — \
402             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
403             use `--target shell` to emit POSIX sh via bashrs-backend",
404            stages.len()
405        ))),
406        // PMAT-048: same disposition as the rest of the shell domain.
407        Stmt::ShellLoop { .. } => Err(CodegenError::Unsupported(
408            "Rust backend does not lower Stmt::ShellLoop — \
409             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
410             use `--target shell`"
411                .into(),
412        )),
413        // PMAT-051: same disposition.
414        Stmt::ShellAssign { name, .. } => Err(CodegenError::Unsupported(format!(
415            "Rust backend does not lower Stmt::ShellAssign (`{name}=…`) — \
416             contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
417             use `--target shell`"
418        ))),
419    }
420}
421
422fn emit_param(out: &mut String, p: &Param) -> Result<(), CodegenError> {
423    // PMAT-460: `mut name: T` for params mutated in-place (currently
424    // only via xs.append(v)). Required for Rust to type-check the
425    // emitted `name.push(v)`.
426    if p.mutable {
427        write!(out, "mut ")?;
428    }
429    write!(out, "{}: ", p.name)?;
430    emit_type(out, &p.ty)?;
431    Ok(())
432}
433
434/// Escape a string for emission inside a Rust `"..."` literal.
435/// PMAT-449 (v0.2.0 Track 1.A): minimal escape set for the first
436/// `Type::Str` pass — `\` and `"`. Newlines / tabs / unicode escapes
437/// land in subsequent sub-tracks alongside f-string lowering.
438fn escape_rust_str(s: &str) -> String {
439    let mut out = String::with_capacity(s.len());
440    for c in s.chars() {
441        match c {
442            '\\' => out.push_str("\\\\"),
443            '"' => out.push_str("\\\""),
444            other => out.push(other),
445        }
446    }
447    out
448}
449
450fn emit_type(out: &mut String, t: &Type) -> Result<(), CodegenError> {
451    match t {
452        Type::I64 => out.push_str("i64"),
453        // PMAT-477 (R8): Python `float` → Rust `f64`.
454        Type::F64 => out.push_str("f64"),
455        Type::Bool => out.push_str("bool"),
456        // PMAT-012: re-exported from `xpile-bigint` (which wraps
457        // `num_bigint::BigInt`). Operator overloads (`+`, `-`, `*`,
458        // `<=`, …) work without method calls, matching the i64 codegen
459        // shape — except no `.checked_*().expect(...)` since BigInt
460        // never overflows.
461        Type::BigInt => out.push_str("xpile_bigint::BigInt"),
462        // PMAT-449: v0.2.0 Track 1.A — Python `str` → Rust owned
463        // `String`. First pass is owned-only; `&str` borrowing is the
464        // 1.D stretch sub-track per sub/v0.2.0-depyler-merger.md.
465        Type::Str => out.push_str("String"),
466        // PMAT-455: v0.2.0 Track 1.B — Python `list[T]` → Rust
467        // `Vec<T>`. Owned-first; lifetime-borrowing variants come
468        // after Track 1.D `&str` work lands.
469        Type::List(elem_ty) => {
470            out.push_str("Vec<");
471            emit_type(out, elem_ty)?;
472            out.push('>');
473        }
474        // PMAT-462: v0.2.0 Track 1.C — Python `dict[K, V]` → Rust
475        // `std::collections::HashMap<K, V>`. Owned-first. The
476        // fully-qualified path avoids requiring callers to add a
477        // `use` statement.
478        Type::Dict(k_ty, v_ty) => {
479            out.push_str("std::collections::HashMap<");
480            emit_type(out, k_ty)?;
481            out.push_str(", ");
482            emit_type(out, v_ty)?;
483            out.push('>');
484        }
485        // PMAT-500: Python `set[T]` → Rust `HashSet<T>`.
486        Type::Set(elem_ty) => {
487            out.push_str("std::collections::HashSet<");
488            emit_type(out, elem_ty)?;
489            out.push('>');
490        }
491        // PMAT-494: Python `tuple[T0, T1, ...]` → Rust `(T0, T1, ...)`.
492        Type::Tuple(elems) => {
493            out.push('(');
494            for (i, t) in elems.iter().enumerate() {
495                if i > 0 {
496                    out.push_str(", ");
497                }
498                emit_type(out, t)?;
499            }
500            out.push(')');
501        }
502        // PMAT-046: bashrs-domain types. Rust backend refuses — the
503        // analogous Rust type for ShellString would be the bashrs
504        // runtime's quoting-aware wrapper (not yet shipped); the
505        // analogous type for ExitCode is `std::process::ExitStatus`
506        // but lowering meta-HIR `Type::ExitCode` to that requires
507        // touching the broader `std::process` integration which is
508        // XPILE-BASHRS-MERGER-***+. Use `--target shell` instead.
509        Type::ShellString | Type::ExitCode => {
510            return Err(CodegenError::Unsupported(format!(
511                "Rust backend does not lower {t:?} — \
512                 contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
513                 use `--target shell` for shell-typed signatures"
514            )));
515        }
516    }
517    Ok(())
518}
519
520fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), CodegenError> {
521    match e {
522        Expr::Ident(name) => {
523            // PMAT-013: in BigInt mode, append `.clone()` to every
524            // Ident reference. BigInt isn't `Copy`, so an Ident used
525            // more than once in a function body (cond + branches,
526            // multiplication + recursive call, etc.) would move-on-
527            // first-use otherwise. Cloning unconditionally is mechanical
528            // and correct; LLVM elides unneeded clones at -O.
529            if mode {
530                write!(out, "{}.clone()", name)?;
531            } else {
532                write!(out, "{}", name)?;
533            }
534        }
535        Expr::LitInt(v) => {
536            if mode {
537                // PMAT-012: literal `n` in a BigInt-mode function is
538                // `BigInt::from(<n>i64)`. num-bigint accepts i64 directly.
539                write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
540            } else {
541                write!(out, "{}i64", v)?;
542            }
543        }
544        // PMAT-477 (R8): float literal → `<v>f64`; float arithmetic →
545        // plain infix (IEEE-754 saturates, no checked path).
546        Expr::LitFloat(v) => write!(out, "{}f64", v)?,
547        Expr::FloatBinOp { op, lhs, rhs } => {
548            out.push('(');
549            emit_expr(out, lhs, mode)?;
550            write!(out, " {} ", float_op_sym(*op))?;
551            emit_expr(out, rhs, mode)?;
552            out.push(')');
553        }
554        // PMAT-456 (v0.2.0 Track 1.B): bool literal — Rust's
555        // lowercase `true` / `false`.
556        Expr::LitBool(b) => write!(out, "{}", b)?,
557        Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
558        // PMAT-451 (v0.2.0 Track 1.A): str concatenation. Rust's
559        // `String + &str` is the idiomatic form but requires the lhs
560        // to be owned and rhs to be borrowed — annoying to thread
561        // through when both come from the same xpile lowering pipeline.
562        // `format!("{}{}", l, r)` works uniformly for any `Display`
563        // operands and produces an owned `String`, matching the v0.2.0
564        // owned-only ownership posture (see C-XLATE-PY-STR-TO-RUST-STRING
565        // `ownership_owned` equation).
566        Expr::Concat { lhs, rhs } => {
567            out.push_str("format!(\"{}{}\", ");
568            emit_expr(out, lhs, mode)?;
569            out.push_str(", ");
570            emit_expr(out, rhs, mode)?;
571            out.push(')');
572        }
573        // PMAT-492/493b: Python string methods. No-arg transforms emit a
574        // suffix; the startswith/endswith predicates emit
575        // `.starts_with(&(<pat>)[..])` — the `&(..)[..]` reslice yields
576        // `&str` uniformly whether the pattern is a `String` or a literal.
577        Expr::StrMethod { recv, op, args } => {
578            // PMAT-492d: `join` inverts receiver/arg — Python `sep.join(xs)`
579            // is Rust `xs.join(sep)` — so emit the list arg as the receiver.
580            if matches!(op, StrMethodOp::Join) {
581                emit_expr(out, &args[0], mode)?;
582                out.push_str(".join(&(");
583                emit_expr(out, recv, mode)?;
584                out.push_str(")[..])");
585            } else {
586                emit_expr(out, recv, mode)?;
587                match op {
588                    StrMethodOp::Upper => out.push_str(".to_uppercase()"),
589                    StrMethodOp::Lower => out.push_str(".to_lowercase()"),
590                    StrMethodOp::Strip => out.push_str(".trim().to_string()"),
591                    StrMethodOp::StartsWith | StrMethodOp::EndsWith => {
592                        out.push_str(if matches!(op, StrMethodOp::StartsWith) {
593                            ".starts_with(&("
594                        } else {
595                            ".ends_with(&("
596                        });
597                        emit_expr(out, &args[0], mode)?;
598                        out.push_str(")[..])");
599                    }
600                    // PMAT-492c: `.split(sep)` → Vec<String>.
601                    StrMethodOp::Split => {
602                        out.push_str(".split(&(");
603                        emit_expr(out, &args[0], mode)?;
604                        out.push_str(")[..]).map(|__c| __c.to_string()).collect::<Vec<String>>()");
605                    }
606                    // PMAT-502b: `.replace(old, new)` → `.replace(&(old)[..], &(new)[..])`.
607                    StrMethodOp::Replace => {
608                        out.push_str(".replace(&(");
609                        emit_expr(out, &args[0], mode)?;
610                        out.push_str(")[..], &(");
611                        emit_expr(out, &args[1], mode)?;
612                        out.push_str(")[..])");
613                    }
614                    StrMethodOp::Join => unreachable!("Join handled above"),
615                }
616            }
617        }
618        // PMAT-455 (v0.2.0 Track 1.B): Python list literal → Rust
619        // `vec![...]` macro. The element types are guaranteed
620        // homogeneous by the frontend's lowering check.
621        Expr::ListLit(elems) => {
622            out.push_str("vec![");
623            for (i, e) in elems.iter().enumerate() {
624                if i > 0 {
625                    out.push_str(", ");
626                }
627                emit_expr(out, e, mode)?;
628            }
629            out.push(']');
630        }
631        // PMAT-494: Python tuple literal → Rust `(e0, e1, ...)`.
632        Expr::TupleLit(elems) => {
633            out.push('(');
634            for (i, e) in elems.iter().enumerate() {
635                if i > 0 {
636                    out.push_str(", ");
637                }
638                emit_expr(out, e, mode)?;
639            }
640            out.push(')');
641        }
642        // PMAT-496: Python `xs[lo:hi]` slice → `<c>[(lo) as usize..(hi)
643        // as usize].to_vec()` (list) / `.to_string()` (str).
644        Expr::Slice {
645            collection,
646            lo,
647            hi,
648            of_str,
649        } => {
650            emit_expr(out, collection, mode)?;
651            out.push_str("[(");
652            emit_expr(out, lo, mode)?;
653            out.push_str(") as usize..(");
654            emit_expr(out, hi, mode)?;
655            out.push_str(") as usize]");
656            out.push_str(if *of_str { ".to_string()" } else { ".to_vec()" });
657        }
658        // PMAT-498: scalar numeric builtins → receiver-method form.
659        Expr::NumBuiltin { op, args } => {
660            out.push('(');
661            emit_expr(out, &args[0], mode)?;
662            out.push(')');
663            match op {
664                NumBuiltinOp::Abs => out.push_str(".abs()"),
665                NumBuiltinOp::Min | NumBuiltinOp::Max => {
666                    out.push_str(if matches!(op, NumBuiltinOp::Min) {
667                        ".min("
668                    } else {
669                        ".max("
670                    });
671                    emit_expr(out, &args[1], mode)?;
672                    out.push(')');
673                }
674            }
675        }
676        // PMAT-498b: `sum(xs)` → `<list>.iter().sum::<T>()`.
677        Expr::Sum { list, of_float } => {
678            emit_expr(out, list, mode)?;
679            out.push_str(if *of_float {
680                ".iter().sum::<f64>()"
681            } else {
682                ".iter().sum::<i64>()"
683            });
684        }
685        // PMAT-502e: 1-arg `min(xs)`/`max(xs)` reduction over an int list.
686        Expr::ListMinMax { list, is_max } => {
687            emit_expr(out, list, mode)?;
688            out.push_str(if *is_max {
689                ".iter().copied().max().unwrap()"
690            } else {
691                ".iter().copied().min().unwrap()"
692            });
693        }
694        // PMAT-502c: `sorted(xs)` → `{ let mut __xv = <list>.clone(); __xv.sort(); __xv }`.
695        Expr::Sorted { list } => {
696            out.push_str("{ let mut __xv = ");
697            emit_expr(out, list, mode)?;
698            out.push_str(".clone(); __xv.sort(); __xv }");
699        }
700        // PMAT-502d: `reversed(xs)` → a new reversed Vec.
701        Expr::Reversed { list } => {
702            out.push_str("{ let mut __xv = ");
703            emit_expr(out, list, mode)?;
704            out.push_str(".clone(); __xv.reverse(); __xv }");
705        }
706        // PMAT-462 (v0.2.0 Track 1.C): Python dict literal →
707        // Rust `{ let mut m = HashMap::new(); m.insert(k, v); ... m }`
708        // block expression returning the owned HashMap.
709        Expr::DictLit(pairs) => {
710            // PMAT-466: the empty literal emits a bare `HashMap::new()`
711            // (the surrounding `let`'s annotation supplies K/V). A
712            // `{ let mut m = …; m }` block with no inserts would trip
713            // clippy's `unused_mut` under `-D warnings`.
714            if pairs.is_empty() {
715                out.push_str("std::collections::HashMap::new()");
716            } else {
717                out.push_str("{ let mut m = std::collections::HashMap::new(); ");
718                for (k, v) in pairs {
719                    out.push_str("m.insert(");
720                    emit_expr(out, k, mode)?;
721                    out.push_str(", ");
722                    emit_expr(out, v, mode)?;
723                    out.push_str("); ");
724                }
725                out.push_str("m }");
726            }
727        }
728        // PMAT-457 (v0.2.0 Track 1.B): Python `xs[i]` → Rust
729        // `xs[i as usize].clone()`. The `.clone()` produces an
730        // owned value matching the v0.2.0 owned-only ownership
731        // posture (we don't yet emit `&xs[i]` borrowed refs). `i64`
732        // indices coerce to `usize` via `as`; negative indices
733        // would underflow and panic — that's the v0.2.0 first-cut
734        // semantics (Python's negative-index wrap is a v0.3.0+
735        // sub-track).
736        Expr::Index { collection, index } => {
737            emit_expr(out, collection, mode)?;
738            out.push('[');
739            emit_expr(out, index, mode)?;
740            out.push_str(" as usize].clone()");
741        }
742        // PMAT-466 (v0.2.0 Track 1.C): Python `d[k]` → Rust
743        // `d[&(k)].clone()`. HashMap's `Index` panics on an absent key
744        // (matches Python `KeyError`); `.clone()` yields an owned value
745        // (the v0.2.0 owned-only posture); `&(k)` borrows the key for
746        // the `Index<&Q>` impl.
747        Expr::DictGet { dict, key } => {
748            emit_expr(out, dict, mode)?;
749            out.push_str("[&(");
750            emit_expr(out, key, mode)?;
751            out.push_str(")].clone()");
752        }
753        // PMAT-466: Python `d.get(k, default)` → Rust
754        // `d.get(&(k)).cloned().unwrap_or(default)`. Total: never
755        // panics; returns `default` for an absent key.
756        Expr::DictGetOr { dict, key, default } => {
757            emit_expr(out, dict, mode)?;
758            out.push_str(".get(&(");
759            emit_expr(out, key, mode)?;
760            out.push_str(")).cloned().unwrap_or(");
761            emit_expr(out, default, mode)?;
762            out.push(')');
763        }
764        // PMAT-466: Python `k in d` → Rust `d.contains_key(&(k))`.
765        Expr::DictContains { dict, key } => {
766            emit_expr(out, dict, mode)?;
767            out.push_str(".contains_key(&(");
768            emit_expr(out, key, mode)?;
769            out.push_str("))");
770        }
771        // PMAT-500: Python set literal `{a, b, c}` → HashSet-init block.
772        // PMAT-501b: an empty SetLit (the set-comprehension accumulator)
773        // emits a bare `HashSet::new()` (the let annotation supplies T) —
774        // a `{ … }` block with no inserts would trip clippy's unused_mut.
775        Expr::SetLit(elems) => {
776            if elems.is_empty() {
777                out.push_str("std::collections::HashSet::new()");
778            } else {
779                out.push_str("{ let mut __xset = std::collections::HashSet::new(); ");
780                for e in elems {
781                    out.push_str("__xset.insert(");
782                    emit_expr(out, e, mode)?;
783                    out.push_str("); ");
784                }
785                out.push_str("__xset }");
786            }
787        }
788        // PMAT-500: Python `x in s` → `<set>.contains(&(<elem>))`.
789        Expr::SetContains { set, elem } => {
790            emit_expr(out, set, mode)?;
791            out.push_str(".contains(&(");
792            emit_expr(out, elem, mode)?;
793            out.push_str("))");
794        }
795        // PMAT-459 (v0.2.0 Track 1.B): Python `len(x)` → Rust
796        // `x.len() as i64`. Vec/String both expose `.len()` returning
797        // `usize`; the `as i64` cast brings the result back into
798        // Python's signed-int domain.
799        Expr::Len(inner) => {
800            emit_expr(out, inner, mode)?;
801            out.push_str(".len() as i64");
802        }
803        Expr::IfExpr {
804            cond,
805            then_expr,
806            else_expr,
807        } => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
808        Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
809        Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
810        // PMAT-449 (v0.2.0 Track 1.A): Python `str` literals lower
811        // to owned `String::from("...")`. The character set is
812        // escape-aware (`"` and `\` → `\"` / `\\`); v0.2.0 starts
813        // with the minimal escape set, expanded in later sub-tracks.
814        Expr::LitStr(s) => {
815            write!(out, "String::from(\"{}\")", escape_rust_str(s))?;
816        }
817        // PMAT-042: `QuotedString` carries an explicit shell-domain
818        // quoting strategy (bareword vs single-quote vs double-quote);
819        // its semantics are bashrs-only. Rust backend refuses.
820        Expr::QuotedString { .. } => {
821            return Err(CodegenError::Unsupported(
822                "Rust backend does not lower Expr::QuotedString — \
823                 contract C-BASHRS-POSIX-IDEMPOTENCE governs quoted shell strings; \
824                 use `--target shell`"
825                    .into(),
826            ));
827        }
828        // PMAT-045: shell-variable references — same disposition.
829        Expr::ShellVar(name) => {
830            return Err(CodegenError::Unsupported(format!(
831                "Rust backend does not lower Expr::ShellVar (${name}) — \
832                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable references; \
833                 use `--target shell`"
834            )));
835        }
836        // PMAT-047: command substitution — same disposition.
837        Expr::CommandSubstitution(_) => {
838            return Err(CodegenError::Unsupported(
839                "Rust backend does not lower Expr::CommandSubstitution — \
840                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
841                 use `--target shell`"
842                    .into(),
843            ));
844        }
845        // PMAT-055: shell special parameters — same disposition.
846        Expr::ShellSpecial(name) => {
847            return Err(CodegenError::Unsupported(format!(
848                "Rust backend does not lower Expr::ShellSpecial (${name}) — \
849                 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
850                 use `--target shell`"
851            )));
852        }
853    }
854    Ok(())
855}
856
857fn emit_unop(out: &mut String, op: UnOp, operand: &Expr, mode: bool) -> Result<(), CodegenError> {
858    match op {
859        UnOp::Neg => {
860            if mode {
861                // BigInt::neg returns BigInt without overflow risk.
862                // PMAT-012 — slow-path side of C-PY-INT-ARITH.
863                write!(out, "(-")?;
864                emit_expr(out, operand, mode)?;
865                write!(out, ")")?;
866            } else {
867                // Python: `-x` on int never overflows mathematically (int is unbounded).
868                // Rust: `i64::MIN.checked_neg() == None`. Use checked_neg + panic that
869                // points at the unimplemented bigint promotion slow path of
870                // contract C-PY-INT-ARITH. See py-int-arith-v1.yaml.
871                write!(out, "(")?;
872                emit_expr(out, operand, mode)?;
873                write!(
874                    out,
875                    ").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
876                )?;
877            }
878        }
879        UnOp::Not => {
880            write!(out, "(!")?;
881            emit_expr(out, operand, mode)?;
882            write!(out, ")")?;
883        }
884    }
885    Ok(())
886}
887
888fn emit_call(
889    out: &mut String,
890    callee: &str,
891    args: &[Expr],
892    mode: bool,
893) -> Result<(), CodegenError> {
894    write!(out, "{}(", callee)?;
895    for (i, a) in args.iter().enumerate() {
896        if i > 0 {
897            write!(out, ", ")?;
898        }
899        emit_expr(out, a, mode)?;
900    }
901    write!(out, ")")?;
902    Ok(())
903}
904
905/// Rust `if cond { then } else { else_ }` — usable as an expression.
906/// When the `else_` is itself another `IfExpr`, emit a flat
907/// `else if ...` form (no extra braces) for readability.
908fn emit_if_expr(
909    out: &mut String,
910    cond: &Expr,
911    then_expr: &Expr,
912    else_expr: &Expr,
913    mode: bool,
914) -> Result<(), CodegenError> {
915    write!(out, "if ")?;
916    emit_expr(out, cond, mode)?;
917    write!(out, " {{ ")?;
918    emit_expr(out, then_expr, mode)?;
919    write!(out, " }} else ")?;
920    match else_expr {
921        Expr::IfExpr {
922            cond: c2,
923            then_expr: t2,
924            else_expr: e2,
925        } => {
926            emit_if_expr(out, c2, t2, e2, mode)?;
927            return Ok(());
928        }
929        _ => {
930            write!(out, "{{ ")?;
931            emit_expr(out, else_expr, mode)?;
932            write!(out, " }}")?;
933        }
934    }
935    Ok(())
936}
937
938/// Emit a binary op.
939///
940/// Arithmetic (`+`, `-`, `*`, `//`, `%`) uses `checked_*` variants with
941/// `.expect("…")` rather than wrapping/truncating. Python `int` is
942/// mathematically unbounded — silently wrapping at i64 would violate
943/// the Layer-1 contract `C-PY-INT-ARITH`. Until the contract's bigint
944/// slow path is implemented, overflow panics with a message pointing
945/// at the contract.
946///
947/// FloorDiv / Mod additionally preserve Python-floor semantics via
948/// `checked_div_euclid` / `checked_rem_euclid` (plain `/` and `%` in
949/// Rust truncate toward zero, which diverges from Python on negative
950/// operands).
951///
952/// Comparisons and logical ops never overflow, so they remain infix.
953fn emit_binop(
954    out: &mut String,
955    op: BinOp,
956    lhs: &Expr,
957    rhs: &Expr,
958    mode: bool,
959) -> Result<(), CodegenError> {
960    match op {
961        // Arithmetic: in BigInt mode all of these are plain infix
962        // (BigInt overloads `+ - * <= ...` via num-bigint) — no
963        // overflow risk, so no `.checked_*().expect(...)`. The C-PY-INT-ARITH
964        // slow path is satisfied directly. PMAT-012.
965        BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
966        BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
967        BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
968        BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
969        BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
970        // PMAT-026 / PMAT-013-FOLLOWUP: bitwise + shift + power on
971        // BigInt. num-bigint's `BitAnd / BitOr / BitXor` are direct
972        // infix operators on `BigInt`; `<< >>` and `**` take rhs as
973        // `usize` / `u32` (not BigInt), so we route through helpers
974        // in `xpile_bigint::{shl, shr, pow}` that handle the
975        // BigInt → primitive conversion (with a contract-named panic
976        // on out-of-range exponents — same posture as the i64 fast
977        // path's shift / pow handling).
978        BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
979        BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
980        BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
981        BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
982        BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
983        BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
984        BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
985        BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
986        BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
987        BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
988        BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
989        BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
990        BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
991        BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
992        BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
993        BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
994        BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
995        BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
996        BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
997        BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
998        BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
999        BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
1000        BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
1001        BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
1002        BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
1003    }
1004}
1005
1006/// BigInt-mode floor-div / mod via the helpers exposed in xpile-bigint.
1007/// Takes references because num-bigint's `Integer::div_floor` consumes
1008/// `self`; the wrappers borrow. PMAT-012.
1009fn emit_bigint_floor_call(
1010    out: &mut String,
1011    method: &str,
1012    lhs: &Expr,
1013    rhs: &Expr,
1014    mode: bool,
1015) -> Result<(), CodegenError> {
1016    write!(out, "xpile_bigint::{method}(&")?;
1017    emit_expr(out, lhs, mode)?;
1018    write!(out, ", &")?;
1019    emit_expr(out, rhs, mode)?;
1020    write!(out, ")")?;
1021    Ok(())
1022}
1023
1024/// Emit `(lhs).checked_pow(u32::try_from(rhs).expect(...)).expect(...)`.
1025/// Same panic-naming pattern as shifts: the inner expect fires on a
1026/// negative exponent (Python would return Float, which v0.1.0's type
1027/// system has no I64-compatible representation for); the outer expect
1028/// fires on i64 overflow.
1029fn emit_checked_pow(
1030    out: &mut String,
1031    lhs: &Expr,
1032    rhs: &Expr,
1033    mode: bool,
1034) -> Result<(), CodegenError> {
1035    write!(out, "(")?;
1036    emit_expr(out, lhs, mode)?;
1037    write!(out, ").checked_pow(u32::try_from(")?;
1038    emit_expr(out, rhs, mode)?;
1039    write!(
1040        out,
1041        ").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\")"
1042    )?;
1043    Ok(())
1044}
1045
1046/// Emit a shift: `(lhs).checked_sh*(u32::try_from(rhs).expect(...)).expect(...)`.
1047/// Both panics name `C-PY-INT-ARITH` so the trail is still legible — the
1048/// inner one fires when Python's "shift by negative or huge" raises in
1049/// CPython; the outer one fires when the shift amount is >= 64 on i64.
1050fn emit_checked_shift(
1051    out: &mut String,
1052    lhs: &Expr,
1053    method: &str,
1054    rhs: &Expr,
1055    op_name: &str,
1056    mode: bool,
1057) -> Result<(), CodegenError> {
1058    write!(out, "(")?;
1059    emit_expr(out, lhs, mode)?;
1060    write!(out, ").{method}(u32::try_from(")?;
1061    emit_expr(out, rhs, mode)?;
1062    write!(
1063        out,
1064        ").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\")"
1065    )?;
1066    Ok(())
1067}
1068
1069/// Emit a checked binary op: `(<lhs>).<method>(<rhs>).expect("<msg> overflow ...")`.
1070/// Returns `i64`, identical to infix on the no-overflow fast path.
1071fn emit_checked(
1072    out: &mut String,
1073    lhs: &Expr,
1074    method: &str,
1075    rhs: &Expr,
1076    op_name: &str,
1077    mode: bool,
1078) -> Result<(), CodegenError> {
1079    write!(out, "(")?;
1080    emit_expr(out, lhs, mode)?;
1081    write!(out, ").{method}(")?;
1082    emit_expr(out, rhs, mode)?;
1083    write!(
1084        out,
1085        ").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
1086    )?;
1087    Ok(())
1088}
1089
1090fn emit_infix(
1091    out: &mut String,
1092    lhs: &Expr,
1093    op: &str,
1094    rhs: &Expr,
1095    mode: bool,
1096) -> Result<(), CodegenError> {
1097    write!(out, "(")?;
1098    emit_expr(out, lhs, mode)?;
1099    out.push_str(op);
1100    emit_expr(out, rhs, mode)?;
1101    write!(out, ")")?;
1102    Ok(())
1103}
1104
1105pub struct RustBackend;
1106
1107impl Backend for RustBackend {
1108    fn name(&self) -> &'static str {
1109        "rust"
1110    }
1111
1112    fn targets(&self) -> &[Target] {
1113        &[Target::Rust]
1114    }
1115
1116    fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
1117        let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
1118        Ok(Artifact {
1119            primary,
1120            sidecars: Vec::new(),
1121            citations: Vec::new(),
1122            quorum_status: QuorumStatus::Single {
1123                emitter: "xpile-rust-codegen".to_string(),
1124            },
1125        })
1126    }
1127}
1128
1129// ── C emit path (PMAT-467, v0.2.0 Track 2.A) ────────────────────────
1130//
1131// Isolated from the Python/Ruchy emit above so C's semantics can't
1132// regress it. C `int` is fixed-width `i32`; signed overflow is UB, for
1133// which `wrapping_*` is the sound conservative discharge (it produces a
1134// deterministic two's-complement result rather than invoking Rust UB).
1135// This mirrors the standalone-decy → C-C-INT-ARITH plan in
1136// `sub/v0.2.0-decy-merger.md`; the contract substrate is queued.
1137
1138fn emit_c_function(out: &mut String, f: &Function) -> Result<(), CodegenError> {
1139    // Forward-reference citation (substrate queued, same posture as the
1140    // dict lane citing C-XLATE-PY-DICT-TO-HASHMAP before it existed).
1141    writeln!(out, "// xpile-contract: C-C-INT-ARITH")?;
1142    write!(out, "pub fn {}(", f.name)?;
1143    for (i, p) in f.params.iter().enumerate() {
1144        if i > 0 {
1145            write!(out, ", ")?;
1146        }
1147        write!(out, "{}: i32", p.name)?;
1148    }
1149    writeln!(out, ") -> i32 {{")?;
1150    for stmt in &f.body.stmts {
1151        emit_c_stmt(out, stmt, "    ")?;
1152    }
1153    write!(out, "    ")?;
1154    emit_c_expr(out, &f.body.trailing_return)?;
1155    writeln!(out)?;
1156    writeln!(out, "}}")?;
1157    Ok(())
1158}
1159
1160fn emit_c_stmt(out: &mut String, stmt: &Stmt, indent: &str) -> Result<(), CodegenError> {
1161    match stmt {
1162        Stmt::Let {
1163            name,
1164            value,
1165            mutable,
1166            ..
1167        } => {
1168            let kw = if *mutable { "let mut" } else { "let" };
1169            write!(out, "{indent}{kw} {name}: i32 = ")?;
1170            emit_c_expr(out, value)?;
1171            writeln!(out, ";")?;
1172            Ok(())
1173        }
1174        Stmt::Assign { name, value } => {
1175            write!(out, "{indent}{name} = ")?;
1176            emit_c_expr(out, value)?;
1177            writeln!(out, ";")?;
1178            Ok(())
1179        }
1180        // PMAT-479 (R10): C early `return <expr>;` (guard clause).
1181        Stmt::Return(e) => {
1182            write!(out, "{indent}return ")?;
1183            emit_c_expr(out, e)?;
1184            writeln!(out, ";")?;
1185            Ok(())
1186        }
1187        Stmt::While { cond, body } => {
1188            write!(out, "{indent}while ")?;
1189            emit_c_expr(out, cond)?;
1190            writeln!(out, " {{")?;
1191            let inner = format!("{indent}    ");
1192            for s in body {
1193                emit_c_stmt(out, s, &inner)?;
1194            }
1195            writeln!(out, "{indent}}}")?;
1196            Ok(())
1197        }
1198        // PMAT-478 (R9): C `if (c) { … } else { … }` → Rust if/else
1199        // statement (the `else` block omitted when empty).
1200        Stmt::If {
1201            cond,
1202            then_body,
1203            else_body,
1204        } => {
1205            write!(out, "{indent}if ")?;
1206            emit_c_expr(out, cond)?;
1207            writeln!(out, " {{")?;
1208            let inner = format!("{indent}    ");
1209            for s in then_body {
1210                emit_c_stmt(out, s, &inner)?;
1211            }
1212            if else_body.is_empty() {
1213                writeln!(out, "{indent}}}")?;
1214            } else {
1215                writeln!(out, "{indent}}} else {{")?;
1216                for s in else_body {
1217                    emit_c_stmt(out, s, &inner)?;
1218                }
1219                writeln!(out, "{indent}}}")?;
1220            }
1221            Ok(())
1222        }
1223        other => Err(CodegenError::Unsupported(format!(
1224            "C backend supports `int x = e;`, `x = e;`, `if (c) {{ … }} else {{ … }}`, and `while (c) {{ … }}`, got {other:?}"
1225        ))),
1226    }
1227}
1228
1229fn emit_c_expr(out: &mut String, e: &Expr) -> Result<(), CodegenError> {
1230    match e {
1231        Expr::LitInt(v) => write!(out, "{v}i32")?,
1232        Expr::Ident(name) => write!(out, "{name}")?,
1233        Expr::BinOp { op, lhs, rhs } => emit_c_binop(out, *op, lhs, rhs)?,
1234        Expr::UnOp { op, operand } => match op {
1235            // C unary minus on `int` is wrapping (INT_MIN negation is UB
1236            // in C; `wrapping_neg` is the sound deterministic discharge).
1237            UnOp::Neg => {
1238                write!(out, "(")?;
1239                emit_c_expr(out, operand)?;
1240                write!(out, ").wrapping_neg()")?;
1241            }
1242            UnOp::Not => {
1243                write!(out, "!(")?;
1244                emit_c_expr(out, operand)?;
1245                write!(out, ")")?;
1246            }
1247        },
1248        Expr::IfExpr {
1249            cond,
1250            then_expr,
1251            else_expr,
1252        } => {
1253            write!(out, "if ")?;
1254            emit_c_expr(out, cond)?;
1255            write!(out, " {{ ")?;
1256            emit_c_expr(out, then_expr)?;
1257            write!(out, " }} else {{ ")?;
1258            emit_c_expr(out, else_expr)?;
1259            write!(out, " }}")?;
1260        }
1261        Expr::Call { callee, args } => {
1262            write!(out, "{callee}(")?;
1263            for (i, a) in args.iter().enumerate() {
1264                if i > 0 {
1265                    write!(out, ", ")?;
1266                }
1267                emit_c_expr(out, a)?;
1268            }
1269            write!(out, ")")?;
1270        }
1271        other => {
1272            return Err(CodegenError::Unsupported(format!(
1273                "C backend slice 1 does not lower {other:?} — supported: int literals, \
1274                 identifiers, calls, + - *, comparisons, && ||, unary - !, and the ternary"
1275            )));
1276        }
1277    }
1278    Ok(())
1279}
1280
1281fn emit_c_binop(out: &mut String, op: BinOp, lhs: &Expr, rhs: &Expr) -> Result<(), CodegenError> {
1282    // Arithmetic: wrapping (C signed overflow is UB → deterministic
1283    // two's-complement). Comparisons / logicals: plain infix, producing
1284    // a Rust `bool` (correct for `if`/`&&`/`||` operand positions, which
1285    // is where the C frontend places them).
1286    let wrapping = |out: &mut String, method: &str| -> Result<(), CodegenError> {
1287        write!(out, "(")?;
1288        emit_c_expr(out, lhs)?;
1289        write!(out, ").{method}(")?;
1290        emit_c_expr(out, rhs)?;
1291        write!(out, ")")?;
1292        Ok(())
1293    };
1294    let infix = |out: &mut String, sym: &str| -> Result<(), CodegenError> {
1295        emit_c_expr(out, lhs)?;
1296        write!(out, " {sym} ")?;
1297        emit_c_expr(out, rhs)?;
1298        Ok(())
1299    };
1300    match op {
1301        BinOp::Add => wrapping(out, "wrapping_add")?,
1302        BinOp::Sub => wrapping(out, "wrapping_sub")?,
1303        BinOp::Mul => wrapping(out, "wrapping_mul")?,
1304        // C `/` truncates toward zero (Rust integer `/` does too);
1305        // `wrapping_div`/`wrapping_rem` add the INT_MIN/-1 UB guard.
1306        // The frontend carries these as FloorDiv/Mod (shared IR
1307        // variants); here they mean C truncating div/rem, not Python
1308        // floor.
1309        BinOp::FloorDiv => wrapping(out, "wrapping_div")?,
1310        BinOp::Mod => wrapping(out, "wrapping_rem")?,
1311        BinOp::Eq => infix(out, "==")?,
1312        BinOp::NotEq => infix(out, "!=")?,
1313        BinOp::Lt => infix(out, "<")?,
1314        BinOp::LtEq => infix(out, "<=")?,
1315        BinOp::Gt => infix(out, ">")?,
1316        BinOp::GtEq => infix(out, ">=")?,
1317        BinOp::And => infix(out, "&&")?,
1318        BinOp::Or => infix(out, "||")?,
1319        other => {
1320            return Err(CodegenError::Unsupported(format!(
1321                "C backend slice 1 does not lower BinOp::{other:?} — `/`, `%`, bitwise, \
1322                 shift, and power are deferred to a later decy slice"
1323            )));
1324        }
1325    }
1326    Ok(())
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331    use super::*;
1332    use xpile_meta_hir::{Module, SourceLang};
1333
1334    fn module_with(name: &str, items: Vec<Item>) -> Module {
1335        Module {
1336            name: name.into(),
1337            source_lang: SourceLang::Python,
1338            items,
1339            ffi_boundaries: Vec::new(),
1340        }
1341    }
1342
1343    fn add_fn() -> Function {
1344        Function {
1345            name: "add".into(),
1346            params: vec![
1347                Param {
1348                    name: "a".into(),
1349                    ty: Type::I64,
1350                    mutable: false,
1351                },
1352                Param {
1353                    name: "b".into(),
1354                    ty: Type::I64,
1355                    mutable: false,
1356                },
1357            ],
1358            return_type: Type::I64,
1359            body: Block {
1360                stmts: vec![],
1361                trailing_return: Expr::BinOp {
1362                    op: BinOp::Add,
1363                    lhs: Box::new(Expr::Ident("a".into())),
1364                    rhs: Box::new(Expr::Ident("b".into())),
1365                },
1366            },
1367        }
1368    }
1369
1370    #[test]
1371    fn emits_add_function() {
1372        let m = module_with("fixture", vec![Item::Function(add_fn())]);
1373        let rust = emit_module(&m).expect("emit ok");
1374        assert!(rust.contains("pub fn add(a: i64, b: i64) -> i64"));
1375        // After contract C-PY-INT-ARITH was wired in (PMAT-002),
1376        // addition emits `(a).checked_add(b).expect(...)`, not plain
1377        // `(a + b)`. Assert on the load-bearing invariants rather
1378        // than the exact shape.
1379        assert!(rust.contains("checked_add"), "expected checked_add: {rust}");
1380        assert!(
1381            rust.contains("C-PY-INT-ARITH"),
1382            "expected contract reference in panic msg: {rust}"
1383        );
1384    }
1385
1386    #[test]
1387    fn emits_floordiv_as_div_euclid() {
1388        // Python `a // b` must NOT lower to Rust `/`.
1389        let f = Function {
1390            name: "fdiv".into(),
1391            params: vec![
1392                Param {
1393                    name: "a".into(),
1394                    ty: Type::I64,
1395                    mutable: false,
1396                },
1397                Param {
1398                    name: "b".into(),
1399                    ty: Type::I64,
1400                    mutable: false,
1401                },
1402            ],
1403            return_type: Type::I64,
1404            body: Block {
1405                stmts: vec![],
1406                trailing_return: Expr::BinOp {
1407                    op: BinOp::FloorDiv,
1408                    lhs: Box::new(Expr::Ident("a".into())),
1409                    rhs: Box::new(Expr::Ident("b".into())),
1410                },
1411            },
1412        };
1413        let m = module_with("fixture", vec![Item::Function(f)]);
1414        let rust = emit_module(&m).expect("emit ok");
1415        assert!(
1416            rust.contains("div_euclid"),
1417            "Python floor-div must lower to div_euclid (got: {})",
1418            rust
1419        );
1420        assert!(
1421            !rust.contains(" / "),
1422            "must not use plain Rust `/` for Python `//`"
1423        );
1424    }
1425
1426    #[test]
1427    fn emits_comparison_returning_bool() {
1428        let f = Function {
1429            name: "le".into(),
1430            params: vec![
1431                Param {
1432                    name: "a".into(),
1433                    ty: Type::I64,
1434                    mutable: false,
1435                },
1436                Param {
1437                    name: "b".into(),
1438                    ty: Type::I64,
1439                    mutable: false,
1440                },
1441            ],
1442            return_type: Type::Bool,
1443            body: Block {
1444                stmts: vec![],
1445                trailing_return: Expr::BinOp {
1446                    op: BinOp::LtEq,
1447                    lhs: Box::new(Expr::Ident("a".into())),
1448                    rhs: Box::new(Expr::Ident("b".into())),
1449                },
1450            },
1451        };
1452        let m = module_with("fixture", vec![Item::Function(f)]);
1453        let rust = emit_module(&m).expect("emit ok");
1454        assert!(rust.contains("-> bool"));
1455        assert!(rust.contains("(a <= b)"));
1456    }
1457
1458    #[test]
1459    fn emits_if_expression_for_ternary() {
1460        let f = Function {
1461            name: "pick".into(),
1462            params: vec![
1463                Param {
1464                    name: "a".into(),
1465                    ty: Type::I64,
1466                    mutable: false,
1467                },
1468                Param {
1469                    name: "b".into(),
1470                    ty: Type::I64,
1471                    mutable: false,
1472                },
1473            ],
1474            return_type: Type::I64,
1475            body: Block {
1476                stmts: vec![],
1477                trailing_return: Expr::IfExpr {
1478                    cond: Box::new(Expr::BinOp {
1479                        op: BinOp::LtEq,
1480                        lhs: Box::new(Expr::Ident("a".into())),
1481                        rhs: Box::new(Expr::Ident("b".into())),
1482                    }),
1483                    then_expr: Box::new(Expr::Ident("a".into())),
1484                    else_expr: Box::new(Expr::Ident("b".into())),
1485                },
1486            },
1487        };
1488        let m = module_with("fixture", vec![Item::Function(f)]);
1489        let rust = emit_module(&m).expect("emit ok");
1490        assert!(rust.contains("if (a <= b) { a } else { b }"));
1491        assert!(rust.contains("pub fn pick(a: i64, b: i64) -> i64"));
1492    }
1493
1494    #[test]
1495    fn emit_module_produces_rustc_parseable_output() {
1496        // Run the emitted source through `syn::parse_file` to ensure
1497        // syntactic well-formedness without spawning rustc. Doesn't
1498        // type-check (that's the workspace-test job).
1499        // (syn isn't a dep here; instead check basic shape.)
1500        let m = module_with("fixture", vec![Item::Function(add_fn())]);
1501        let rust = emit_module(&m).expect("emit ok");
1502        // sanity: balanced braces and trailing newline
1503        assert_eq!(rust.matches('{').count(), rust.matches('}').count());
1504        assert!(rust.ends_with('\n'));
1505    }
1506}