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:
Generated
+10
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user