1use std::fmt::Write;
14use xpile_backend::{Artifact, Backend, BackendConfig, BackendError, QuorumStatus, Target};
15use xpile_meta_hir::{BinOp, Block, Expr, Function, Item, Module, Param, Stmt, Type, UnOp};
16
17#[derive(Debug, thiserror::Error)]
18pub enum RuchyCodegenError {
19 #[error("unsupported item: {0}")]
20 Unsupported(String),
21 #[error("formatting error: {0}")]
22 Format(#[from] std::fmt::Error),
23}
24
25pub fn emit_module(module: &Module) -> Result<String, RuchyCodegenError> {
26 let mut out = String::new();
27 writeln!(
28 out,
29 "// xpile-generated from {:?} module {}",
30 module.source_lang, module.name
31 )?;
32 writeln!(out)?;
33 for item in &module.items {
34 match item {
35 Item::Function(f) => emit_function(&mut out, f)?,
36 }
37 }
38 Ok(out)
39}
40
41fn emit_function(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
42 emit_contract_citations(out, f)?;
43 write!(out, "fun {}(", f.name)?;
45 for (i, p) in f.params.iter().enumerate() {
46 if i > 0 {
47 write!(out, ", ")?;
48 }
49 emit_param(out, p)?;
50 }
51 write!(out, ") -> ")?;
52 emit_type(out, f.return_type)?;
53 writeln!(out, " {{")?;
54 let mode = function_bigint_mode(f);
55 emit_block(out, &f.body, mode)?;
56 writeln!(out, "}}")?;
57 Ok(())
58}
59
60fn function_bigint_mode(f: &Function) -> bool {
67 if f.return_type == Type::BigInt {
68 return true;
69 }
70 if f.params.iter().any(|p| p.ty == Type::BigInt) {
71 return true;
72 }
73 fn stmt_has_bigint(s: &Stmt) -> bool {
74 match s {
75 Stmt::Let { ty, .. } => *ty == Type::BigInt,
76 Stmt::Assign { .. } | Stmt::Assert { .. } => false,
77 Stmt::While { body, .. } => body.iter().any(stmt_has_bigint),
78 Stmt::Cmd { .. } => false,
81 Stmt::Pipeline { .. } => false,
83 Stmt::ShellLoop { .. } => false,
85 Stmt::ShellAssign { .. } => false,
87 }
88 }
89 f.body.stmts.iter().any(stmt_has_bigint)
90}
91
92fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
95 for id in f.applicable_contracts() {
96 writeln!(out, "// xpile-contract: {id}")?;
97 }
98 Ok(())
99}
100
101fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), RuchyCodegenError> {
102 for stmt in &block.stmts {
103 emit_stmt(out, stmt, mode)?;
104 }
105 write!(out, " ")?;
106 emit_expr(out, &block.trailing_return, mode)?;
107 writeln!(out)?;
108 Ok(())
109}
110
111fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), RuchyCodegenError> {
112 emit_stmt_indented(out, stmt, " ", mode)
113}
114
115fn emit_stmt_indented(
116 out: &mut String,
117 stmt: &Stmt,
118 indent: &str,
119 mode: bool,
120) -> Result<(), RuchyCodegenError> {
121 match stmt {
122 Stmt::Let {
123 name,
124 ty,
125 value,
126 mutable,
127 } => {
128 let kw = if *mutable { "let mut" } else { "let" };
129 write!(out, "{indent}{kw} {name}: ")?;
130 emit_type(out, *ty)?;
131 write!(out, " = ")?;
132 emit_expr(out, value, mode)?;
133 writeln!(out, ";")?;
134 Ok(())
135 }
136 Stmt::Assign { name, value } => {
137 write!(out, "{indent}{name} = ")?;
138 emit_expr(out, value, mode)?;
139 writeln!(out, ";")?;
140 Ok(())
141 }
142 Stmt::While { cond, body } => {
143 write!(out, "{indent}while ")?;
144 emit_expr(out, cond, mode)?;
145 writeln!(out, " {{")?;
146 let inner = format!("{indent} ");
147 for s in body {
148 emit_stmt_indented(out, s, &inner, mode)?;
149 }
150 writeln!(out, "{indent}}}")?;
151 Ok(())
152 }
153 Stmt::Assert { cond } => {
154 write!(out, "{indent}assert!(")?;
155 emit_expr(out, cond, mode)?;
156 writeln!(out, ");")?;
157 Ok(())
158 }
159 Stmt::Cmd { program, args } => Err(RuchyCodegenError::Unsupported(format!(
164 "Ruchy backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
165 contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
166 use `--target shell` to emit POSIX sh via bashrs-backend",
167 args.len()
168 ))),
169 Stmt::Pipeline { stages } => Err(RuchyCodegenError::Unsupported(format!(
171 "Ruchy backend does not lower Stmt::Pipeline ({} stages) — \
172 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
173 use `--target shell`",
174 stages.len()
175 ))),
176 Stmt::ShellLoop { .. } => Err(RuchyCodegenError::Unsupported(
178 "Ruchy backend does not lower Stmt::ShellLoop — \
179 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
180 use `--target shell`"
181 .into(),
182 )),
183 Stmt::ShellAssign { name, .. } => Err(RuchyCodegenError::Unsupported(format!(
185 "Ruchy backend does not lower Stmt::ShellAssign (`{name}=…`) — \
186 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
187 use `--target shell`"
188 ))),
189 }
190}
191
192fn emit_param(out: &mut String, p: &Param) -> Result<(), RuchyCodegenError> {
193 write!(out, "{}: ", p.name)?;
194 emit_type(out, p.ty)?;
195 Ok(())
196}
197
198fn emit_type(out: &mut String, t: Type) -> Result<(), RuchyCodegenError> {
199 out.push_str(match t {
200 Type::I64 => "i64",
201 Type::Bool => "bool",
202 Type::BigInt => "xpile_bigint::BigInt",
204 Type::ShellString | Type::ExitCode => {
206 return Err(RuchyCodegenError::Unsupported(format!(
207 "Ruchy backend does not lower {t:?} — \
208 contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
209 use `--target shell`"
210 )));
211 }
212 });
213 Ok(())
214}
215
216fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), RuchyCodegenError> {
217 match e {
218 Expr::Ident(name) => {
219 if mode {
225 write!(out, "{}.clone()", name)?;
226 } else {
227 write!(out, "{}", name)?;
228 }
229 }
230 Expr::LitInt(v) => {
231 if mode {
232 write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
233 } else {
234 write!(out, "{}i64", v)?;
235 }
236 }
237 Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
238 Expr::IfExpr {
239 cond,
240 then_expr,
241 else_expr,
242 } => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
243 Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
244 Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
245 Expr::LitStr(_) | Expr::QuotedString { .. } => {
247 return Err(RuchyCodegenError::Unsupported(
248 "Ruchy backend does not lower Expr::LitStr / Expr::QuotedString — \
249 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell string literals; \
250 use `--target shell`"
251 .into(),
252 ));
253 }
254 Expr::ShellVar(name) => {
256 return Err(RuchyCodegenError::Unsupported(format!(
257 "Ruchy backend does not lower Expr::ShellVar (${name}) — \
258 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable refs; \
259 use `--target shell`"
260 )));
261 }
262 Expr::CommandSubstitution(_) => {
264 return Err(RuchyCodegenError::Unsupported(
265 "Ruchy backend does not lower Expr::CommandSubstitution — \
266 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
267 use `--target shell`"
268 .into(),
269 ));
270 }
271 Expr::ShellSpecial(name) => {
273 return Err(RuchyCodegenError::Unsupported(format!(
274 "Ruchy backend does not lower Expr::ShellSpecial (${name}) — \
275 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
276 use `--target shell`"
277 )));
278 }
279 }
280 Ok(())
281}
282
283fn emit_unop(
284 out: &mut String,
285 op: UnOp,
286 operand: &Expr,
287 mode: bool,
288) -> Result<(), RuchyCodegenError> {
289 match op {
290 UnOp::Neg => {
291 if mode {
292 write!(out, "(-")?;
294 emit_expr(out, operand, mode)?;
295 write!(out, ")")?;
296 } else {
297 write!(out, "(")?;
301 emit_expr(out, operand, mode)?;
302 write!(
303 out,
304 ").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
305 )?;
306 }
307 }
308 UnOp::Not => {
309 write!(out, "(!")?;
310 emit_expr(out, operand, mode)?;
311 write!(out, ")")?;
312 }
313 }
314 Ok(())
315}
316
317fn emit_call(
318 out: &mut String,
319 callee: &str,
320 args: &[Expr],
321 mode: bool,
322) -> Result<(), RuchyCodegenError> {
323 write!(out, "{}(", callee)?;
324 for (i, a) in args.iter().enumerate() {
325 if i > 0 {
326 write!(out, ", ")?;
327 }
328 emit_expr(out, a, mode)?;
329 }
330 write!(out, ")")?;
331 Ok(())
332}
333
334fn emit_if_expr(
337 out: &mut String,
338 cond: &Expr,
339 then_expr: &Expr,
340 else_expr: &Expr,
341 mode: bool,
342) -> Result<(), RuchyCodegenError> {
343 write!(out, "if ")?;
344 emit_expr(out, cond, mode)?;
345 write!(out, " {{ ")?;
346 emit_expr(out, then_expr, mode)?;
347 write!(out, " }} else ")?;
348 match else_expr {
349 Expr::IfExpr {
350 cond: c2,
351 then_expr: t2,
352 else_expr: e2,
353 } => emit_if_expr(out, c2, t2, e2, mode),
354 _ => {
355 write!(out, "{{ ")?;
356 emit_expr(out, else_expr, mode)?;
357 write!(out, " }}")?;
358 Ok(())
359 }
360 }
361}
362
363fn emit_binop(
375 out: &mut String,
376 op: BinOp,
377 lhs: &Expr,
378 rhs: &Expr,
379 mode: bool,
380) -> Result<(), RuchyCodegenError> {
381 match op {
382 BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
383 BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
384 BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
385 BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
386 BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
387 BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
390 BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
391 BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
392 BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
393 BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
394 BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
395 BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
396 BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
397 BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
398 BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
399 BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
400 BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
401 BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
402 BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
403 BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
404 BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
405 BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
406 BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
407 BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
408 BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
409 BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
410 BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
411 BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
412 BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
413 BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
414 }
415}
416
417fn emit_bigint_floor_call(
421 out: &mut String,
422 method: &str,
423 lhs: &Expr,
424 rhs: &Expr,
425 mode: bool,
426) -> Result<(), RuchyCodegenError> {
427 write!(out, "xpile_bigint::{method}(&")?;
428 emit_expr(out, lhs, mode)?;
429 write!(out, ", &")?;
430 emit_expr(out, rhs, mode)?;
431 write!(out, ")")?;
432 Ok(())
433}
434
435fn emit_checked_pow(
436 out: &mut String,
437 lhs: &Expr,
438 rhs: &Expr,
439 mode: bool,
440) -> Result<(), RuchyCodegenError> {
441 write!(out, "(")?;
442 emit_expr(out, lhs, mode)?;
443 write!(out, ").checked_pow(u32::try_from(")?;
444 emit_expr(out, rhs, mode)?;
445 write!(
446 out,
447 ").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\")"
448 )?;
449 Ok(())
450}
451
452fn emit_checked_shift(
453 out: &mut String,
454 lhs: &Expr,
455 method: &str,
456 rhs: &Expr,
457 op_name: &str,
458 mode: bool,
459) -> Result<(), RuchyCodegenError> {
460 write!(out, "(")?;
461 emit_expr(out, lhs, mode)?;
462 write!(out, ").{method}(u32::try_from(")?;
463 emit_expr(out, rhs, mode)?;
464 write!(
465 out,
466 ").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\")"
467 )?;
468 Ok(())
469}
470
471fn emit_checked(
472 out: &mut String,
473 lhs: &Expr,
474 method: &str,
475 rhs: &Expr,
476 op_name: &str,
477 mode: bool,
478) -> Result<(), RuchyCodegenError> {
479 write!(out, "(")?;
480 emit_expr(out, lhs, mode)?;
481 write!(out, ").{method}(")?;
482 emit_expr(out, rhs, mode)?;
483 write!(
484 out,
485 ").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
486 )?;
487 Ok(())
488}
489
490fn emit_infix(
491 out: &mut String,
492 lhs: &Expr,
493 op: &str,
494 rhs: &Expr,
495 mode: bool,
496) -> Result<(), RuchyCodegenError> {
497 write!(out, "(")?;
498 emit_expr(out, lhs, mode)?;
499 out.push_str(op);
500 emit_expr(out, rhs, mode)?;
501 write!(out, ")")?;
502 Ok(())
503}
504
505pub struct RuchyBackend;
506
507impl Backend for RuchyBackend {
508 fn name(&self) -> &'static str {
509 "ruchy"
510 }
511
512 fn targets(&self) -> &[Target] {
513 &[Target::Ruchy]
514 }
515
516 fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
517 let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
518 Ok(Artifact {
519 primary,
520 sidecars: Vec::new(),
521 citations: Vec::new(),
522 quorum_status: QuorumStatus::Single {
523 emitter: "xpile-ruchy-codegen".to_string(),
524 },
525 })
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use xpile_meta_hir::{Module, SourceLang};
533
534 fn module_with(name: &str, items: Vec<Item>) -> Module {
535 Module {
536 name: name.into(),
537 source_lang: SourceLang::Python,
538 items,
539 ffi_boundaries: Vec::new(),
540 }
541 }
542
543 fn add_fn() -> Function {
544 Function {
545 name: "add".into(),
546 params: vec![
547 Param {
548 name: "a".into(),
549 ty: Type::I64,
550 },
551 Param {
552 name: "b".into(),
553 ty: Type::I64,
554 },
555 ],
556 return_type: Type::I64,
557 body: Block {
558 stmts: vec![],
559 trailing_return: Expr::BinOp {
560 op: BinOp::Add,
561 lhs: Box::new(Expr::Ident("a".into())),
562 rhs: Box::new(Expr::Ident("b".into())),
563 },
564 },
565 }
566 }
567
568 #[test]
569 fn emits_fun_keyword_not_pub_fn() {
570 let m = module_with("fixture", vec![Item::Function(add_fn())]);
571 let ruchy = emit_module(&m).expect("emit ok");
572 assert!(
573 ruchy.contains("fun add("),
574 "Ruchy uses `fun`, not `fn` or `pub fn`: got\n{}",
575 ruchy
576 );
577 assert!(
578 !ruchy.contains("pub fn"),
579 "Ruchy emission must not produce `pub fn` (that's Rust)"
580 );
581 assert!(
585 ruchy.contains("checked_add"),
586 "expected checked_add: {ruchy}"
587 );
588 assert!(ruchy.contains("C-PY-INT-ARITH"));
589 }
590
591 #[test]
592 fn ruchy_floordiv_also_uses_div_euclid() {
593 let f = Function {
594 name: "fdiv".into(),
595 params: vec![
596 Param {
597 name: "a".into(),
598 ty: Type::I64,
599 },
600 Param {
601 name: "b".into(),
602 ty: Type::I64,
603 },
604 ],
605 return_type: Type::I64,
606 body: Block {
607 stmts: vec![],
608 trailing_return: Expr::BinOp {
609 op: BinOp::FloorDiv,
610 lhs: Box::new(Expr::Ident("a".into())),
611 rhs: Box::new(Expr::Ident("b".into())),
612 },
613 },
614 };
615 let m = module_with("fixture", vec![Item::Function(f)]);
616 let ruchy = emit_module(&m).expect("emit ok");
617 assert!(ruchy.contains("div_euclid"));
618 assert!(!ruchy.contains(" / "));
619 }
620}