use std::fmt::Write;
use xpile_backend::{Artifact, Backend, BackendConfig, BackendError, QuorumStatus, Target};
use xpile_meta_hir::{
BinOp, Block, Expr, FloatOp, Function, Item, Module, Param, Stmt, Type, UnOp,
};
fn float_op_sym(op: FloatOp) -> &'static str {
match op {
FloatOp::Add => "+",
FloatOp::Sub => "-",
FloatOp::Mul => "*",
FloatOp::Div => "/",
}
}
#[derive(Debug, thiserror::Error)]
pub enum RuchyCodegenError {
#[error("unsupported item: {0}")]
Unsupported(String),
#[error("formatting error: {0}")]
Format(#[from] std::fmt::Error),
}
pub fn emit_module(module: &Module) -> Result<String, RuchyCodegenError> {
let mut out = String::new();
writeln!(
out,
"// xpile-generated from {:?} module {}",
module.source_lang, module.name
)?;
writeln!(out)?;
for item in &module.items {
match item {
Item::Function(f) => emit_function(&mut out, f)?,
}
}
Ok(out)
}
fn emit_function(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
emit_contract_citations(out, f)?;
write!(out, "fun {}(", f.name)?;
for (i, p) in f.params.iter().enumerate() {
if i > 0 {
write!(out, ", ")?;
}
emit_param(out, p)?;
}
write!(out, ") -> ")?;
emit_type(out, &f.return_type)?;
writeln!(out, " {{")?;
let mode = function_bigint_mode(f);
emit_block(out, &f.body, mode)?;
writeln!(out, "}}")?;
Ok(())
}
fn function_bigint_mode(f: &Function) -> bool {
if matches!(f.return_type, Type::BigInt) {
return true;
}
if f.params.iter().any(|p| matches!(p.ty, Type::BigInt)) {
return true;
}
fn stmt_has_bigint(s: &Stmt) -> bool {
match s {
Stmt::Let { ty, .. } => matches!(ty, Type::BigInt),
Stmt::Assign { .. } | Stmt::Assert { .. } | Stmt::Return(_) => false,
Stmt::While { body, .. } | Stmt::ForEach { body, .. } => {
body.iter().any(stmt_has_bigint)
}
Stmt::If {
then_body,
else_body,
..
} => then_body.iter().any(stmt_has_bigint) || else_body.iter().any(stmt_has_bigint),
Stmt::ListAppend { .. } => false,
Stmt::IndexAssign { .. } => false,
Stmt::DictSet { .. } => false,
Stmt::Cmd { .. } => false,
Stmt::Pipeline { .. } => false,
Stmt::ShellLoop { .. } => false,
Stmt::ShellAssign { .. } => false,
}
}
f.body.stmts.iter().any(stmt_has_bigint)
}
fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
for id in f.applicable_contracts() {
writeln!(out, "// xpile-contract: {id}")?;
}
Ok(())
}
fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), RuchyCodegenError> {
for stmt in &block.stmts {
emit_stmt(out, stmt, mode)?;
}
write!(out, " ")?;
emit_expr(out, &block.trailing_return, mode)?;
writeln!(out)?;
Ok(())
}
fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), RuchyCodegenError> {
emit_stmt_indented(out, stmt, " ", mode)
}
fn emit_stmt_indented(
out: &mut String,
stmt: &Stmt,
indent: &str,
mode: bool,
) -> Result<(), RuchyCodegenError> {
match stmt {
Stmt::Let {
name,
ty,
value,
mutable,
} => {
let kw = if *mutable { "let mut" } else { "let" };
write!(out, "{indent}{kw} {name}: ")?;
emit_type(out, ty)?;
write!(out, " = ")?;
emit_expr(out, value, mode)?;
writeln!(out, ";")?;
Ok(())
}
Stmt::Return(e) => {
write!(out, "{indent}return ")?;
emit_expr(out, e, mode)?;
writeln!(out, ";")?;
Ok(())
}
Stmt::If {
cond,
then_body,
else_body,
} => {
write!(out, "{indent}if ")?;
emit_expr(out, cond, mode)?;
writeln!(out, " {{")?;
let inner = format!("{indent} ");
for s in then_body {
emit_stmt_indented(out, s, &inner, mode)?;
}
if else_body.is_empty() {
writeln!(out, "{indent}}}")?;
} else {
writeln!(out, "{indent}}} else {{")?;
for s in else_body {
emit_stmt_indented(out, s, &inner, mode)?;
}
writeln!(out, "{indent}}}")?;
}
Ok(())
}
Stmt::Assign { name, value } => {
write!(out, "{indent}{name} = ")?;
emit_expr(out, value, mode)?;
writeln!(out, ";")?;
Ok(())
}
Stmt::While { cond, body } => {
write!(out, "{indent}while ")?;
emit_expr(out, cond, mode)?;
writeln!(out, " {{")?;
let inner = format!("{indent} ");
for s in body {
emit_stmt_indented(out, s, &inner, mode)?;
}
writeln!(out, "{indent}}}")?;
Ok(())
}
Stmt::ForEach {
var,
iter,
body,
over_keys,
..
} => {
let method = if *over_keys { "keys" } else { "iter" };
write!(out, "{indent}for {var} in ")?;
emit_expr(out, iter, mode)?;
writeln!(out, ".{method}().cloned() {{")?;
let inner = format!("{indent} ");
for s in body {
emit_stmt_indented(out, s, &inner, mode)?;
}
writeln!(out, "{indent}}}")?;
Ok(())
}
Stmt::ListAppend { list_name, elem } => {
write!(out, "{indent}{list_name}.push(")?;
emit_expr(out, elem, mode)?;
writeln!(out, ");")?;
Ok(())
}
Stmt::IndexAssign {
list_name,
index,
value,
} => {
write!(out, "{indent}{list_name}[")?;
emit_expr(out, index, mode)?;
out.push_str(" as usize] = ");
emit_expr(out, value, mode)?;
writeln!(out, ";")?;
Ok(())
}
Stmt::DictSet {
dict_name,
key,
value,
} => {
write!(out, "{indent}{{ let __xpile_dict_val = ")?;
emit_expr(out, value, mode)?;
write!(out, "; {dict_name}.insert(")?;
emit_expr(out, key, mode)?;
writeln!(out, ".clone(), __xpile_dict_val); }}")?;
Ok(())
}
Stmt::Assert { cond } => {
write!(out, "{indent}assert!(")?;
emit_expr(out, cond, mode)?;
writeln!(out, ");")?;
Ok(())
}
Stmt::Cmd { program, args } => Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
use `--target shell` to emit POSIX sh via bashrs-backend",
args.len()
))),
Stmt::Pipeline { stages } => Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower Stmt::Pipeline ({} stages) — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
use `--target shell`",
stages.len()
))),
Stmt::ShellLoop { .. } => Err(RuchyCodegenError::Unsupported(
"Ruchy backend does not lower Stmt::ShellLoop — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
use `--target shell`"
.into(),
)),
Stmt::ShellAssign { name, .. } => Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower Stmt::ShellAssign (`{name}=…`) — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
use `--target shell`"
))),
}
}
fn emit_param(out: &mut String, p: &Param) -> Result<(), RuchyCodegenError> {
if p.mutable {
write!(out, "mut ")?;
}
write!(out, "{}: ", p.name)?;
emit_type(out, &p.ty)?;
Ok(())
}
fn escape_ruchy_str(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
other => out.push(other),
}
}
out
}
fn emit_type(out: &mut String, t: &Type) -> Result<(), RuchyCodegenError> {
match t {
Type::I64 => out.push_str("i64"),
Type::F64 => out.push_str("f64"),
Type::Bool => out.push_str("bool"),
Type::BigInt => out.push_str("xpile_bigint::BigInt"),
Type::Str => out.push_str("String"),
Type::List(elem_ty) => {
out.push_str("Vec<");
emit_type(out, elem_ty)?;
out.push('>');
}
Type::Dict(k_ty, v_ty) => {
out.push_str("std::collections::HashMap<");
emit_type(out, k_ty)?;
out.push_str(", ");
emit_type(out, v_ty)?;
out.push('>');
}
Type::ShellString | Type::ExitCode => {
return Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower {t:?} — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
use `--target shell`"
)));
}
}
Ok(())
}
fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), RuchyCodegenError> {
match e {
Expr::Ident(name) => {
if mode {
write!(out, "{}.clone()", name)?;
} else {
write!(out, "{}", name)?;
}
}
Expr::LitInt(v) => {
if mode {
write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
} else {
write!(out, "{}i64", v)?;
}
}
Expr::LitFloat(v) => write!(out, "{}f64", v)?,
Expr::FloatBinOp { op, lhs, rhs } => {
out.push('(');
emit_expr(out, lhs, mode)?;
write!(out, " {} ", float_op_sym(*op))?;
emit_expr(out, rhs, mode)?;
out.push(')');
}
Expr::LitBool(b) => write!(out, "{}", b)?,
Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
Expr::Concat { lhs, rhs } => {
out.push_str("format!(\"{}{}\", ");
emit_expr(out, lhs, mode)?;
out.push_str(", ");
emit_expr(out, rhs, mode)?;
out.push(')');
}
Expr::ListLit(elems) => {
out.push_str("vec![");
for (i, e) in elems.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
emit_expr(out, e, mode)?;
}
out.push(']');
}
Expr::DictLit(pairs) => {
if pairs.is_empty() {
out.push_str("std::collections::HashMap::new()");
} else {
out.push_str("{ let mut m = std::collections::HashMap::new(); ");
for (k, v) in pairs {
out.push_str("m.insert(");
emit_expr(out, k, mode)?;
out.push_str(", ");
emit_expr(out, v, mode)?;
out.push_str("); ");
}
out.push_str("m }");
}
}
Expr::Index { collection, index } => {
emit_expr(out, collection, mode)?;
out.push('[');
emit_expr(out, index, mode)?;
out.push_str(" as usize].clone()");
}
Expr::DictGet { dict, key } => {
emit_expr(out, dict, mode)?;
out.push_str("[&(");
emit_expr(out, key, mode)?;
out.push_str(")].clone()");
}
Expr::DictGetOr { dict, key, default } => {
emit_expr(out, dict, mode)?;
out.push_str(".get(&(");
emit_expr(out, key, mode)?;
out.push_str(")).cloned().unwrap_or(");
emit_expr(out, default, mode)?;
out.push(')');
}
Expr::DictContains { dict, key } => {
emit_expr(out, dict, mode)?;
out.push_str(".contains_key(&(");
emit_expr(out, key, mode)?;
out.push_str("))");
}
Expr::Len(inner) => {
emit_expr(out, inner, mode)?;
out.push_str(".len() as i64");
}
Expr::IfExpr {
cond,
then_expr,
else_expr,
} => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
Expr::LitStr(s) => {
write!(out, "String::from(\"{}\")", escape_ruchy_str(s))?;
}
Expr::QuotedString { .. } => {
return Err(RuchyCodegenError::Unsupported(
"Ruchy backend does not lower Expr::QuotedString — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs quoted shell strings; \
use `--target shell`"
.into(),
));
}
Expr::ShellVar(name) => {
return Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower Expr::ShellVar (${name}) — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable refs; \
use `--target shell`"
)));
}
Expr::CommandSubstitution(_) => {
return Err(RuchyCodegenError::Unsupported(
"Ruchy backend does not lower Expr::CommandSubstitution — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
use `--target shell`"
.into(),
));
}
Expr::ShellSpecial(name) => {
return Err(RuchyCodegenError::Unsupported(format!(
"Ruchy backend does not lower Expr::ShellSpecial (${name}) — \
contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
use `--target shell`"
)));
}
}
Ok(())
}
fn emit_unop(
out: &mut String,
op: UnOp,
operand: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
match op {
UnOp::Neg => {
if mode {
write!(out, "(-")?;
emit_expr(out, operand, mode)?;
write!(out, ")")?;
} else {
write!(out, "(")?;
emit_expr(out, operand, mode)?;
write!(
out,
").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
)?;
}
}
UnOp::Not => {
write!(out, "(!")?;
emit_expr(out, operand, mode)?;
write!(out, ")")?;
}
}
Ok(())
}
fn emit_call(
out: &mut String,
callee: &str,
args: &[Expr],
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "{}(", callee)?;
for (i, a) in args.iter().enumerate() {
if i > 0 {
write!(out, ", ")?;
}
emit_expr(out, a, mode)?;
}
write!(out, ")")?;
Ok(())
}
fn emit_if_expr(
out: &mut String,
cond: &Expr,
then_expr: &Expr,
else_expr: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "if ")?;
emit_expr(out, cond, mode)?;
write!(out, " {{ ")?;
emit_expr(out, then_expr, mode)?;
write!(out, " }} else ")?;
match else_expr {
Expr::IfExpr {
cond: c2,
then_expr: t2,
else_expr: e2,
} => emit_if_expr(out, c2, t2, e2, mode),
_ => {
write!(out, "{{ ")?;
emit_expr(out, else_expr, mode)?;
write!(out, " }}")?;
Ok(())
}
}
}
fn emit_binop(
out: &mut String,
op: BinOp,
lhs: &Expr,
rhs: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
match op {
BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
}
}
fn emit_bigint_floor_call(
out: &mut String,
method: &str,
lhs: &Expr,
rhs: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "xpile_bigint::{method}(&")?;
emit_expr(out, lhs, mode)?;
write!(out, ", &")?;
emit_expr(out, rhs, mode)?;
write!(out, ")")?;
Ok(())
}
fn emit_checked_pow(
out: &mut String,
lhs: &Expr,
rhs: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "(")?;
emit_expr(out, lhs, mode)?;
write!(out, ").checked_pow(u32::try_from(")?;
emit_expr(out, rhs, mode)?;
write!(
out,
").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\")"
)?;
Ok(())
}
fn emit_checked_shift(
out: &mut String,
lhs: &Expr,
method: &str,
rhs: &Expr,
op_name: &str,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "(")?;
emit_expr(out, lhs, mode)?;
write!(out, ").{method}(u32::try_from(")?;
emit_expr(out, rhs, mode)?;
write!(
out,
").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\")"
)?;
Ok(())
}
fn emit_checked(
out: &mut String,
lhs: &Expr,
method: &str,
rhs: &Expr,
op_name: &str,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "(")?;
emit_expr(out, lhs, mode)?;
write!(out, ").{method}(")?;
emit_expr(out, rhs, mode)?;
write!(
out,
").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
)?;
Ok(())
}
fn emit_infix(
out: &mut String,
lhs: &Expr,
op: &str,
rhs: &Expr,
mode: bool,
) -> Result<(), RuchyCodegenError> {
write!(out, "(")?;
emit_expr(out, lhs, mode)?;
out.push_str(op);
emit_expr(out, rhs, mode)?;
write!(out, ")")?;
Ok(())
}
pub struct RuchyBackend;
impl Backend for RuchyBackend {
fn name(&self) -> &'static str {
"ruchy"
}
fn targets(&self) -> &[Target] {
&[Target::Ruchy]
}
fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
Ok(Artifact {
primary,
sidecars: Vec::new(),
citations: Vec::new(),
quorum_status: QuorumStatus::Single {
emitter: "xpile-ruchy-codegen".to_string(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use xpile_meta_hir::{Module, SourceLang};
fn module_with(name: &str, items: Vec<Item>) -> Module {
Module {
name: name.into(),
source_lang: SourceLang::Python,
items,
ffi_boundaries: Vec::new(),
}
}
fn add_fn() -> Function {
Function {
name: "add".into(),
params: vec![
Param {
name: "a".into(),
ty: Type::I64,
mutable: false,
},
Param {
name: "b".into(),
ty: Type::I64,
mutable: false,
},
],
return_type: Type::I64,
body: Block {
stmts: vec![],
trailing_return: Expr::BinOp {
op: BinOp::Add,
lhs: Box::new(Expr::Ident("a".into())),
rhs: Box::new(Expr::Ident("b".into())),
},
},
}
}
#[test]
fn emits_fun_keyword_not_pub_fn() {
let m = module_with("fixture", vec![Item::Function(add_fn())]);
let ruchy = emit_module(&m).expect("emit ok");
assert!(
ruchy.contains("fun add("),
"Ruchy uses `fun`, not `fn` or `pub fn`: got\n{}",
ruchy
);
assert!(
!ruchy.contains("pub fn"),
"Ruchy emission must not produce `pub fn` (that's Rust)"
);
assert!(
ruchy.contains("checked_add"),
"expected checked_add: {ruchy}"
);
assert!(ruchy.contains("C-PY-INT-ARITH"));
}
#[test]
fn ruchy_floordiv_also_uses_div_euclid() {
let f = Function {
name: "fdiv".into(),
params: vec![
Param {
name: "a".into(),
ty: Type::I64,
mutable: false,
},
Param {
name: "b".into(),
ty: Type::I64,
mutable: false,
},
],
return_type: Type::I64,
body: Block {
stmts: vec![],
trailing_return: Expr::BinOp {
op: BinOp::FloorDiv,
lhs: Box::new(Expr::Ident("a".into())),
rhs: Box::new(Expr::Ident("b".into())),
},
},
};
let m = module_with("fixture", vec![Item::Function(f)]);
let ruchy = emit_module(&m).expect("emit ok");
assert!(ruchy.contains("div_euclid"));
assert!(!ruchy.contains(" / "));
}
}