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
Generated
+10
View File
@@ -2315,6 +2315,16 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "charka-codegen"
version = "0.1.0"
dependencies = [
"charka-bcd",
"charka-ir",
"charka-lexer",
"charka-parser",
]
[[package]]
name = "charka-ir"
version = "0.1.0"
+1
View File
@@ -161,6 +161,7 @@ members = [
"crates/modules/charka/charka-parser",
"crates/modules/charka/charka-ir",
"crates/modules/charka/charka-runtime",
"crates/modules/charka/charka-codegen",
# ============================================================
# modules/mirada/ — Compositor Wayland
+26 -4
View File
@@ -15,6 +15,7 @@ embebido, dialectos IBM Enterprise) es un esfuerzo multi-mes.
| `charka-parser` | lib | Parser COBOL'85 (subconjunto): tokens → AST (`Program`) |
| `charka-ir` | lib | Representación intermedia: el AST con los statements del PROCEDURE ya tipados |
| `charka-runtime` | lib | Soporte de ejecución de los programas transpilados: campos `Num` y `Text` |
| `charka-codegen` | lib | Emisión de Rust: IR → fuente Rust sobre `charka-runtime` |
## charka-bcd
@@ -113,16 +114,37 @@ los listaba al revés): el codegen emite llamadas contra esta API, así
que el runtime debe existir primero — y es un crate autocontenido,
verificable sin depender del código emitido.
## charka-codegen
La etapa final: `generate(&Ir) -> String` produce un fuente Rust (un
`main.rs`) que, compilado contra `charka-runtime`, ejecuta la lógica
del programa COBOL.
- Un `struct Program` con un campo por cada dato elemental (`Num` /
`Text`); `Program::new()` lo inicializa desde las cláusulas `VALUE`.
- Un método `p_<párrafo>(&mut self)` por párrafo; `run()` los encadena
en orden (el «caer» de COBOL); `main()` construye y corre.
- Cada `Stmt` → líneas Rust: `MOVE`→`.store`, `DISPLAY`→`println!`,
`COMPUTE`/aritmética → expresiones `Decimal`, `IF`→`if`, `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 siempre compila.
- Verificado de punta a punta: un programa COBOL de demostración
transpila a Rust que compila contra `charka-runtime` y produce la
salida correcta.
- Fuera de alcance v1: grupos como campo propio, `REDEFINES`,
`OCCURS`/tablas, `PERFORM ... THRU` como rango, E/S de ficheros.
## Estado
`charka-bcd` (22 tests), `charka-lexer` (17 tests), `charka-parser`
(15 tests), `charka-ir` (17 tests) y `charka-runtime` (17 tests)
implementados y verdes. **Pendiente** — el resto del transpilador
(Fase D del plan macro):
(15 tests), `charka-ir` (17 tests), `charka-runtime` (17 tests) y
`charka-codegen` (14 tests) implementados y verdes. El pipeline
COBOL→Rust corre de punta a punta. **Pendiente** — el último crate:
| crate pendiente | rol |
| ----------------- | ---------------------------------------------------- |
| `charka-codegen` | emisión de Rust (IR → fuente Rust sobre el runtime) |
| `charka-shadow` | validador en sombra (original vs transpilado) |
Hito intermedio sugerido: subconjunto COBOL'85 puro antes de CICS/SQL.
@@ -0,0 +1,17 @@
[package]
name = "charka-codegen"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "charka-codegen — emisión de Rust desde el IR de charka: el modelo de datos COBOL y los statements del PROCEDURE a código que enlaza con charka-runtime."
[dependencies]
charka-ir = { path = "../charka-ir" }
charka-bcd = { path = "../charka-bcd" }
[dev-dependencies]
charka-parser = { path = "../charka-parser" }
charka-lexer = { path = "../charka-lexer" }
@@ -0,0 +1,45 @@
//! `Emitter` — un acumulador de código Rust con control de sangría.
/// Acumula líneas de código Rust generado, llevando la sangría actual.
pub(crate) struct Emitter {
out: String,
depth: usize,
}
impl Emitter {
pub(crate) fn new() -> Self {
Self {
out: String::new(),
depth: 0,
}
}
/// Escribe una línea con la sangría actual.
pub(crate) fn line(&mut self, s: &str) {
for _ in 0..self.depth {
self.out.push_str(" ");
}
self.out.push_str(s);
self.out.push('\n');
}
/// Una línea en blanco.
pub(crate) fn blank(&mut self) {
self.out.push('\n');
}
/// Aumenta un nivel de sangría.
pub(crate) fn indent(&mut self) {
self.depth += 1;
}
/// Reduce un nivel de sangría.
pub(crate) fn dedent(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
/// Entrega el código acumulado.
pub(crate) fn finish(self) -> String {
self.out
}
}
@@ -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,
}
}
@@ -0,0 +1,344 @@
//! `charka-codegen` — emisión de Rust desde el IR del transpilador.
//!
//! Etapa final del pipeline COBOL→Rust: toma el [`Ir`] de `charka-ir`
//! y produce un fuente Rust (un `String`) que, compilado contra
//! `charka-runtime`, ejecuta la lógica del programa COBOL original.
//!
//! La forma del código emitido:
//!
//! - Un `struct Program` con un campo por cada dato elemental — `Num`
//! para los numéricos, `Text` para los alfanuméricos.
//! - `Program::new()` inicializa los campos desde sus cláusulas `VALUE`.
//! - Un método `p_<párrafo>(&mut self)` por cada párrafo del PROCEDURE.
//! - `run()` los encadena en orden (el «caer» de COBOL); `main()`
//! construye el `Program` y lo corre.
//!
//! Es **tolerante**: lo que no sabe transpilar (un `Stmt::Unknown`, un
//! dato sin resolver, `**`) se emite como un comentario `// charka:` —
//! el código generado siempre compila.
//!
//! Alcance v1 — fuera: grupos como campo propio, `REDEFINES`,
//! `OCCURS`/tablas, `PERFORM ... THRU` como rango, E/S de ficheros,
//! `EVALUATE`, CICS y SQL embebido.
#![forbid(unsafe_code)]
mod emit;
mod expr;
mod stmt;
mod sym;
use std::collections::HashMap;
use charka_ir::{Ir, Procedure};
use emit::Emitter;
use expr::rust_str;
use stmt::emit_stmt;
use sym::{paragraph_method, Field, FieldKind, Symbols};
/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
pub fn generate(ir: &Ir) -> String {
let sym = Symbols::build(&ir.data);
let mut em = Emitter::new();
emit_header(&mut em);
emit_struct(&mut em, &sym);
emit_impl(&mut em, &sym, ir);
emit_main(&mut em);
em.finish()
}
/// El preámbulo: doc, `allow`s, el `use` del runtime y el helper `dec`.
fn emit_header(em: &mut Emitter) {
em.line("//! Generado por charka — transpilador COBOL → Rust.");
em.line("//! No editar a mano: regenerar desde el fuente COBOL.");
em.blank();
em.line(
"#![allow(dead_code, unused_mut, unused_variables, unused_parens, \
unreachable_code, clippy::all)]",
);
em.blank();
em.line("use charka_runtime::*;");
em.blank();
em.line("/// Construye un `Decimal` desde un literal numérico COBOL.");
em.line("fn dec(s: &str) -> Decimal {");
em.line(" Decimal::parse(s).expect(\"charka: literal numérico inválido\")");
em.line("}");
em.blank();
}
/// El `struct Program` con un campo por dato elemental.
fn emit_struct(em: &mut Emitter, sym: &Symbols) {
em.line("/// El estado del programa: un campo por cada dato elemental.");
em.line("struct Program {");
em.indent();
for f in &sym.fields {
let ty = match f.kind {
FieldKind::Num { .. } => "Num",
FieldKind::Text { .. } => "Text",
};
em.line(&format!("{}: {ty},", f.ident));
}
em.dedent();
em.line("}");
em.blank();
}
/// El bloque `impl Program`: `new`, los párrafos y `run`.
fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) {
em.line("impl Program {");
em.indent();
// new()
em.line("fn new() -> Self {");
em.indent();
em.line("Self {");
em.indent();
for f in &sym.fields {
em.line(&format!("{}: {},", f.ident, field_init(f)));
}
em.dedent();
em.line("}");
em.dedent();
em.line("}");
em.blank();
// Un método por párrafo.
let methods = paragraph_methods(ir);
for (name, proc) in &methods {
em.line(&format!("fn {name}(&mut self) {{"));
em.indent();
for s in &proc.body {
emit_stmt(em, sym, s);
}
em.dedent();
em.line("}");
em.blank();
}
// run() — encadena los párrafos en orden.
em.line("fn run(&mut self) {");
em.indent();
if methods.is_empty() {
em.line("// programa sin PROCEDURE division");
}
for (name, _) in &methods {
em.line(&format!("self.{name}();"));
}
em.dedent();
em.line("}");
em.dedent();
em.line("}");
em.blank();
}
/// El `fn main`.
fn emit_main(em: &mut Emitter) {
em.line("fn main() {");
em.indent();
em.line("Program::new().run();");
em.dedent();
em.line("}");
}
/// El inicializador de un campo, a partir de su cláusula `VALUE`.
fn field_init(f: &Field) -> String {
match &f.kind {
FieldKind::Num { int, frac, signed } => format!(
"Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
rust_str(&numeric_value(f.value.as_deref()))
),
FieldKind::Text { len } => format!(
"Text::with_value({len}, {})",
rust_str(&text_value(f.value.as_deref()))
),
}
}
/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
fn numeric_value(v: Option<&str>) -> String {
let Some(raw) = v else {
return "0".to_string();
};
let up = raw.to_uppercase();
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
return "0".to_string();
}
if charka_bcd::Decimal::parse(raw).is_ok() {
raw.to_string()
} else {
"0".to_string()
}
}
/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
/// literales de texto en comillas simples; aquí se desenvuelven.
fn text_value(v: Option<&str>) -> String {
let Some(raw) = v else {
return String::new();
};
let up = raw.to_uppercase();
if matches!(up.as_str(), "SPACE" | "SPACES") {
return String::new();
}
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
return "0".to_string();
}
if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
raw[1..raw.len() - 1].to_string()
} else {
raw.to_string()
}
}
/// Asigna a cada párrafo un nombre de método único.
fn paragraph_methods(ir: &Ir) -> Vec<(String, &Procedure)> {
let mut seen: HashMap<String, u32> = HashMap::new();
let mut out = Vec::new();
for proc in &ir.procedures {
let base = paragraph_method(&proc.name);
let n = seen.entry(base.clone()).or_insert(0);
let name = if *n > 0 { format!("{base}_{n}") } else { base };
*n += 1;
out.push((name, proc));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
/// Helper: lexa, parsea, baja a IR y transpila un fuente COBOL.
fn gen(src: &str) -> String {
let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).expect("lex");
let prog = charka_parser::parse(&toks).expect("parse");
let ir = charka_ir::lower(&prog);
generate(&ir)
}
/// Un programa COBOL de demostración, razonablemente completo.
const DEMO: &str = "IDENTIFICATION DIVISION.\n\
PROGRAM-ID. DEMO.\n\
DATA DIVISION.\n\
WORKING-STORAGE SECTION.\n\
01 WS-A PIC 9(3) VALUE 10.\n\
01 WS-B PIC 9(3).\n\
01 WS-NAME PIC X(8) VALUE 'BOB'.\n\
PROCEDURE DIVISION.\n\
MAIN-PARA.\n\
MOVE 5 TO WS-B.\n\
COMPUTE WS-B = WS-A + WS-B.\n\
DISPLAY 'B=' WS-B.\n\
IF WS-B > 0 DISPLAY 'POS' END-IF.\n\
PERFORM SUB-PARA.\n\
STOP RUN.\n\
SUB-PARA.\n\
DISPLAY WS-NAME.\n";
#[test]
fn header_and_main_are_emitted() {
let out = gen(DEMO);
assert!(out.contains("use charka_runtime::*;"));
assert!(out.contains("fn dec(s: &str) -> Decimal {"));
assert!(out.contains("fn main() {"));
assert!(out.contains("Program::new().run();"));
}
#[test]
fn numeric_field_becomes_num() {
let out = gen(DEMO);
assert!(out.contains("ws_a: Num,"));
assert!(out.contains("Num::with_value(Picture::new(3, 0, false), \"10\")"));
}
#[test]
fn alphanumeric_field_becomes_text() {
let out = gen(DEMO);
assert!(out.contains("ws_name: Text,"));
assert!(out.contains("Text::with_value(8, \"BOB\")"));
}
#[test]
fn move_emits_a_store() {
assert!(gen(DEMO).contains("self.ws_b.store(dec(\"5\"));"));
}
#[test]
fn compute_emits_the_expression() {
let out = gen(DEMO);
assert!(out.contains("self.ws_b.store((self.ws_a.value()).add(&(self.ws_b.value())));"));
}
#[test]
fn display_emits_a_println() {
let out = gen(DEMO);
assert!(out.contains("println!(\"{}{}\", \"B=\", self.ws_b.display());"));
}
#[test]
fn if_emits_a_rust_if() {
let out = gen(DEMO);
assert!(out.contains("if (self.ws_b.value()) > (dec(\"0\")) {"));
}
#[test]
fn paragraphs_become_methods_and_run_chains_them() {
let out = gen(DEMO);
assert!(out.contains("fn p_main_para(&mut self) {"));
assert!(out.contains("fn p_sub_para(&mut self) {"));
assert!(out.contains("fn run(&mut self) {"));
assert!(out.contains("self.p_main_para();"));
assert!(out.contains("self.p_sub_para();"));
}
#[test]
fn perform_calls_the_paragraph_method() {
assert!(gen(DEMO).contains("self.p_sub_para();"));
}
#[test]
fn stop_run_exits() {
assert!(gen(DEMO).contains("std::process::exit(0);"));
}
#[test]
fn unknown_verb_becomes_a_comment() {
let out = gen("PROCEDURE DIVISION.\n\
MAIN.\n\
INSPECT WS-X TALLYING WS-N FOR ALL ' '.\n");
assert!(out.contains("// charka: verbo no transpilado — INSPECT"));
}
#[test]
fn add_giving_emits_a_sum() {
let out = gen("DATA DIVISION.\n\
WORKING-STORAGE SECTION.\n\
01 A PIC 9(3).\n\
01 B PIC 9(3).\n\
01 C PIC 9(3).\n\
PROCEDURE DIVISION.\n\
MAIN.\n\
ADD A B GIVING C.\n");
assert!(out.contains("self.c.store((self.a.value()).add(&(self.b.value())));"));
}
#[test]
fn perform_times_emits_a_loop() {
let out = gen("PROCEDURE DIVISION.\n\
MAIN.\n\
PERFORM SUB 3 TIMES.\n\
SUB.\n\
CONTINUE.\n");
assert!(out.contains("for _ in 0..3usize {"));
}
#[test]
fn empty_program_still_compiles_shape() {
let out = gen("");
assert!(out.contains("struct Program {"));
assert!(out.contains("fn main() {"));
assert!(out.contains("fn run(&mut self) {"));
}
}
@@ -0,0 +1,364 @@
//! Emisión de los statements del PROCEDURE: cada [`Stmt`] se traduce a
//! una o varias líneas de código Rust sobre `charka-runtime`.
use charka_ir::{Operand, Perform, PerformControl, PerformTarget, Stmt};
use crate::emit::Emitter;
use crate::expr::{
emit_cond, emit_expr, figurative_fill, operand_decimal, operand_display, operand_str,
};
use crate::sym::{paragraph_method, FieldKind, Symbols};
/// Emite un statement.
pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) {
match stmt {
Stmt::Move { from, to } => emit_move(em, sym, from, to),
Stmt::Display { items } => emit_display(em, sym, items),
Stmt::Accept { into } => {
em.line(&format!(
"// charka: ACCEPT {into} — entrada interactiva no soportada en v1"
));
}
Stmt::Compute {
targets,
rounded,
expr,
} => {
let value = emit_expr(sym, expr);
for t in targets {
emit_store(em, sym, t, &value, *rounded);
}
}
Stmt::Add {
addends,
to,
giving,
rounded,
} => emit_add(em, sym, addends, to, giving, *rounded),
Stmt::Subtract {
amounts,
from,
giving,
rounded,
} => emit_subtract(em, sym, amounts, from, giving, *rounded),
Stmt::Multiply {
left,
by,
giving,
rounded,
} => emit_multiply(em, sym, left, by, giving, *rounded),
Stmt::Divide {
left,
right,
by_form,
giving,
rounded,
} => emit_divide(em, sym, left, right, *by_form, giving, *rounded),
Stmt::If {
cond,
then_branch,
else_branch,
} => {
em.line(&format!("if {} {{", emit_cond(sym, cond)));
em.indent();
emit_block(em, sym, then_branch);
em.dedent();
if else_branch.is_empty() {
em.line("}");
} else {
em.line("} else {");
em.indent();
emit_block(em, sym, else_branch);
em.dedent();
em.line("}");
}
}
Stmt::Perform(p) => emit_perform(em, sym, p),
Stmt::GoTo { target } => {
em.line(&format!(
"self.{}(); return; // charka: GO TO (aproximado)",
paragraph_method(target)
));
}
Stmt::StopRun | Stmt::Goback => em.line("std::process::exit(0);"),
Stmt::Exit => em.line("return;"),
Stmt::Continue => em.line("// CONTINUE"),
Stmt::Unknown { verb, .. } => {
em.line(&format!("// charka: verbo no transpilado — {verb}"));
}
}
}
/// Emite una secuencia de statements (un cuerpo de bloque).
fn emit_block(em: &mut Emitter, sym: &Symbols, stmts: &[Stmt]) {
for s in stmts {
emit_stmt(em, sym, s);
}
}
/// Almacena un valor `Decimal` (texto de expresión) en un campo.
fn emit_store(em: &mut Emitter, sym: &Symbols, name: &str, value: &str, rounded: bool) {
match sym.lookup(name) {
Some(f) => match f.kind {
FieldKind::Num { .. } => {
let method = if rounded { "store_rounded" } else { "store" };
em.line(&format!("self.{}.{method}({value});", f.ident));
}
FieldKind::Text { .. } => {
em.line(&format!(
"self.{}.store(({value}).to_string().as_str());",
f.ident
));
}
},
None => em.line(&format!("// charka: destino no resuelto — {name}")),
}
}
fn emit_move(em: &mut Emitter, sym: &Symbols, from: &Operand, to: &[String]) {
for t in to {
match sym.lookup(t) {
Some(f) => match f.kind {
FieldKind::Num { .. } => {
em.line(&format!(
"self.{}.store({});",
f.ident,
operand_decimal(sym, from)
));
}
FieldKind::Text { .. } => {
if let Operand::Figurative(fig) = from {
em.line(&format!(
"self.{}.fill('{}');",
f.ident,
figurative_fill(*fig)
));
} else {
em.line(&format!(
"self.{}.store({});",
f.ident,
operand_str(sym, from)
));
}
}
},
None => em.line(&format!("// charka: destino MOVE no resuelto — {t}")),
}
}
}
fn emit_display(em: &mut Emitter, sym: &Symbols, items: &[Operand]) {
if items.is_empty() {
em.line("println!();");
return;
}
let placeholders = "{}".repeat(items.len());
let args: Vec<String> = items.iter().map(|o| operand_display(sym, o)).collect();
em.line(&format!(
"println!(\"{placeholders}\", {});",
args.join(", ")
));
}
/// La suma de una lista de operandos, encadenando `.add`.
fn fold_sum(sym: &Symbols, ops: &[Operand]) -> String {
let mut it = ops.iter();
let Some(first) = it.next() else {
return "Decimal::zero()".to_string();
};
let mut acc = operand_decimal(sym, first);
for o in it {
acc = format!("({acc}).add(&({}))", operand_decimal(sym, o));
}
acc
}
fn emit_add(
em: &mut Emitter,
sym: &Symbols,
addends: &[Operand],
to: &[String],
giving: &[String],
rounded: bool,
) {
let sum = fold_sum(sym, addends);
if !giving.is_empty() {
let base = match to.first() {
Some(first) => format!(
"({sum}).add(&({}))",
operand_decimal(sym, &Operand::Data(first.clone()))
),
None => sum,
};
for g in giving {
emit_store(em, sym, g, &base, rounded);
}
} else {
for t in to {
emit_inplace(em, sym, t, "add", &sum, rounded);
}
}
}
fn emit_subtract(
em: &mut Emitter,
sym: &Symbols,
amounts: &[Operand],
from: &[String],
giving: &[String],
rounded: bool,
) {
let sum = fold_sum(sym, amounts);
if !giving.is_empty() {
let minuend = from
.first()
.map(|f| operand_decimal(sym, &Operand::Data(f.clone())))
.unwrap_or_else(|| "Decimal::zero()".to_string());
let value = format!("({minuend}).sub(&({sum}))");
for g in giving {
emit_store(em, sym, g, &value, rounded);
}
} else {
for t in from {
emit_inplace(em, sym, t, "sub", &sum, rounded);
}
}
}
fn emit_multiply(
em: &mut Emitter,
sym: &Symbols,
left: &Operand,
by: &Operand,
giving: &[String],
rounded: bool,
) {
let l = operand_decimal(sym, left);
if !giving.is_empty() {
let value = format!("({l}).mul(&({}))", operand_decimal(sym, by));
for g in giving {
emit_store(em, sym, g, &value, rounded);
}
} else if let Operand::Data(name) = by {
// `MULTIPLY a BY b` sin GIVING: b queda con a*b.
emit_inplace(em, sym, name, "mul", &l, rounded);
} else {
em.line("// charka: MULTIPLY sin destino claro");
}
}
fn emit_divide(
em: &mut Emitter,
sym: &Symbols,
left: &Operand,
right: &Operand,
by_form: bool,
giving: &[String],
rounded: bool,
) {
// `a BY b` → a/b; `a INTO b` → b/a.
let (num, den) = if by_form {
(operand_decimal(sym, left), operand_decimal(sym, right))
} else {
(operand_decimal(sym, right), operand_decimal(sym, left))
};
if !giving.is_empty() {
for g in giving {
let value = format!(
"({num}).div(&({den}), {}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())",
target_scale(sym, g)
);
emit_store(em, sym, g, &value, rounded);
}
} else if let Operand::Data(name) = right {
// `DIVIDE a INTO b` sin GIVING: b queda con b/a.
let value = format!(
"({num}).div(&({den}), {}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())",
target_scale(sym, name)
);
emit_store(em, sym, name, &value, rounded);
} else {
em.line("// charka: DIVIDE sin destino claro");
}
}
/// Emite una operación aritmética en el lugar: `t = t <op> rhs`.
fn emit_inplace(em: &mut Emitter, sym: &Symbols, name: &str, op: &str, rhs: &str, rounded: bool) {
match sym.lookup(name) {
Some(f) if matches!(f.kind, FieldKind::Num { .. }) => {
let method = if rounded { "store_rounded" } else { "store" };
em.line(&format!(
"self.{0}.{method}(self.{0}.value().{op}(&({rhs})));",
f.ident
));
}
_ => em.line(&format!(
"// charka: destino aritmético no resuelto — {name}"
)),
}
}
/// La escala de redondeo de un destino numérico (sus dígitos
/// fraccionarios), o 4 por defecto.
fn target_scale(sym: &Symbols, name: &str) -> u8 {
match sym.lookup(name).map(|f| &f.kind) {
Some(FieldKind::Num { frac, .. }) => *frac,
_ => 4,
}
}
/// Una expresión `usize` para el número de repeticiones de un `PERFORM`.
fn count_expr(sym: &Symbols, op: &Operand) -> String {
match op {
Operand::Num(n) => match n.trim_start_matches('+').parse::<i128>() {
Ok(v) if v >= 0 => format!("{v}usize"),
_ => "0usize".to_string(),
},
_ => format!(
"(({}).rescale(0, Rounding::Truncate).mantissa().max(0) as usize)",
operand_decimal(sym, op)
),
}
}
fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) {
// Emite el "cuerpo": la llamada al párrafo o el bloque en línea.
let emit_body = |em: &mut Emitter, sym: &Symbols| match &p.target {
PerformTarget::Paragraph { name, thru } => {
let note = thru
.as_ref()
.map(|t| format!(" // charka: THRU {t} — rango no soportado"))
.unwrap_or_default();
em.line(&format!("self.{}();{note}", paragraph_method(name)));
}
PerformTarget::Inline(body) => emit_block(em, sym, body),
};
match &p.control {
PerformControl::Once => {
if matches!(p.target, PerformTarget::Inline(_)) {
em.line("{");
em.indent();
emit_body(em, sym);
em.dedent();
em.line("}");
} else {
emit_body(em, sym);
}
}
PerformControl::Times(n) => {
em.line(&format!("for _ in 0..{} {{", count_expr(sym, n)));
em.indent();
emit_body(em, sym);
em.dedent();
em.line("}");
}
PerformControl::Until(cond) => {
em.line(&format!("while !({}) {{", emit_cond(sym, cond)));
em.indent();
emit_body(em, sym);
em.dedent();
em.line("}");
}
}
}
@@ -0,0 +1,233 @@
//! Tabla de símbolos: el modelo de datos COBOL traducido a campos Rust.
use std::collections::HashMap;
use charka_ir::DataItem;
/// Un campo del struct `Program` generado.
pub(crate) struct Field {
/// Nombre COBOL en mayúsculas.
pub cobol: String,
/// Identificador Rust saneado y único.
pub ident: String,
/// Numérico o alfanumérico.
pub kind: FieldKind,
/// La cláusula `VALUE`, si la hay.
pub value: Option<String>,
}
/// El tipo de un campo elemental.
pub(crate) enum FieldKind {
/// Campo numérico — se emite como `Num`.
Num { int: u8, frac: u8, signed: bool },
/// Campo alfanumérico — se emite como `Text`.
Text { len: usize },
}
/// El conjunto de campos del programa, indexado por nombre COBOL.
pub(crate) struct Symbols {
pub fields: Vec<Field>,
by_name: HashMap<String, usize>,
}
impl Symbols {
/// Construye la tabla recorriendo el árbol de datos.
pub(crate) fn build(data: &[DataItem]) -> Self {
let mut fields = Vec::new();
collect(data, &mut fields);
dedup_idents(&mut fields);
let by_name = fields
.iter()
.enumerate()
.map(|(i, f)| (f.cobol.clone(), i))
.collect();
Self { fields, by_name }
}
/// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
pub(crate) fn lookup(&self, cobol: &str) -> Option<&Field> {
self.by_name
.get(&cobol.to_uppercase())
.map(|&i| &self.fields[i])
}
}
/// Recoge los datos elementales del árbol. Los grupos no son campos —
/// se recurre en sus hijos. Se saltan niveles 88/66 y los `FILLER`.
fn collect(items: &[DataItem], out: &mut Vec<Field>) {
for it in items {
if it.level == 88 || it.level == 66 {
continue;
}
if !it.children.is_empty() {
collect(&it.children, out);
continue;
}
if it.name == "FILLER" {
continue;
}
let Some(kind) = classify(it.picture.as_deref()) else {
continue;
};
out.push(Field {
cobol: it.name.clone(),
ident: sanitize_ident(&it.name),
kind,
value: it.value.clone(),
});
}
}
/// Dos datos pueden compartir nombre (COBOL los califica); aquí, como
/// son campos de un struct, sus identificadores deben ser únicos.
fn dedup_idents(fields: &mut [Field]) {
let mut seen: HashMap<String, u32> = HashMap::new();
for f in fields.iter_mut() {
let n = seen.entry(f.ident.clone()).or_insert(0);
if *n > 0 {
f.ident = format!("{}_{}", f.ident, n);
}
*n += 1;
}
}
/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
/// como texto de presentación.
fn classify(pic: Option<&str>) -> Option<FieldKind> {
let up = pic?.to_uppercase();
if up.contains('X') || up.contains('A') {
return Some(FieldKind::Text {
len: pic_width(&up).max(1),
});
}
if let Ok(p) = charka_bcd::Picture::parse(&up) {
return Some(FieldKind::Num {
int: p.integer_digits,
frac: p.fraction_digits,
signed: p.signed,
});
}
Some(FieldKind::Text {
len: pic_width(&up).max(1),
})
}
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
/// la repetición `C(n)`. `S` y `V` no ocupan posición.
fn pic_width(up: &str) -> usize {
let chars: Vec<char> = up.chars().collect();
let mut i = 0;
let mut total = 0usize;
while i < chars.len() {
let c = chars[i];
i += 1;
if c == 'S' || c == 'V' {
continue;
}
let mut count = 1usize;
if chars.get(i) == Some(&'(') {
i += 1;
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
count = n;
}
if chars.get(i) == Some(&')') {
i += 1;
}
}
total += count;
}
total
}
/// Convierte un nombre COBOL en un identificador Rust válido.
fn sanitize_ident(name: &str) -> String {
let mut s: String = name
.chars()
.map(|c| {
if c == '-' {
'_'
} else {
c.to_ascii_lowercase()
}
})
.filter(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if s.is_empty() || s.starts_with(|c: char| c.is_ascii_digit()) {
s = format!("f_{s}");
}
if is_rust_keyword(&s) {
s.push('_');
}
s
}
/// El nombre del método de un párrafo. El párrafo implícito ("") es
/// `p_start`; el resto lleva el prefijo `p_`.
pub(crate) fn paragraph_method(name: &str) -> String {
if name.is_empty() {
return "p_start".to_string();
}
let body: String = name
.chars()
.map(|c| {
if c == '-' {
'_'
} else {
c.to_ascii_lowercase()
}
})
.filter(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
format!("p_{body}")
}
/// ¿Es `s` una palabra reservada de Rust? (Para no chocar al nombrar
/// campos — un dato COBOL `MOVE` se volvería el keyword `move`.)
fn is_rust_keyword(s: &str) -> bool {
matches!(
s,
"as" | "break"
| "const"
| "continue"
| "crate"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "box"
| "yield"
)
}
+27
View File
@@ -3,6 +3,33 @@
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
del plan macro) — el parser COBOL completo es un esfuerzo multi-mes.
### feat(charka-codegen): emisión de Rust desde el IR
Crate nuevo `crates/modules/charka/charka-codegen` — la etapa final del
pipeline. `generate(&Ir) -> String` produce un fuente Rust (un
`main.rs`) que, compilado contra `charka-runtime`, ejecuta la lógica
del programa COBOL.
- Un `struct Program` con un campo por cada dato elemental — `Num`
para los numéricos, `Text` para los alfanuméricos; `Program::new()`
los inicializa desde sus cláusulas `VALUE`.
- Un método `p_<párrafo>(&mut self)` por cada párrafo del PROCEDURE;
`run()` los encadena en orden (el «caer» de COBOL); `main()`
construye el `Program` y lo corre.
- Cada `Stmt` → código Rust: `MOVE``.store`/`.fill`,
`DISPLAY``println!`, `COMPUTE` y la aritmética → expresiones
`Decimal`, `IF``if`/`else`, `PERFORM`→ llamada de método / `for` /
`while`, `GO TO`→ llamada + `return`, `STOP RUN``process::exit`.
- Tolerante: lo no transpilable (`Stmt::Unknown`, un dato sin resolver,
el operador `**`) se emite como comentario `// charka:`; el código
generado siempre compila.
- Saneado de identificadores COBOL→Rust (incl. choques con keywords).
- Verificado de punta a punta: un programa COBOL de demostración
transpila a Rust que compila contra `charka-runtime` y produce la
salida esperada (`COMPUTE`, `IF`, `PERFORM`, `DISPLAY`).
- 14 tests del fuente emitido; fuera de alcance v1: grupos como campo,
`REDEFINES`, `OCCURS`, `PERFORM ... THRU` como rango, E/S.
### feat(charka-runtime): soporte de ejecución — campos Num y Text
Crate nuevo `crates/modules/charka/charka-runtime` — el soporte que los