diff --git a/Cargo.lock b/Cargo.lock index 10c610b..5ed4ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2307,6 +2307,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "charka" +version = "0.1.0" +dependencies = [ + "anyhow", + "charka-codegen", + "charka-ir", + "charka-lexer", + "charka-parser", + "charka-shadow", + "clap", +] + [[package]] name = "charka-bcd" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 859a60a..afb1d65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -298,6 +298,7 @@ members = [ "crates/apps/mirada-launcher", "crates/apps/mirada-portal", "crates/apps/mirada-greeter", + "crates/apps/charka", ] [workspace.package] diff --git a/crates/apps/charka/Cargo.toml b/crates/apps/charka/Cargo.toml new file mode 100644 index 0000000..5b0e0f4 --- /dev/null +++ b/crates/apps/charka/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "charka" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "charka — CLI del transpilador COBOL→Rust: transpila, ejecuta y valida programas COBOL." + +[[bin]] +name = "charka" +path = "src/main.rs" + +[dependencies] +charka-lexer = { path = "../../modules/charka/charka-lexer" } +charka-parser = { path = "../../modules/charka/charka-parser" } +charka-ir = { path = "../../modules/charka/charka-ir" } +charka-codegen = { path = "../../modules/charka/charka-codegen" } +charka-shadow = { path = "../../modules/charka/charka-shadow" } +clap = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/apps/charka/src/main.rs b/crates/apps/charka/src/main.rs new file mode 100644 index 0000000..58fa4cc --- /dev/null +++ b/crates/apps/charka/src/main.rs @@ -0,0 +1,305 @@ +//! `charka` — la CLI del transpilador COBOL → Rust. +//! +//! Envuelve el pipeline (lexer → parser → IR → codegen) y el validador +//! en sombra en cuatro comandos: +//! +//! - `transpile` — emite el código Rust de un fuente COBOL. +//! - `scaffold` — genera un crate Rust completo y compilable. +//! - `run` — ejecuta el programa (intérprete sombra) y lo imprime. +//! - `check` — ejecuta y compara la salida contra un archivo dado. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +use anyhow::{Context, Result}; +use charka_ir::{Ir, PerformTarget, Stmt}; +use clap::{Parser, Subcommand}; + +/// Ruta a `charka-runtime`, fijada al compilar — el crate generado por +/// `scaffold` la usa como dependencia. +const RUNTIME_PATH: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../modules/charka/charka-runtime" +); + +/// El transpilador de COBOL a Rust. +#[derive(Parser)] +#[command(name = "charka", version, about = "Transpilador COBOL → Rust")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Transpila un fuente COBOL a código Rust. + Transpile { + /// El fuente COBOL (.cob), en formato libre. + input: PathBuf, + /// Archivo de salida; si se omite, va a la salida estándar. + #[arg(short, long)] + output: Option, + }, + /// Genera un crate Rust completo y compilable. + Scaffold { + /// El fuente COBOL (.cob). + input: PathBuf, + /// El directorio del crate a crear. + #[arg(short, long)] + output: PathBuf, + }, + /// Ejecuta un programa COBOL (intérprete sombra) y muestra su salida. + Run { + /// El fuente COBOL (.cob). + input: PathBuf, + }, + /// Ejecuta un programa y compara su salida con un archivo esperado. + Check { + /// El fuente COBOL (.cob). + input: PathBuf, + /// El archivo con la salida esperada. + #[arg(short, long)] + expect: PathBuf, + }, +} + +fn main() -> ExitCode { + match dispatch(Cli::parse().command) { + Ok(code) => code, + Err(err) => { + eprintln!("charka: {err:#}"); + ExitCode::FAILURE + } + } +} + +fn dispatch(command: Command) -> Result { + match command { + Command::Transpile { input, output } => transpile(&input, output.as_deref()), + Command::Scaffold { input, output } => scaffold(&input, &output), + Command::Run { input } => run(&input), + Command::Check { input, expect } => check(&input, &expect), + } +} + +// ── Comandos ────────────────────────────────────────────────────── + +fn transpile(input: &Path, output: Option<&Path>) -> Result { + let rust = charka_codegen::generate(&load_ir(input)?); + match output { + Some(path) => { + fs::write(path, rust) + .with_context(|| format!("no se pudo escribir {}", path.display()))?; + eprintln!("charka: escrito {}", path.display()); + } + None => print!("{rust}"), + } + Ok(ExitCode::SUCCESS) +} + +fn scaffold(input: &Path, output: &Path) -> Result { + let ir = load_ir(input)?; + let rust = charka_codegen::generate(&ir); + let name = crate_name(input); + + fs::create_dir_all(output.join("src")) + .with_context(|| format!("no se pudo crear {}", output.display()))?; + fs::write(output.join("src/main.rs"), rust)?; + fs::write(output.join("Cargo.toml"), cargo_toml(&name))?; + + eprintln!("charka: crate «{name}» generado en {}", output.display()); + eprintln!( + " cargo run --manifest-path {}", + output.join("Cargo.toml").display() + ); + warn_unknowns(&ir); + Ok(ExitCode::SUCCESS) +} + +fn run(input: &Path) -> Result { + let ir = load_ir(input)?; + let outcome = charka_shadow::interpret(&ir); + for line in &outcome.lines { + println!("{line}"); + } + warn_unknowns(&ir); + if outcome.halt == charka_shadow::Halt::StepLimit { + eprintln!("charka: aviso — se agotó el tope de pasos (¿un bucle sin fin?)"); + return Ok(ExitCode::FAILURE); + } + Ok(ExitCode::SUCCESS) +} + +fn check(input: &Path, expect: &Path) -> Result { + let ir = load_ir(input)?; + let outcome = charka_shadow::interpret(&ir); + let expected = fs::read_to_string(expect) + .with_context(|| format!("no se pudo leer {}", expect.display()))?; + + let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect(); + let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect(); + + if got == want { + println!("charka: OK — {} líneas coinciden", got.len()); + Ok(ExitCode::SUCCESS) + } else { + eprintln!("charka: FALLA — la salida difiere de {}", expect.display()); + report_diff(&got, &want); + Ok(ExitCode::FAILURE) + } +} + +// ── Apoyo ───────────────────────────────────────────────────────── + +/// Lee un fuente COBOL y lo lleva hasta el IR. +fn load_ir(input: &Path) -> Result { + let source = fs::read_to_string(input) + .with_context(|| format!("no se pudo leer {}", input.display()))?; + let tokens = + charka_lexer::lex(&source, charka_lexer::SourceFormat::Free).context("error de léxico")?; + let program = charka_parser::parse(&tokens).context("error de parseo")?; + Ok(charka_ir::lower(&program)) +} + +/// El `Cargo.toml` de un crate generado por `scaffold`. +fn cargo_toml(name: &str) -> String { + format!( + "[package]\n\ + name = \"{name}\"\n\ + version = \"0.1.0\"\n\ + edition = \"2021\"\n\ + \n\ + [[bin]]\n\ + name = \"{name}\"\n\ + path = \"src/main.rs\"\n\ + \n\ + [dependencies]\n\ + charka-runtime = {{ path = \"{RUNTIME_PATH}\" }}\n\ + \n\ + [workspace]\n" + ) +} + +/// Un nombre de crate válido derivado del nombre del archivo fuente. +fn crate_name(input: &Path) -> String { + let stem = input + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("programa"); + let mut name: String = stem + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c.to_ascii_lowercase() + } else { + '_' + } + }) + .collect(); + if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) { + name = format!("cobol_{name}"); + } + name +} + +/// Avisa de los verbos COBOL que el transpilador no soporta todavía. +fn warn_unknowns(ir: &Ir) { + let mut verbs = Vec::new(); + for proc in &ir.procedures { + collect_unknowns(&proc.body, &mut verbs); + } + if verbs.is_empty() { + return; + } + verbs.sort(); + verbs.dedup(); + eprintln!( + "charka: aviso — verbos no transpilados (se omitieron): {}", + verbs.join(", ") + ); +} + +/// Recoge los verbos de los `Stmt::Unknown`, incluso los anidados. +fn collect_unknowns(stmts: &[Stmt], out: &mut Vec) { + for s in stmts { + match s { + Stmt::Unknown { verb, .. } => out.push(verb.clone()), + Stmt::If { + then_branch, + else_branch, + .. + } => { + collect_unknowns(then_branch, out); + collect_unknowns(else_branch, out); + } + Stmt::Perform(p) => { + if let PerformTarget::Inline(body) = &p.target { + collect_unknowns(body, out); + } + } + _ => {} + } + } +} + +/// Imprime las líneas en que la salida obtenida difiere de la esperada. +fn report_diff(got: &[&str], want: &[&str]) { + for i in 0..got.len().max(want.len()) { + let g = got.get(i).copied().unwrap_or(""); + let w = want.get(i).copied().unwrap_or(""); + if g != w { + eprintln!(" línea {}:", i + 1); + eprintln!(" obtenido: {g}"); + eprintln!(" esperado: {w}"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ir_of(src: &str) -> Ir { + let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).unwrap(); + charka_ir::lower(&charka_parser::parse(&toks).unwrap()) + } + + #[test] + fn crate_name_is_sanitized() { + assert_eq!(crate_name(Path::new("/x/06-nomina.cob")), "cobol_06_nomina"); + assert_eq!(crate_name(Path::new("PAYROLL.CBL")), "payroll"); + } + + #[test] + fn cargo_toml_names_the_crate_and_the_runtime() { + let toml = cargo_toml("demo"); + assert!(toml.contains("name = \"demo\"")); + assert!(toml.contains("charka-runtime")); + assert!(toml.contains("[workspace]")); + } + + #[test] + fn unknown_verbs_are_collected() { + let ir = ir_of( + "PROCEDURE DIVISION.\n\ + MAIN.\n\ + INSPECT WS-X TALLYING WS-N FOR ALL ' '.\n", + ); + let mut verbs = Vec::new(); + for proc in &ir.procedures { + collect_unknowns(&proc.body, &mut verbs); + } + assert_eq!(verbs, vec!["INSPECT".to_string()]); + } + + #[test] + fn known_program_has_no_unknowns() { + let ir = ir_of("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'OK'.\n STOP RUN.\n"); + let mut verbs = Vec::new(); + for proc in &ir.procedures { + collect_unknowns(&proc.body, &mut verbs); + } + assert!(verbs.is_empty()); + } +} diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 835672d..5177f76 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -160,12 +160,23 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar. (`01-hola` … `07-clasificar`), cada uno con su `.expected`. Ejercita el pipeline completo de punta a punta. Ver su `README.md`. +## La CLI + +`crates/apps/charka/` — el binario `charka`, que envuelve el pipeline +en cuatro comandos: `transpile` (emite Rust), `scaffold` (genera un +crate compilable), `run` (ejecuta vía el intérprete sombra) y `check` +(ejecuta y diferencia contra una salida esperada). Avisa de los verbos +no transpilados. + ## Estado Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (17), `charka-parser` (15), `charka-ir` (17), `charka-runtime` (17), -`charka-codegen` (14) y `charka-shadow` (11) implementados y verdes. -COBOL → Rust corre de punta a punta, validado contra el corpus. +`charka-codegen` (14), `charka-shadow` (11) y la CLI `charka` (4) +implementados y verdes. COBOL → Rust corre de punta a punta, validado +contra el corpus. El crate que genera `scaffold` compila y su salida +coincide con la del intérprete sombra — las dos rutas de ejecución +concuerdan. Próximo hito mayor: salir del subconjunto COBOL'85 puro hacia CICS, SQL embebido y los dialectos IBM Enterprise; ampliar el codegen diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index a640410..66beb67 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,26 @@ 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): CLI del transpilador — transpile / scaffold / run / check + +App nueva `crates/apps/charka` — el binario `charka`, que vuelve usable +el pipeline desde la terminal. Cuatro comandos: + +- `transpile [-o out.rs]` — emite el código Rust (a un archivo + o a la salida estándar). +- `scaffold -o ` — genera un crate Rust completo + (`Cargo.toml` + `src/main.rs`) que depende de `charka-runtime` y + compila tal cual. +- `run ` — ejecuta el programa con el intérprete sombra y + muestra su salida, sin compilar nada. +- `check -e ` — ejecuta y diferencia la salida + contra un archivo esperado; reporta las líneas que difieren. + +Avisa de los verbos COBOL que aún no se transpilan. Verificado de +punta a punta contra el corpus: `scaffold` de `06-nomina` genera un +crate que compila y produce la misma salida que el intérprete sombra — +las dos rutas de ejecución concuerdan. 4 tests; fmt + clippy limpios. + ### feat(charka-shadow): validador en sombra + corpus COBOL Crate nuevo `crates/modules/charka/charka-shadow` y un corpus de prueba