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 matches!(f.return_type, Type::BigInt) {
68 return true;
69 }
70 if f.params.iter().any(|p| matches!(p.ty, Type::BigInt)) {
71 return true;
72 }
73 fn stmt_has_bigint(s: &Stmt) -> bool {
74 match s {
75 Stmt::Let { ty, .. } => matches!(ty, Type::BigInt),
76 Stmt::Assign { .. } | Stmt::Assert { .. } => false,
77 Stmt::While { body, .. } | Stmt::ForEach { body, .. } => {
78 body.iter().any(stmt_has_bigint)
79 }
80 Stmt::ListAppend { .. } => false,
82 Stmt::IndexAssign { .. } => false,
84 Stmt::Cmd { .. } => false,
87 Stmt::Pipeline { .. } => false,
89 Stmt::ShellLoop { .. } => false,
91 Stmt::ShellAssign { .. } => false,
93 }
94 }
95 f.body.stmts.iter().any(stmt_has_bigint)
96}
97
98fn emit_contract_citations(out: &mut String, f: &Function) -> Result<(), RuchyCodegenError> {
101 for id in f.applicable_contracts() {
102 writeln!(out, "// xpile-contract: {id}")?;
103 }
104 Ok(())
105}
106
107fn emit_block(out: &mut String, block: &Block, mode: bool) -> Result<(), RuchyCodegenError> {
108 for stmt in &block.stmts {
109 emit_stmt(out, stmt, mode)?;
110 }
111 write!(out, " ")?;
112 emit_expr(out, &block.trailing_return, mode)?;
113 writeln!(out)?;
114 Ok(())
115}
116
117fn emit_stmt(out: &mut String, stmt: &Stmt, mode: bool) -> Result<(), RuchyCodegenError> {
118 emit_stmt_indented(out, stmt, " ", mode)
119}
120
121fn emit_stmt_indented(
122 out: &mut String,
123 stmt: &Stmt,
124 indent: &str,
125 mode: bool,
126) -> Result<(), RuchyCodegenError> {
127 match stmt {
128 Stmt::Let {
129 name,
130 ty,
131 value,
132 mutable,
133 } => {
134 let kw = if *mutable { "let mut" } else { "let" };
135 write!(out, "{indent}{kw} {name}: ")?;
136 emit_type(out, ty)?;
137 write!(out, " = ")?;
138 emit_expr(out, value, mode)?;
139 writeln!(out, ";")?;
140 Ok(())
141 }
142 Stmt::Assign { name, value } => {
143 write!(out, "{indent}{name} = ")?;
144 emit_expr(out, value, mode)?;
145 writeln!(out, ";")?;
146 Ok(())
147 }
148 Stmt::While { cond, body } => {
149 write!(out, "{indent}while ")?;
150 emit_expr(out, cond, mode)?;
151 writeln!(out, " {{")?;
152 let inner = format!("{indent} ");
153 for s in body {
154 emit_stmt_indented(out, s, &inner, mode)?;
155 }
156 writeln!(out, "{indent}}}")?;
157 Ok(())
158 }
159 Stmt::ForEach {
162 var, iter, body, ..
163 } => {
164 write!(out, "{indent}for {var} in ")?;
165 emit_expr(out, iter, mode)?;
166 writeln!(out, ".iter().cloned() {{")?;
167 let inner = format!("{indent} ");
168 for s in body {
169 emit_stmt_indented(out, s, &inner, mode)?;
170 }
171 writeln!(out, "{indent}}}")?;
172 Ok(())
173 }
174 Stmt::ListAppend { list_name, elem } => {
176 write!(out, "{indent}{list_name}.push(")?;
177 emit_expr(out, elem, mode)?;
178 writeln!(out, ");")?;
179 Ok(())
180 }
181 Stmt::IndexAssign {
184 list_name,
185 index,
186 value,
187 } => {
188 write!(out, "{indent}{list_name}[")?;
189 emit_expr(out, index, mode)?;
190 out.push_str(" as usize] = ");
191 emit_expr(out, value, mode)?;
192 writeln!(out, ";")?;
193 Ok(())
194 }
195 Stmt::Assert { cond } => {
196 write!(out, "{indent}assert!(")?;
197 emit_expr(out, cond, mode)?;
198 writeln!(out, ");")?;
199 Ok(())
200 }
201 Stmt::Cmd { program, args } => Err(RuchyCodegenError::Unsupported(format!(
206 "Ruchy backend does not lower Stmt::Cmd (`{program}` with {} arg(s)) — \
207 contract C-BASHRS-POSIX-IDEMPOTENCE governs this construct; \
208 use `--target shell` to emit POSIX sh via bashrs-backend",
209 args.len()
210 ))),
211 Stmt::Pipeline { stages } => Err(RuchyCodegenError::Unsupported(format!(
213 "Ruchy backend does not lower Stmt::Pipeline ({} stages) — \
214 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell pipelines; \
215 use `--target shell`",
216 stages.len()
217 ))),
218 Stmt::ShellLoop { .. } => Err(RuchyCodegenError::Unsupported(
220 "Ruchy backend does not lower Stmt::ShellLoop — \
221 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell loops; \
222 use `--target shell`"
223 .into(),
224 )),
225 Stmt::ShellAssign { name, .. } => Err(RuchyCodegenError::Unsupported(format!(
227 "Ruchy backend does not lower Stmt::ShellAssign (`{name}=…`) — \
228 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable assignment; \
229 use `--target shell`"
230 ))),
231 }
232}
233
234fn emit_param(out: &mut String, p: &Param) -> Result<(), RuchyCodegenError> {
235 if p.mutable {
237 write!(out, "mut ")?;
238 }
239 write!(out, "{}: ", p.name)?;
240 emit_type(out, &p.ty)?;
241 Ok(())
242}
243
244fn escape_ruchy_str(s: &str) -> String {
247 let mut out = String::with_capacity(s.len());
248 for c in s.chars() {
249 match c {
250 '\\' => out.push_str("\\\\"),
251 '"' => out.push_str("\\\""),
252 other => out.push(other),
253 }
254 }
255 out
256}
257
258fn emit_type(out: &mut String, t: &Type) -> Result<(), RuchyCodegenError> {
259 match t {
260 Type::I64 => out.push_str("i64"),
261 Type::Bool => out.push_str("bool"),
262 Type::BigInt => out.push_str("xpile_bigint::BigInt"),
264 Type::Str => out.push_str("String"),
267 Type::List(elem_ty) => {
269 out.push_str("Vec<");
270 emit_type(out, elem_ty)?;
271 out.push('>');
272 }
273 Type::Dict(k_ty, v_ty) => {
275 out.push_str("std::collections::HashMap<");
276 emit_type(out, k_ty)?;
277 out.push_str(", ");
278 emit_type(out, v_ty)?;
279 out.push('>');
280 }
281 Type::ShellString | Type::ExitCode => {
283 return Err(RuchyCodegenError::Unsupported(format!(
284 "Ruchy backend does not lower {t:?} — \
285 contract C-BASHRS-POSIX-IDEMPOTENCE governs the bashrs type domain; \
286 use `--target shell`"
287 )));
288 }
289 }
290 Ok(())
291}
292
293fn emit_expr(out: &mut String, e: &Expr, mode: bool) -> Result<(), RuchyCodegenError> {
294 match e {
295 Expr::Ident(name) => {
296 if mode {
302 write!(out, "{}.clone()", name)?;
303 } else {
304 write!(out, "{}", name)?;
305 }
306 }
307 Expr::LitInt(v) => {
308 if mode {
309 write!(out, "xpile_bigint::BigInt::from({}i64)", v)?;
310 } else {
311 write!(out, "{}i64", v)?;
312 }
313 }
314 Expr::LitBool(b) => write!(out, "{}", b)?,
317 Expr::BinOp { op, lhs, rhs } => emit_binop(out, *op, lhs, rhs, mode)?,
318 Expr::Concat { lhs, rhs } => {
322 out.push_str("format!(\"{}{}\", ");
323 emit_expr(out, lhs, mode)?;
324 out.push_str(", ");
325 emit_expr(out, rhs, mode)?;
326 out.push(')');
327 }
328 Expr::ListLit(elems) => {
330 out.push_str("vec![");
331 for (i, e) in elems.iter().enumerate() {
332 if i > 0 {
333 out.push_str(", ");
334 }
335 emit_expr(out, e, mode)?;
336 }
337 out.push(']');
338 }
339 Expr::DictLit(pairs) => {
341 out.push_str("{ let mut m = std::collections::HashMap::new(); ");
342 for (k, v) in pairs {
343 out.push_str("m.insert(");
344 emit_expr(out, k, mode)?;
345 out.push_str(", ");
346 emit_expr(out, v, mode)?;
347 out.push_str("); ");
348 }
349 out.push_str("m }");
350 }
351 Expr::Index { collection, index } => {
354 emit_expr(out, collection, mode)?;
355 out.push('[');
356 emit_expr(out, index, mode)?;
357 out.push_str(" as usize].clone()");
358 }
359 Expr::Len(inner) => {
361 emit_expr(out, inner, mode)?;
362 out.push_str(".len() as i64");
363 }
364 Expr::IfExpr {
365 cond,
366 then_expr,
367 else_expr,
368 } => emit_if_expr(out, cond, then_expr, else_expr, mode)?,
369 Expr::Call { callee, args } => emit_call(out, callee, args, mode)?,
370 Expr::UnOp { op, operand } => emit_unop(out, *op, operand, mode)?,
371 Expr::LitStr(s) => {
375 write!(out, "String::from(\"{}\")", escape_ruchy_str(s))?;
376 }
377 Expr::QuotedString { .. } => {
380 return Err(RuchyCodegenError::Unsupported(
381 "Ruchy backend does not lower Expr::QuotedString — \
382 contract C-BASHRS-POSIX-IDEMPOTENCE governs quoted shell strings; \
383 use `--target shell`"
384 .into(),
385 ));
386 }
387 Expr::ShellVar(name) => {
389 return Err(RuchyCodegenError::Unsupported(format!(
390 "Ruchy backend does not lower Expr::ShellVar (${name}) — \
391 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell variable refs; \
392 use `--target shell`"
393 )));
394 }
395 Expr::CommandSubstitution(_) => {
397 return Err(RuchyCodegenError::Unsupported(
398 "Ruchy backend does not lower Expr::CommandSubstitution — \
399 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell substitution; \
400 use `--target shell`"
401 .into(),
402 ));
403 }
404 Expr::ShellSpecial(name) => {
406 return Err(RuchyCodegenError::Unsupported(format!(
407 "Ruchy backend does not lower Expr::ShellSpecial (${name}) — \
408 contract C-BASHRS-POSIX-IDEMPOTENCE governs shell special params; \
409 use `--target shell`"
410 )));
411 }
412 }
413 Ok(())
414}
415
416fn emit_unop(
417 out: &mut String,
418 op: UnOp,
419 operand: &Expr,
420 mode: bool,
421) -> Result<(), RuchyCodegenError> {
422 match op {
423 UnOp::Neg => {
424 if mode {
425 write!(out, "(-")?;
427 emit_expr(out, operand, mode)?;
428 write!(out, ")")?;
429 } else {
430 write!(out, "(")?;
434 emit_expr(out, operand, mode)?;
435 write!(
436 out,
437 ").checked_neg().expect(\"xpile: i64 negation overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
438 )?;
439 }
440 }
441 UnOp::Not => {
442 write!(out, "(!")?;
443 emit_expr(out, operand, mode)?;
444 write!(out, ")")?;
445 }
446 }
447 Ok(())
448}
449
450fn emit_call(
451 out: &mut String,
452 callee: &str,
453 args: &[Expr],
454 mode: bool,
455) -> Result<(), RuchyCodegenError> {
456 write!(out, "{}(", callee)?;
457 for (i, a) in args.iter().enumerate() {
458 if i > 0 {
459 write!(out, ", ")?;
460 }
461 emit_expr(out, a, mode)?;
462 }
463 write!(out, ")")?;
464 Ok(())
465}
466
467fn emit_if_expr(
470 out: &mut String,
471 cond: &Expr,
472 then_expr: &Expr,
473 else_expr: &Expr,
474 mode: bool,
475) -> Result<(), RuchyCodegenError> {
476 write!(out, "if ")?;
477 emit_expr(out, cond, mode)?;
478 write!(out, " {{ ")?;
479 emit_expr(out, then_expr, mode)?;
480 write!(out, " }} else ")?;
481 match else_expr {
482 Expr::IfExpr {
483 cond: c2,
484 then_expr: t2,
485 else_expr: e2,
486 } => emit_if_expr(out, c2, t2, e2, mode),
487 _ => {
488 write!(out, "{{ ")?;
489 emit_expr(out, else_expr, mode)?;
490 write!(out, " }}")?;
491 Ok(())
492 }
493 }
494}
495
496fn emit_binop(
508 out: &mut String,
509 op: BinOp,
510 lhs: &Expr,
511 rhs: &Expr,
512 mode: bool,
513) -> Result<(), RuchyCodegenError> {
514 match op {
515 BinOp::Add if mode => emit_infix(out, lhs, " + ", rhs, mode),
516 BinOp::Sub if mode => emit_infix(out, lhs, " - ", rhs, mode),
517 BinOp::Mul if mode => emit_infix(out, lhs, " * ", rhs, mode),
518 BinOp::FloorDiv if mode => emit_bigint_floor_call(out, "div_floor", lhs, rhs, mode),
519 BinOp::Mod if mode => emit_bigint_floor_call(out, "mod_floor", lhs, rhs, mode),
520 BinOp::BitAnd if mode => emit_infix(out, lhs, " & ", rhs, mode),
523 BinOp::BitOr if mode => emit_infix(out, lhs, " | ", rhs, mode),
524 BinOp::BitXor if mode => emit_infix(out, lhs, " ^ ", rhs, mode),
525 BinOp::Shl if mode => emit_bigint_floor_call(out, "shl", lhs, rhs, mode),
526 BinOp::Shr if mode => emit_bigint_floor_call(out, "shr", lhs, rhs, mode),
527 BinOp::Pow if mode => emit_bigint_floor_call(out, "pow", lhs, rhs, mode),
528 BinOp::Add => emit_checked(out, lhs, "checked_add", rhs, "addition", mode),
529 BinOp::Sub => emit_checked(out, lhs, "checked_sub", rhs, "subtraction", mode),
530 BinOp::Mul => emit_checked(out, lhs, "checked_mul", rhs, "multiplication", mode),
531 BinOp::FloorDiv => emit_checked(out, lhs, "checked_div_euclid", rhs, "floor-div", mode),
532 BinOp::Mod => emit_checked(out, lhs, "checked_rem_euclid", rhs, "modulo", mode),
533 BinOp::Eq => emit_infix(out, lhs, " == ", rhs, mode),
534 BinOp::NotEq => emit_infix(out, lhs, " != ", rhs, mode),
535 BinOp::Lt => emit_infix(out, lhs, " < ", rhs, mode),
536 BinOp::LtEq => emit_infix(out, lhs, " <= ", rhs, mode),
537 BinOp::Gt => emit_infix(out, lhs, " > ", rhs, mode),
538 BinOp::GtEq => emit_infix(out, lhs, " >= ", rhs, mode),
539 BinOp::And => emit_infix(out, lhs, " && ", rhs, mode),
540 BinOp::Or => emit_infix(out, lhs, " || ", rhs, mode),
541 BinOp::BitAnd => emit_infix(out, lhs, " & ", rhs, mode),
542 BinOp::BitOr => emit_infix(out, lhs, " | ", rhs, mode),
543 BinOp::BitXor => emit_infix(out, lhs, " ^ ", rhs, mode),
544 BinOp::Shl => emit_checked_shift(out, lhs, "checked_shl", rhs, "left-shift", mode),
545 BinOp::Shr => emit_checked_shift(out, lhs, "checked_shr", rhs, "right-shift", mode),
546 BinOp::Pow => emit_checked_pow(out, lhs, rhs, mode),
547 }
548}
549
550fn emit_bigint_floor_call(
554 out: &mut String,
555 method: &str,
556 lhs: &Expr,
557 rhs: &Expr,
558 mode: bool,
559) -> Result<(), RuchyCodegenError> {
560 write!(out, "xpile_bigint::{method}(&")?;
561 emit_expr(out, lhs, mode)?;
562 write!(out, ", &")?;
563 emit_expr(out, rhs, mode)?;
564 write!(out, ")")?;
565 Ok(())
566}
567
568fn emit_checked_pow(
569 out: &mut String,
570 lhs: &Expr,
571 rhs: &Expr,
572 mode: bool,
573) -> Result<(), RuchyCodegenError> {
574 write!(out, "(")?;
575 emit_expr(out, lhs, mode)?;
576 write!(out, ").checked_pow(u32::try_from(")?;
577 emit_expr(out, rhs, mode)?;
578 write!(
579 out,
580 ").expect(\"xpile: exponent out of range for u32 — Python returns Float for negative exponents which v0.1.0 cannot represent (contract C-PY-INT-ARITH)\")).expect(\"xpile: i64 power overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
581 )?;
582 Ok(())
583}
584
585fn emit_checked_shift(
586 out: &mut String,
587 lhs: &Expr,
588 method: &str,
589 rhs: &Expr,
590 op_name: &str,
591 mode: bool,
592) -> Result<(), RuchyCodegenError> {
593 write!(out, "(")?;
594 emit_expr(out, lhs, mode)?;
595 write!(out, ").{method}(u32::try_from(")?;
596 emit_expr(out, rhs, mode)?;
597 write!(
598 out,
599 ").expect(\"xpile: shift amount out of range for u32 (contract C-PY-INT-ARITH)\")).expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
600 )?;
601 Ok(())
602}
603
604fn emit_checked(
605 out: &mut String,
606 lhs: &Expr,
607 method: &str,
608 rhs: &Expr,
609 op_name: &str,
610 mode: bool,
611) -> Result<(), RuchyCodegenError> {
612 write!(out, "(")?;
613 emit_expr(out, lhs, mode)?;
614 write!(out, ").{method}(")?;
615 emit_expr(out, rhs, mode)?;
616 write!(
617 out,
618 ").expect(\"xpile: i64 {op_name} overflow; bigint promotion (contract C-PY-INT-ARITH slow path) not yet implemented\")"
619 )?;
620 Ok(())
621}
622
623fn emit_infix(
624 out: &mut String,
625 lhs: &Expr,
626 op: &str,
627 rhs: &Expr,
628 mode: bool,
629) -> Result<(), RuchyCodegenError> {
630 write!(out, "(")?;
631 emit_expr(out, lhs, mode)?;
632 out.push_str(op);
633 emit_expr(out, rhs, mode)?;
634 write!(out, ")")?;
635 Ok(())
636}
637
638pub struct RuchyBackend;
639
640impl Backend for RuchyBackend {
641 fn name(&self) -> &'static str {
642 "ruchy"
643 }
644
645 fn targets(&self) -> &[Target] {
646 &[Target::Ruchy]
647 }
648
649 fn lower(&self, module: &Module, _config: &BackendConfig) -> Result<Artifact, BackendError> {
650 let primary = emit_module(module).map_err(|e| BackendError::Lower(e.to_string()))?;
651 Ok(Artifact {
652 primary,
653 sidecars: Vec::new(),
654 citations: Vec::new(),
655 quorum_status: QuorumStatus::Single {
656 emitter: "xpile-ruchy-codegen".to_string(),
657 },
658 })
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use xpile_meta_hir::{Module, SourceLang};
666
667 fn module_with(name: &str, items: Vec<Item>) -> Module {
668 Module {
669 name: name.into(),
670 source_lang: SourceLang::Python,
671 items,
672 ffi_boundaries: Vec::new(),
673 }
674 }
675
676 fn add_fn() -> Function {
677 Function {
678 name: "add".into(),
679 params: vec![
680 Param {
681 name: "a".into(),
682 ty: Type::I64,
683 mutable: false,
684 },
685 Param {
686 name: "b".into(),
687 ty: Type::I64,
688 mutable: false,
689 },
690 ],
691 return_type: Type::I64,
692 body: Block {
693 stmts: vec![],
694 trailing_return: Expr::BinOp {
695 op: BinOp::Add,
696 lhs: Box::new(Expr::Ident("a".into())),
697 rhs: Box::new(Expr::Ident("b".into())),
698 },
699 },
700 }
701 }
702
703 #[test]
704 fn emits_fun_keyword_not_pub_fn() {
705 let m = module_with("fixture", vec![Item::Function(add_fn())]);
706 let ruchy = emit_module(&m).expect("emit ok");
707 assert!(
708 ruchy.contains("fun add("),
709 "Ruchy uses `fun`, not `fn` or `pub fn`: got\n{}",
710 ruchy
711 );
712 assert!(
713 !ruchy.contains("pub fn"),
714 "Ruchy emission must not produce `pub fn` (that's Rust)"
715 );
716 assert!(
720 ruchy.contains("checked_add"),
721 "expected checked_add: {ruchy}"
722 );
723 assert!(ruchy.contains("C-PY-INT-ARITH"));
724 }
725
726 #[test]
727 fn ruchy_floordiv_also_uses_div_euclid() {
728 let f = Function {
729 name: "fdiv".into(),
730 params: vec![
731 Param {
732 name: "a".into(),
733 ty: Type::I64,
734 mutable: false,
735 },
736 Param {
737 name: "b".into(),
738 ty: Type::I64,
739 mutable: false,
740 },
741 ],
742 return_type: Type::I64,
743 body: Block {
744 stmts: vec![],
745 trailing_return: Expr::BinOp {
746 op: BinOp::FloorDiv,
747 lhs: Box::new(Expr::Ident("a".into())),
748 rhs: Box::new(Expr::Ident("b".into())),
749 },
750 },
751 };
752 let m = module_with("fixture", vec![Item::Function(f)]);
753 let ruchy = emit_module(&m).expect("emit ok");
754 assert!(ruchy.contains("div_euclid"));
755 assert!(!ruchy.contains(" / "));
756 }
757}