⎇ 634a43006ac4257288af40320b5360e9f157ab09
crates/apps/charka/src/main.rs
Historia
Quién tocó qué
crudo
feat(charka): PICTURE de edición — Z, coma de millares y punto decimal
1//! `charka` — la CLI del transpilador COBOL → Rust.
2//!
3//! Envuelve el pipeline (lexer → parser → IR → codegen) y el validador
4//! en sombra en cuatro comandos:
5//!
6//! - `transpile` — emite el código Rust de un fuente COBOL.
7//! - `scaffold` — genera un crate Rust completo y compilable.
8//! - `run` — ejecuta el programa (intérprete sombra) y lo imprime.
9//! - `check` — ejecuta y compara la salida contra un archivo dado.
10
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::process::ExitCode;
14
15use anyhow::{Context, Result};
16use charka_ir::{Ir, PerformTarget, Stmt};
17use clap::{Parser, Subcommand};
18
19/// Ruta a `charka-runtime`, fijada al compilar — el crate generado por
20/// `scaffold` la usa como dependencia.
21const RUNTIME_PATH: &str = concat!(
22 env!("CARGO_MANIFEST_DIR"),
23 "/../../modules/charka/charka-runtime"
24);
25
26/// El transpilador de COBOL a Rust.
27#[derive(Parser)]
28#[command(name = "charka", version, about = "Transpilador COBOL → Rust")]
29struct Cli {
30 #[command(subcommand)]
31 command: Command,
32}
33
34#[derive(Subcommand)]
35enum Command {
36 /// Transpila un fuente COBOL a código Rust.
37 Transpile {
38 /// El fuente COBOL (.cob), en formato libre.
39 input: PathBuf,
40 /// Archivo de salida; si se omite, va a la salida estándar.
41 #[arg(short, long)]
42 output: Option<PathBuf>,
43 },
44 /// Genera un crate Rust completo y compilable.
45 Scaffold {
46 /// El fuente COBOL (.cob).
47 input: PathBuf,
48 /// El directorio del crate a crear.
49 #[arg(short, long)]
50 output: PathBuf,
51 },
52 /// Ejecuta un programa COBOL (intérprete sombra) y muestra su salida.
53 Run {
54 /// El fuente COBOL (.cob).
55 input: PathBuf,
56 },
57 /// Ejecuta un programa y compara su salida con un archivo esperado.
58 Check {
59 /// El fuente COBOL (.cob).
60 input: PathBuf,
61 /// El archivo con la salida esperada.
62 #[arg(short, long)]
63 expect: PathBuf,
64 },
65}
66
67fn main() -> ExitCode {
68 match dispatch(Cli::parse().command) {
69 Ok(code) => code,
70 Err(err) => {
71 eprintln!("charka: {err:#}");
72 ExitCode::FAILURE
73 }
74 }
75}
76
77fn dispatch(command: Command) -> Result<ExitCode> {
78 match command {
79 Command::Transpile { input, output } => transpile(&input, output.as_deref()),
80 Command::Scaffold { input, output } => scaffold(&input, &output),
81 Command::Run { input } => run(&input),
82 Command::Check { input, expect } => check(&input, &expect),
83 }
84}
85
86// ── Comandos ──────────────────────────────────────────────────────
87
88fn transpile(input: &Path, output: Option<&Path>) -> Result<ExitCode> {
89 let rust = charka_codegen::generate(&load_ir(input)?);
90 match output {
91 Some(path) => {
92 fs::write(path, rust)
93 .with_context(|| format!("no se pudo escribir {}", path.display()))?;
94 eprintln!("charka: escrito {}", path.display());
95 }
96 None => print!("{rust}"),
97 }
98 Ok(ExitCode::SUCCESS)
99}
100
101fn scaffold(input: &Path, output: &Path) -> Result<ExitCode> {
102 let ir = load_ir(input)?;
103 let rust = charka_codegen::generate(&ir);
104 let name = crate_name(input);
105
106 fs::create_dir_all(output.join("src"))
107 .with_context(|| format!("no se pudo crear {}", output.display()))?;
108 fs::write(output.join("src/main.rs"), rust)?;
109 fs::write(output.join("Cargo.toml"), cargo_toml(&name))?;
110
111 eprintln!("charka: crate «{name}» generado en {}", output.display());
112 eprintln!(
113 " cargo run --manifest-path {}",
114 output.join("Cargo.toml").display()
115 );
116 warn_unknowns(&ir);
117 Ok(ExitCode::SUCCESS)
118}
119
120fn run(input: &Path) -> Result<ExitCode> {
121 let ir = load_ir(input)?;
122 let outcome = charka_shadow::interpret(&ir);
123 for line in &outcome.lines {
124 println!("{line}");
125 }
126 warn_unknowns(&ir);
127 if outcome.halt == charka_shadow::Halt::StepLimit {
128 eprintln!("charka: aviso — se agotó el tope de pasos (¿un bucle sin fin?)");
129 return Ok(ExitCode::FAILURE);
130 }
131 Ok(ExitCode::SUCCESS)
132}
133
134fn check(input: &Path, expect: &Path) -> Result<ExitCode> {
135 let ir = load_ir(input)?;
136 let outcome = charka_shadow::interpret(&ir);
137 let expected = fs::read_to_string(expect)
138 .with_context(|| format!("no se pudo leer {}", expect.display()))?;
139
140 let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect();
141 let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect();
142
143 if got == want {
144 println!("charka: OK — {} líneas coinciden", got.len());
145 Ok(ExitCode::SUCCESS)
146 } else {
147 eprintln!("charka: FALLA — la salida difiere de {}", expect.display());
148 report_diff(&got, &want);
149 Ok(ExitCode::FAILURE)
150 }
151}
152
153// ── Apoyo ─────────────────────────────────────────────────────────
154
155/// Lee un fuente COBOL y lo lleva hasta el IR.
156fn load_ir(input: &Path) -> Result<Ir> {
157 let source = fs::read_to_string(input)
158 .with_context(|| format!("no se pudo leer {}", input.display()))?;
159 let tokens =
160 charka_lexer::lex(&source, charka_lexer::SourceFormat::Free).context("error de léxico")?;
161 let program = charka_parser::parse(&tokens).context("error de parseo")?;
162 Ok(charka_ir::lower(&program))
163}
164
165/// El `Cargo.toml` de un crate generado por `scaffold`.
166fn cargo_toml(name: &str) -> String {
167 format!(
168 "[package]\n\
169 name = \"{name}\"\n\
170 version = \"0.1.0\"\n\
171 edition = \"2021\"\n\
172 \n\
173 [[bin]]\n\
174 name = \"{name}\"\n\
175 path = \"src/main.rs\"\n\
176 \n\
177 [dependencies]\n\
178 charka-runtime = {{ path = \"{RUNTIME_PATH}\" }}\n\
179 \n\
180 [workspace]\n"
181 )
182}
183
184/// Un nombre de crate válido derivado del nombre del archivo fuente.
185fn crate_name(input: &Path) -> String {
186 let stem = input
187 .file_stem()
188 .and_then(|s| s.to_str())
189 .unwrap_or("programa");
190 let mut name: String = stem
191 .chars()
192 .map(|c| {
193 if c.is_ascii_alphanumeric() {
194 c.to_ascii_lowercase()
195 } else {
196 '_'
197 }
198 })
199 .collect();
200 if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) {
201 name = format!("cobol_{name}");
202 }
203 name
204}
205
206/// Avisa de los verbos COBOL que el transpilador no soporta todavía.
207fn warn_unknowns(ir: &Ir) {
208 let mut verbs = Vec::new();
209 for proc in &ir.procedures {
210 collect_unknowns(&proc.body, &mut verbs);
211 }
212 if verbs.is_empty() {
213 return;
214 }
215 verbs.sort();
216 verbs.dedup();
217 eprintln!(
218 "charka: aviso — verbos no transpilados (se omitieron): {}",
219 verbs.join(", ")
220 );
221}
222
223/// Recoge los verbos de los `Stmt::Unknown`, incluso los anidados.
224fn collect_unknowns(stmts: &[Stmt], out: &mut Vec<String>) {
225 for s in stmts {
226 match s {
227 Stmt::Unknown { verb, .. } => out.push(verb.clone()),
228 Stmt::If {
229 then_branch,
230 else_branch,
231 ..
232 } => {
233 collect_unknowns(then_branch, out);
234 collect_unknowns(else_branch, out);
235 }
236 Stmt::Evaluate { whens, other, .. } => {
237 for w in whens {
238 collect_unknowns(&w.body, out);
239 }
240 collect_unknowns(other, out);
241 }
242 Stmt::Read {
243 at_end, not_at_end, ..
244 } => {
245 collect_unknowns(at_end, out);
246 collect_unknowns(not_at_end, out);
247 }
248 Stmt::Perform(p) => {
249 if let PerformTarget::Inline(body) = &p.target {
250 collect_unknowns(body, out);
251 }
252 }
253 _ => {}
254 }
255 }
256}
257
258/// Imprime las líneas en que la salida obtenida difiere de la esperada.
259fn report_diff(got: &[&str], want: &[&str]) {
260 for i in 0..got.len().max(want.len()) {
261 let g = got.get(i).copied().unwrap_or("<falta>");
262 let w = want.get(i).copied().unwrap_or("<falta>");
263 if g != w {
264 eprintln!(" línea {}:", i + 1);
265 eprintln!(" obtenido: {g}");
266 eprintln!(" esperado: {w}");
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn ir_of(src: &str) -> Ir {
276 let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).unwrap();
277 charka_ir::lower(&charka_parser::parse(&toks).unwrap())
278 }
279
280 #[test]
281 fn crate_name_is_sanitized() {
282 assert_eq!(crate_name(Path::new("/x/06-nomina.cob")), "cobol_06_nomina");
283 assert_eq!(crate_name(Path::new("PAYROLL.CBL")), "payroll");
284 }
285
286 #[test]
287 fn cargo_toml_names_the_crate_and_the_runtime() {
288 let toml = cargo_toml("demo");
289 assert!(toml.contains("name = \"demo\""));
290 assert!(toml.contains("charka-runtime"));
291 assert!(toml.contains("[workspace]"));
292 }
293
294 #[test]
295 fn unknown_verbs_are_collected() {
296 let ir = ir_of(
297 "PROCEDURE DIVISION.\n\
298 MAIN.\n\
299 CALL 'SUBPROG'.\n",
300 );
301 let mut verbs = Vec::new();
302 for proc in &ir.procedures {
303 collect_unknowns(&proc.body, &mut verbs);
304 }
305 assert_eq!(verbs, vec!["CALL".to_string()]);
306 }
307
308 #[test]
309 fn known_program_has_no_unknowns() {
310 let ir = ir_of("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'OK'.\n STOP RUN.\n");
311 let mut verbs = Vec::new();
312 for proc in &ir.procedures {
313 collect_unknowns(&proc.body, &mut verbs);
314 }
315 assert!(verbs.is_empty());
316 }
317}