feat(charka): charka-codegen — emisión de Rust desde el IR

La etapa final del transpilador. generate(&Ir) -> String produce un
fuente Rust (un main.rs) que, compilado contra charka-runtime, ejecuta
la lógica del programa COBOL.

- struct Program con un campo Num/Text por dato elemental; new() lo
  inicializa desde las cláusulas VALUE.
- Un método p_<párrafo> por párrafo del PROCEDURE; run() los encadena
  en orden (el «caer» de COBOL); main() construye y corre.
- Cada Stmt -> código Rust: MOVE->.store/.fill, DISPLAY->println!,
  COMPUTE y aritmética -> expresiones Decimal, IF->if/else,
  PERFORM-> llamada / for / while, STOP RUN->process::exit.
- Tolerante: lo no transpilable (Stmt::Unknown, dato sin resolver, **)
  se emite como comentario // charka: — el código generado compila.
- Saneado de identificadores COBOL->Rust (choques con keywords).
- Verificado de punta a punta: un programa COBOL demo transpila a Rust
  que compila contra charka-runtime y produce la salida esperada.
- Módulos: emit / sym / expr / stmt. 14 tests; fmt + clippy limpios.

El pipeline COBOL->Rust corre de punta a punta. Falta sólo
charka-shadow (validador en sombra).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 20:36:26 +00:00
parent 85156c1509
commit e52b3fb572
10 changed files with 1237 additions and 4 deletions
@@ -0,0 +1,170 @@
//! Emisión de expresiones y condiciones: cada nodo del IR se convierte
//! en un fragmento de código Rust (un `String`).
use charka_ir::{BinOp, CmpOp, Cond, Expr, Figurative, Operand};
use crate::sym::{FieldKind, Symbols};
/// Un literal de texto Rust con las comillas y los escapes adecuados.
pub(crate) fn rust_str(s: &str) -> String {
let mut out = String::from("\"");
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
/// El texto que representa una constante figurativa.
pub(crate) fn figurative_text(f: Figurative) -> &'static str {
match f {
Figurative::Zero => "0",
Figurative::Space => " ",
Figurative::Quote => "\"",
Figurative::HighValue | Figurative::LowValue | Figurative::Null => "",
}
}
/// El carácter de relleno de una figurativa, para `Text::fill`.
pub(crate) fn figurative_fill(f: Figurative) -> char {
match f {
Figurative::Zero => '0',
Figurative::Quote => '"',
_ => ' ',
}
}
/// Un operando como expresión de tipo `Decimal`.
pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String {
match op {
Operand::Num(n) => format!("dec({})", rust_str(n)),
Operand::Str(s) => format!(
"Decimal::parse({}).unwrap_or_else(|_| Decimal::zero())",
rust_str(s)
),
Operand::Figurative(_) => "Decimal::zero()".to_string(),
Operand::Data(name) => match sym.lookup(name) {
Some(f) => match f.kind {
FieldKind::Num { .. } => format!("self.{}.value()", f.ident),
FieldKind::Text { .. } => format!(
"Decimal::parse(self.{}.display().trim()).unwrap_or_else(|_| Decimal::zero())",
f.ident
),
},
None => format!("Decimal::zero() /* charka: dato no resuelto {name} */"),
},
}
}
/// Un operando como expresión de tipo `&str` (para texto).
pub(crate) fn operand_str(sym: &Symbols, op: &Operand) -> String {
match op {
Operand::Str(s) => rust_str(s),
Operand::Num(n) => rust_str(n),
Operand::Figurative(f) => rust_str(figurative_text(*f)),
Operand::Data(name) => match sym.lookup(name) {
Some(f) => format!("self.{}.display().as_str()", f.ident),
None => format!("\"\" /* charka: dato no resuelto {name} */"),
},
}
}
/// Un operando como expresión que implementa `Display` (para `DISPLAY`).
pub(crate) fn operand_display(sym: &Symbols, op: &Operand) -> String {
match op {
Operand::Str(s) => rust_str(s),
Operand::Num(n) => rust_str(n),
Operand::Figurative(f) => rust_str(figurative_text(*f)),
Operand::Data(name) => match sym.lookup(name) {
Some(f) => format!("self.{}.display()", f.ident),
None => format!("\"\" /* charka: dato no resuelto {name} */"),
},
}
}
/// Emite una expresión aritmética como código Rust de tipo `Decimal`.
pub(crate) fn emit_expr(sym: &Symbols, e: &Expr) -> String {
match e {
Expr::Operand(op) => operand_decimal(sym, op),
Expr::Neg(inner) => format!("Decimal::zero().sub(&({}))", emit_expr(sym, inner)),
Expr::Binary { op, lhs, rhs } => {
let l = emit_expr(sym, lhs);
let r = emit_expr(sym, rhs);
match op {
BinOp::Add => format!("({l}).add(&({r}))"),
BinOp::Sub => format!("({l}).sub(&({r}))"),
BinOp::Mul => format!("({l}).mul(&({r}))"),
BinOp::Div => format!(
"({l}).div(&({r}), 9, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())"
),
BinOp::Pow => "Decimal::zero() /* charka: ** no soportado */".to_string(),
}
}
}
}
/// Emite una condición como código Rust de tipo `bool`.
pub(crate) fn emit_cond(sym: &Symbols, c: &Cond) -> String {
match c {
Cond::Compare { lhs, op, rhs } => emit_compare(sym, lhs, *op, rhs),
Cond::Named(name) => {
format!("false /* charka: condición 88 no soportada: {name} */")
}
Cond::Not(inner) => format!("!({})", emit_cond(sym, inner)),
Cond::And(a, b) => format!("({}) && ({})", emit_cond(sym, a), emit_cond(sym, b)),
Cond::Or(a, b) => format!("({}) || ({})", emit_cond(sym, a), emit_cond(sym, b)),
}
}
/// Emite una comparación: numérica si ambos lados lo son, alfanumérica
/// si alguno es texto.
fn emit_compare(sym: &Symbols, lhs: &Operand, op: CmpOp, rhs: &Operand) -> String {
if is_text_operand(sym, lhs) || is_text_operand(sym, rhs) {
let method = match op {
CmpOp::Eq => "is_eq",
CmpOp::Ne => "is_ne",
CmpOp::Lt => "is_lt",
CmpOp::Gt => "is_gt",
CmpOp::Le => "is_le",
CmpOp::Ge => "is_ge",
};
format!(
"cobol_text_cmp({}, {}).{method}()",
operand_str(sym, lhs),
operand_str(sym, rhs)
)
} else {
let rust_op = match op {
CmpOp::Eq => "==",
CmpOp::Ne => "!=",
CmpOp::Lt => "<",
CmpOp::Gt => ">",
CmpOp::Le => "<=",
CmpOp::Ge => ">=",
};
format!(
"({}) {rust_op} ({})",
operand_decimal(sym, lhs),
operand_decimal(sym, rhs)
)
}
}
/// ¿El operando es alfanumérico?
fn is_text_operand(sym: &Symbols, op: &Operand) -> bool {
match op {
Operand::Str(_) => true,
Operand::Data(name) => matches!(
sym.lookup(name).map(|f| &f.kind),
Some(FieldKind::Text { .. })
),
_ => false,
}
}