gioser

tawasuyu/brahman

Rust 570TOML 215Markdown 68JSON 198261 Definiciones

⎇ e52b3fb572cec07d35c55d87519ecc8e0bd0505a crates/modules/charka/charka-codegen/src/lib.rs Rust · 344 líneas · 10.2 KB Historia Quién tocó qué crudo

feat(charka): charka-codegen — emisión de Rust desde el IR e52b3fb57 · sergio ·

1//! `charka-codegen` — emisión de Rust desde el IR del transpilador.
2//!
3//! Etapa final del pipeline COBOL→Rust: toma el [`Ir`] de `charka-ir`
4//! y produce un fuente Rust (un `String`) que, compilado contra
5//! `charka-runtime`, ejecuta la lógica del programa COBOL original.
6//!
7//! La forma del código emitido:
8//!
9//! - Un `struct Program` con un campo por cada dato elemental — `Num`
10//!   para los numéricos, `Text` para los alfanuméricos.
11//! - `Program::new()` inicializa los campos desde sus cláusulas `VALUE`.
12//! - Un método `p_<párrafo>(&mut self)` por cada párrafo del PROCEDURE.
13//! - `run()` los encadena en orden (el «caer» de COBOL); `main()`
14//!   construye el `Program` y lo corre.
15//!
16//! Es **tolerante**: lo que no sabe transpilar (un `Stmt::Unknown`, un
17//! dato sin resolver, `**`) se emite como un comentario `// charka:` —
18//! el código generado siempre compila.
19//!
20//! Alcance v1 — fuera: grupos como campo propio, `REDEFINES`,
21//! `OCCURS`/tablas, `PERFORM ... THRU` como rango, E/S de ficheros,
22//! `EVALUATE`, CICS y SQL embebido.
23
24#![forbid(unsafe_code)]
25
26mod emit;
27mod expr;
28mod stmt;
29mod sym;
30
31use std::collections::HashMap;
32
33use charka_ir::{Ir, Procedure};
34
35use emit::Emitter;
36use expr::rust_str;
37use stmt::emit_stmt;
38use sym::{paragraph_method, Field, FieldKind, Symbols};
39
40/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
41pub fn generate(ir: &Ir) -> String {
42    let sym = Symbols::build(&ir.data);
43    let mut em = Emitter::new();
44    emit_header(&mut em);
45    emit_struct(&mut em, &sym);
46    emit_impl(&mut em, &sym, ir);
47    emit_main(&mut em);
48    em.finish()
49}
50
51/// El preámbulo: doc, `allow`s, el `use` del runtime y el helper `dec`.
52fn emit_header(em: &mut Emitter) {
53    em.line("//! Generado por charka — transpilador COBOL → Rust.");
54    em.line("//! No editar a mano: regenerar desde el fuente COBOL.");
55    em.blank();
56    em.line(
57        "#![allow(dead_code, unused_mut, unused_variables, unused_parens, \
58unreachable_code, clippy::all)]",
59    );
60    em.blank();
61    em.line("use charka_runtime::*;");
62    em.blank();
63    em.line("/// Construye un `Decimal` desde un literal numérico COBOL.");
64    em.line("fn dec(s: &str) -> Decimal {");
65    em.line("    Decimal::parse(s).expect(\"charka: literal numérico inválido\")");
66    em.line("}");
67    em.blank();
68}
69
70/// El `struct Program` con un campo por dato elemental.
71fn emit_struct(em: &mut Emitter, sym: &Symbols) {
72    em.line("/// El estado del programa: un campo por cada dato elemental.");
73    em.line("struct Program {");
74    em.indent();
75    for f in &sym.fields {
76        let ty = match f.kind {
77            FieldKind::Num { .. } => "Num",
78            FieldKind::Text { .. } => "Text",
79        };
80        em.line(&format!("{}: {ty},", f.ident));
81    }
82    em.dedent();
83    em.line("}");
84    em.blank();
85}
86
87/// El bloque `impl Program`: `new`, los párrafos y `run`.
88fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) {
89    em.line("impl Program {");
90    em.indent();
91
92    // new()
93    em.line("fn new() -> Self {");
94    em.indent();
95    em.line("Self {");
96    em.indent();
97    for f in &sym.fields {
98        em.line(&format!("{}: {},", f.ident, field_init(f)));
99    }
100    em.dedent();
101    em.line("}");
102    em.dedent();
103    em.line("}");
104    em.blank();
105
106    // Un método por párrafo.
107    let methods = paragraph_methods(ir);
108    for (name, proc) in &methods {
109        em.line(&format!("fn {name}(&mut self) {{"));
110        em.indent();
111        for s in &proc.body {
112            emit_stmt(em, sym, s);
113        }
114        em.dedent();
115        em.line("}");
116        em.blank();
117    }
118
119    // run() — encadena los párrafos en orden.
120    em.line("fn run(&mut self) {");
121    em.indent();
122    if methods.is_empty() {
123        em.line("// programa sin PROCEDURE division");
124    }
125    for (name, _) in &methods {
126        em.line(&format!("self.{name}();"));
127    }
128    em.dedent();
129    em.line("}");
130
131    em.dedent();
132    em.line("}");
133    em.blank();
134}
135
136/// El `fn main`.
137fn emit_main(em: &mut Emitter) {
138    em.line("fn main() {");
139    em.indent();
140    em.line("Program::new().run();");
141    em.dedent();
142    em.line("}");
143}
144
145/// El inicializador de un campo, a partir de su cláusula `VALUE`.
146fn field_init(f: &Field) -> String {
147    match &f.kind {
148        FieldKind::Num { int, frac, signed } => format!(
149            "Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
150            rust_str(&numeric_value(f.value.as_deref()))
151        ),
152        FieldKind::Text { len } => format!(
153            "Text::with_value({len}, {})",
154            rust_str(&text_value(f.value.as_deref()))
155        ),
156    }
157}
158
159/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
160fn numeric_value(v: Option<&str>) -> String {
161    let Some(raw) = v else {
162        return "0".to_string();
163    };
164    let up = raw.to_uppercase();
165    if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
166        return "0".to_string();
167    }
168    if charka_bcd::Decimal::parse(raw).is_ok() {
169        raw.to_string()
170    } else {
171        "0".to_string()
172    }
173}
174
175/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
176/// literales de texto en comillas simples; aquí se desenvuelven.
177fn text_value(v: Option<&str>) -> String {
178    let Some(raw) = v else {
179        return String::new();
180    };
181    let up = raw.to_uppercase();
182    if matches!(up.as_str(), "SPACE" | "SPACES") {
183        return String::new();
184    }
185    if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
186        return "0".to_string();
187    }
188    if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
189        raw[1..raw.len() - 1].to_string()
190    } else {
191        raw.to_string()
192    }
193}
194
195/// Asigna a cada párrafo un nombre de método único.
196fn paragraph_methods(ir: &Ir) -> Vec<(String, &Procedure)> {
197    let mut seen: HashMap<String, u32> = HashMap::new();
198    let mut out = Vec::new();
199    for proc in &ir.procedures {
200        let base = paragraph_method(&proc.name);
201        let n = seen.entry(base.clone()).or_insert(0);
202        let name = if *n > 0 { format!("{base}_{n}") } else { base };
203        *n += 1;
204        out.push((name, proc));
205    }
206    out
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    /// Helper: lexa, parsea, baja a IR y transpila un fuente COBOL.
214    fn gen(src: &str) -> String {
215        let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).expect("lex");
216        let prog = charka_parser::parse(&toks).expect("parse");
217        let ir = charka_ir::lower(&prog);
218        generate(&ir)
219    }
220
221    /// Un programa COBOL de demostración, razonablemente completo.
222    const DEMO: &str = "IDENTIFICATION DIVISION.\n\
223         PROGRAM-ID. DEMO.\n\
224         DATA DIVISION.\n\
225         WORKING-STORAGE SECTION.\n\
226         01 WS-A    PIC 9(3) VALUE 10.\n\
227         01 WS-B    PIC 9(3).\n\
228         01 WS-NAME PIC X(8) VALUE 'BOB'.\n\
229         PROCEDURE DIVISION.\n\
230         MAIN-PARA.\n\
231             MOVE 5 TO WS-B.\n\
232             COMPUTE WS-B = WS-A + WS-B.\n\
233             DISPLAY 'B=' WS-B.\n\
234             IF WS-B > 0 DISPLAY 'POS' END-IF.\n\
235             PERFORM SUB-PARA.\n\
236             STOP RUN.\n\
237         SUB-PARA.\n\
238             DISPLAY WS-NAME.\n";
239
240    #[test]
241    fn header_and_main_are_emitted() {
242        let out = gen(DEMO);
243        assert!(out.contains("use charka_runtime::*;"));
244        assert!(out.contains("fn dec(s: &str) -> Decimal {"));
245        assert!(out.contains("fn main() {"));
246        assert!(out.contains("Program::new().run();"));
247    }
248
249    #[test]
250    fn numeric_field_becomes_num() {
251        let out = gen(DEMO);
252        assert!(out.contains("ws_a: Num,"));
253        assert!(out.contains("Num::with_value(Picture::new(3, 0, false), \"10\")"));
254    }
255
256    #[test]
257    fn alphanumeric_field_becomes_text() {
258        let out = gen(DEMO);
259        assert!(out.contains("ws_name: Text,"));
260        assert!(out.contains("Text::with_value(8, \"BOB\")"));
261    }
262
263    #[test]
264    fn move_emits_a_store() {
265        assert!(gen(DEMO).contains("self.ws_b.store(dec(\"5\"));"));
266    }
267
268    #[test]
269    fn compute_emits_the_expression() {
270        let out = gen(DEMO);
271        assert!(out.contains("self.ws_b.store((self.ws_a.value()).add(&(self.ws_b.value())));"));
272    }
273
274    #[test]
275    fn display_emits_a_println() {
276        let out = gen(DEMO);
277        assert!(out.contains("println!(\"{}{}\", \"B=\", self.ws_b.display());"));
278    }
279
280    #[test]
281    fn if_emits_a_rust_if() {
282        let out = gen(DEMO);
283        assert!(out.contains("if (self.ws_b.value()) > (dec(\"0\")) {"));
284    }
285
286    #[test]
287    fn paragraphs_become_methods_and_run_chains_them() {
288        let out = gen(DEMO);
289        assert!(out.contains("fn p_main_para(&mut self) {"));
290        assert!(out.contains("fn p_sub_para(&mut self) {"));
291        assert!(out.contains("fn run(&mut self) {"));
292        assert!(out.contains("self.p_main_para();"));
293        assert!(out.contains("self.p_sub_para();"));
294    }
295
296    #[test]
297    fn perform_calls_the_paragraph_method() {
298        assert!(gen(DEMO).contains("self.p_sub_para();"));
299    }
300
301    #[test]
302    fn stop_run_exits() {
303        assert!(gen(DEMO).contains("std::process::exit(0);"));
304    }
305
306    #[test]
307    fn unknown_verb_becomes_a_comment() {
308        let out = gen("PROCEDURE DIVISION.\n\
309             MAIN.\n\
310                 INSPECT WS-X TALLYING WS-N FOR ALL ' '.\n");
311        assert!(out.contains("// charka: verbo no transpilado — INSPECT"));
312    }
313
314    #[test]
315    fn add_giving_emits_a_sum() {
316        let out = gen("DATA DIVISION.\n\
317             WORKING-STORAGE SECTION.\n\
318             01 A PIC 9(3).\n\
319             01 B PIC 9(3).\n\
320             01 C PIC 9(3).\n\
321             PROCEDURE DIVISION.\n\
322             MAIN.\n\
323                 ADD A B GIVING C.\n");
324        assert!(out.contains("self.c.store((self.a.value()).add(&(self.b.value())));"));
325    }
326
327    #[test]
328    fn perform_times_emits_a_loop() {
329        let out = gen("PROCEDURE DIVISION.\n\
330             MAIN.\n\
331                 PERFORM SUB 3 TIMES.\n\
332             SUB.\n\
333                 CONTINUE.\n");
334        assert!(out.contains("for _ in 0..3usize {"));
335    }
336
337    #[test]
338    fn empty_program_still_compiles_shape() {
339        let out = gen("");
340        assert!(out.contains("struct Program {"));
341        assert!(out.contains("fn main() {"));
342        assert!(out.contains("fn run(&mut self) {"));
343    }
344}