tawasuyu

tawasuyu/brahman

Rust 506TOML 198Markdown 56JSON 197212 Definiciones

⎇ fa65f202065abca26e0fb28581427117b84af85c crates/apps/charka/src/main.rs Rust · 311 líneas · 9.6 KB Historia Quién tocó qué crudo

feat(charka): INITIALIZE — resetear datos y grupos fa65f2020 · sergio ·

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::Perform(p) => {
243                if let PerformTarget::Inline(body) = &p.target {
244                    collect_unknowns(body, out);
245                }
246            }
247            _ => {}
248        }
249    }
250}
251
252/// Imprime las líneas en que la salida obtenida difiere de la esperada.
253fn report_diff(got: &[&str], want: &[&str]) {
254    for i in 0..got.len().max(want.len()) {
255        let g = got.get(i).copied().unwrap_or("<falta>");
256        let w = want.get(i).copied().unwrap_or("<falta>");
257        if g != w {
258            eprintln!("  línea {}:", i + 1);
259            eprintln!("    obtenido: {g}");
260            eprintln!("    esperado: {w}");
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    fn ir_of(src: &str) -> Ir {
270        let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).unwrap();
271        charka_ir::lower(&charka_parser::parse(&toks).unwrap())
272    }
273
274    #[test]
275    fn crate_name_is_sanitized() {
276        assert_eq!(crate_name(Path::new("/x/06-nomina.cob")), "cobol_06_nomina");
277        assert_eq!(crate_name(Path::new("PAYROLL.CBL")), "payroll");
278    }
279
280    #[test]
281    fn cargo_toml_names_the_crate_and_the_runtime() {
282        let toml = cargo_toml("demo");
283        assert!(toml.contains("name = \"demo\""));
284        assert!(toml.contains("charka-runtime"));
285        assert!(toml.contains("[workspace]"));
286    }
287
288    #[test]
289    fn unknown_verbs_are_collected() {
290        let ir = ir_of(
291            "PROCEDURE DIVISION.\n\
292             MAIN.\n\
293                 CALL 'SUBPROG'.\n",
294        );
295        let mut verbs = Vec::new();
296        for proc in &ir.procedures {
297            collect_unknowns(&proc.body, &mut verbs);
298        }
299        assert_eq!(verbs, vec!["CALL".to_string()]);
300    }
301
302    #[test]
303    fn known_program_has_no_unknowns() {
304        let ir = ir_of("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'OK'.\n STOP RUN.\n");
305        let mut verbs = Vec::new();
306        for proc in &ir.procedures {
307            collect_unknowns(&proc.body, &mut verbs);
308        }
309        assert!(verbs.is_empty());
310    }
311}