diff --git a/Cargo.lock b/Cargo.lock index b7b8723..7d6017e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5c76c2f..8385186 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index f83fa41..9f661fe 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -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_(&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. diff --git a/crates/modules/charka/charka-codegen/Cargo.toml b/crates/modules/charka/charka-codegen/Cargo.toml new file mode 100644 index 0000000..53c919b --- /dev/null +++ b/crates/modules/charka/charka-codegen/Cargo.toml @@ -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" } diff --git a/crates/modules/charka/charka-codegen/src/emit.rs b/crates/modules/charka/charka-codegen/src/emit.rs new file mode 100644 index 0000000..2e9032a --- /dev/null +++ b/crates/modules/charka/charka-codegen/src/emit.rs @@ -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 + } +} diff --git a/crates/modules/charka/charka-codegen/src/expr.rs b/crates/modules/charka/charka-codegen/src/expr.rs new file mode 100644 index 0000000..4d99c83 --- /dev/null +++ b/crates/modules/charka/charka-codegen/src/expr.rs @@ -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, + } +} diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs new file mode 100644 index 0000000..e7a151c --- /dev/null +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -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_(&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 = 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) {")); + } +} diff --git a/crates/modules/charka/charka-codegen/src/stmt.rs b/crates/modules/charka/charka-codegen/src/stmt.rs new file mode 100644 index 0000000..13d6803 --- /dev/null +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -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 = 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 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::() { + 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("}"); + } + } +} diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs new file mode 100644 index 0000000..cf530b3 --- /dev/null +++ b/crates/modules/charka/charka-codegen/src/sym.rs @@ -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, +} + +/// 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, + by_name: HashMap, +} + +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) { + 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 = 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 { + 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 = 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::().parse::() { + 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" + ) +} diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 9702cf9..457aacb 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -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_(&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