feat(charka): CLI del transpilador — transpile / scaffold / run / check
App nueva crates/apps/charka — el binario `charka`, que vuelve usable el pipeline COBOL->Rust desde la terminal. - transpile <in.cob> [-o out.rs] — emite el código Rust. - scaffold <in.cob> -o <dir> — genera un crate Rust completo (Cargo.toml + src/main.rs) que depende de charka-runtime y compila. - run <in.cob> — ejecuta el programa con el intérprete sombra, sin compilar nada, y muestra su salida. - check <in.cob> -e <esperado> — ejecuta y diferencia contra una salida esperada; 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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+13
@@ -2307,6 +2307,19 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charka"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"charka-codegen",
|
||||||
|
"charka-ir",
|
||||||
|
"charka-lexer",
|
||||||
|
"charka-parser",
|
||||||
|
"charka-shadow",
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charka-bcd"
|
name = "charka-bcd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ members = [
|
|||||||
"crates/apps/mirada-launcher",
|
"crates/apps/mirada-launcher",
|
||||||
"crates/apps/mirada-portal",
|
"crates/apps/mirada-portal",
|
||||||
"crates/apps/mirada-greeter",
|
"crates/apps/mirada-greeter",
|
||||||
|
"crates/apps/charka",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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<PathBuf>,
|
||||||
|
},
|
||||||
|
/// 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<ExitCode> {
|
||||||
|
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<ExitCode> {
|
||||||
|
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<ExitCode> {
|
||||||
|
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<ExitCode> {
|
||||||
|
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<ExitCode> {
|
||||||
|
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<Ir> {
|
||||||
|
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<String>) {
|
||||||
|
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("<falta>");
|
||||||
|
let w = want.get(i).copied().unwrap_or("<falta>");
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,12 +160,23 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
|
|||||||
(`01-hola` … `07-clasificar`), cada uno con su `.expected`. Ejercita
|
(`01-hola` … `07-clasificar`), cada uno con su `.expected`. Ejercita
|
||||||
el pipeline completo de punta a punta. Ver su `README.md`.
|
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
|
## Estado
|
||||||
|
|
||||||
Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (17),
|
Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (17),
|
||||||
`charka-parser` (15), `charka-ir` (17), `charka-runtime` (17),
|
`charka-parser` (15), `charka-ir` (17), `charka-runtime` (17),
|
||||||
`charka-codegen` (14) y `charka-shadow` (11) implementados y verdes.
|
`charka-codegen` (14), `charka-shadow` (11) y la CLI `charka` (4)
|
||||||
COBOL → Rust corre de punta a punta, validado contra el corpus.
|
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,
|
Próximo hito mayor: salir del subconjunto COBOL'85 puro hacia CICS,
|
||||||
SQL embebido y los dialectos IBM Enterprise; ampliar el codegen
|
SQL embebido y los dialectos IBM Enterprise; ampliar el codegen
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
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.
|
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 <in.cob> [-o out.rs]` — emite el código Rust (a un archivo
|
||||||
|
o a la salida estándar).
|
||||||
|
- `scaffold <in.cob> -o <dir>` — genera un crate Rust completo
|
||||||
|
(`Cargo.toml` + `src/main.rs`) que depende de `charka-runtime` y
|
||||||
|
compila tal cual.
|
||||||
|
- `run <in.cob>` — ejecuta el programa con el intérprete sombra y
|
||||||
|
muestra su salida, sin compilar nada.
|
||||||
|
- `check <in.cob> -e <esperado>` — 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
|
### feat(charka-shadow): validador en sombra + corpus COBOL
|
||||||
|
|
||||||
Crate nuevo `crates/modules/charka/charka-shadow` y un corpus de prueba
|
Crate nuevo `crates/modules/charka/charka-shadow` y un corpus de prueba
|
||||||
|
|||||||
Reference in New Issue
Block a user