gioser

tawasuyu/brahman

Rust 534TOML 203Markdown 58JSON 197690 Definiciones

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

feat(charka): PERFORM ... THRU como rango real de párrafos f250fd076 · 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 charka_ir::Ir;
32
33use emit::Emitter;
34use expr::rust_str;
35use stmt::emit_stmt;
36use sym::{Field, FieldKind, Symbols};
37
38/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
39pub fn generate(ir: &Ir) -> String {
40    let sym = Symbols::build(ir);
41    let mut em = Emitter::new();
42    emit_header(&mut em);
43    emit_struct(&mut em, &sym);
44    emit_impl(&mut em, &sym, ir);
45    emit_main(&mut em);
46    em.finish()
47}
48
49/// El preámbulo: doc, `allow`s, el `use` del runtime y el helper `dec`.
50fn emit_header(em: &mut Emitter) {
51    em.line("//! Generado por charka — transpilador COBOL → Rust.");
52    em.line("//! No editar a mano: regenerar desde el fuente COBOL.");
53    em.blank();
54    em.line(
55        "#![allow(dead_code, unused_mut, unused_variables, unused_parens, \
56unreachable_code, clippy::all)]",
57    );
58    em.blank();
59    em.line("use charka_runtime::*;");
60    em.blank();
61    em.line("/// Construye un `Decimal` desde un literal numérico COBOL.");
62    em.line("fn dec(s: &str) -> Decimal {");
63    em.line("    Decimal::parse(s).expect(\"charka: literal numérico inválido\")");
64    em.line("}");
65    em.blank();
66}
67
68/// El `struct Program` con un campo por dato elemental.
69fn emit_struct(em: &mut Emitter, sym: &Symbols) {
70    em.line("/// El estado del programa: un campo por cada dato elemental.");
71    em.line("struct Program {");
72    em.indent();
73    for f in &sym.fields {
74        let elem = match f.kind {
75            FieldKind::Num { .. } => "Num",
76            FieldKind::Text { .. } => "Text",
77        };
78        let ty = match f.occurs {
79            None => elem.to_string(),
80            Some(_) => format!("Vec<{elem}>"),
81        };
82        em.line(&format!("{}: {ty},", f.ident));
83    }
84    em.dedent();
85    em.line("}");
86    em.blank();
87}
88
89/// El bloque `impl Program`: `new`, los párrafos y `run`.
90fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) {
91    em.line("impl Program {");
92    em.indent();
93
94    // new()
95    em.line("fn new() -> Self {");
96    em.indent();
97    em.line("Self {");
98    em.indent();
99    for f in &sym.fields {
100        em.line(&format!("{}: {},", f.ident, field_init(f)));
101    }
102    em.dedent();
103    em.line("}");
104    em.dedent();
105    em.line("}");
106    em.blank();
107
108    // Un método por párrafo (en paralelo con `sym.paragraphs`).
109    for (i, proc) in ir.procedures.iter().enumerate() {
110        em.line(&format!("fn {}(&mut self) {{", sym.paragraphs[i].1));
111        em.indent();
112        for s in &proc.body {
113            emit_stmt(em, sym, s);
114        }
115        em.dedent();
116        em.line("}");
117        em.blank();
118    }
119
120    // run() — encadena los párrafos en orden.
121    em.line("fn run(&mut self) {");
122    em.indent();
123    if sym.paragraphs.is_empty() {
124        em.line("// programa sin PROCEDURE division");
125    }
126    for (_, method) in &sym.paragraphs {
127        em.line(&format!("self.{method}();"));
128    }
129    em.dedent();
130    em.line("}");
131
132    em.dedent();
133    em.line("}");
134    em.blank();
135}
136
137/// El `fn main`.
138fn emit_main(em: &mut Emitter) {
139    em.line("fn main() {");
140    em.indent();
141    em.line("Program::new().run();");
142    em.dedent();
143    em.line("}");
144}
145
146/// El inicializador de un campo, a partir de su `VALUE` ya
147/// normalizado por `charka-ir`. Una tabla (`OCCURS n`) se inicializa
148/// como un `Vec` de `n` copias del valor inicial.
149fn field_init(f: &Field) -> String {
150    let scalar = match &f.kind {
151        FieldKind::Num { int, frac, signed } => format!(
152            "Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
153            rust_str(&f.init)
154        ),
155        FieldKind::Text { len } => {
156            format!("Text::with_value({len}, {})", rust_str(&f.init))
157        }
158    };
159    match f.occurs {
160        None => scalar,
161        Some(n) => format!("vec![{scalar}; {n}]"),
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    /// Helper: lexa, parsea, baja a IR y transpila un fuente COBOL.
170    fn gen(src: &str) -> String {
171        let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).expect("lex");
172        let prog = charka_parser::parse(&toks).expect("parse");
173        let ir = charka_ir::lower(&prog);
174        generate(&ir)
175    }
176
177    /// Un programa COBOL de demostración, razonablemente completo.
178    const DEMO: &str = "IDENTIFICATION DIVISION.\n\
179         PROGRAM-ID. DEMO.\n\
180         DATA DIVISION.\n\
181         WORKING-STORAGE SECTION.\n\
182         01 WS-A    PIC 9(3) VALUE 10.\n\
183         01 WS-B    PIC 9(3).\n\
184         01 WS-NAME PIC X(8) VALUE 'BOB'.\n\
185         PROCEDURE DIVISION.\n\
186         MAIN-PARA.\n\
187             MOVE 5 TO WS-B.\n\
188             COMPUTE WS-B = WS-A + WS-B.\n\
189             DISPLAY 'B=' WS-B.\n\
190             IF WS-B > 0 DISPLAY 'POS' END-IF.\n\
191             PERFORM SUB-PARA.\n\
192             STOP RUN.\n\
193         SUB-PARA.\n\
194             DISPLAY WS-NAME.\n";
195
196    #[test]
197    fn header_and_main_are_emitted() {
198        let out = gen(DEMO);
199        assert!(out.contains("use charka_runtime::*;"));
200        assert!(out.contains("fn dec(s: &str) -> Decimal {"));
201        assert!(out.contains("fn main() {"));
202        assert!(out.contains("Program::new().run();"));
203    }
204
205    #[test]
206    fn numeric_field_becomes_num() {
207        let out = gen(DEMO);
208        assert!(out.contains("ws_a: Num,"));
209        assert!(out.contains("Num::with_value(Picture::new(3, 0, false), \"10\")"));
210    }
211
212    #[test]
213    fn alphanumeric_field_becomes_text() {
214        let out = gen(DEMO);
215        assert!(out.contains("ws_name: Text,"));
216        assert!(out.contains("Text::with_value(8, \"BOB\")"));
217    }
218
219    #[test]
220    fn move_emits_a_store() {
221        assert!(gen(DEMO).contains("self.ws_b.store(dec(\"5\"));"));
222    }
223
224    #[test]
225    fn compute_emits_the_expression() {
226        let out = gen(DEMO);
227        assert!(out.contains("self.ws_b.store((self.ws_a.value()).add(&(self.ws_b.value())));"));
228    }
229
230    #[test]
231    fn display_emits_a_println() {
232        let out = gen(DEMO);
233        assert!(out.contains("println!(\"{}{}\", \"B=\", self.ws_b.display());"));
234    }
235
236    #[test]
237    fn if_emits_a_rust_if() {
238        let out = gen(DEMO);
239        assert!(out.contains("if (self.ws_b.value()) > (dec(\"0\")) {"));
240    }
241
242    #[test]
243    fn paragraphs_become_methods_and_run_chains_them() {
244        let out = gen(DEMO);
245        assert!(out.contains("fn p_main_para(&mut self) {"));
246        assert!(out.contains("fn p_sub_para(&mut self) {"));
247        assert!(out.contains("fn run(&mut self) {"));
248        assert!(out.contains("self.p_main_para();"));
249        assert!(out.contains("self.p_sub_para();"));
250    }
251
252    #[test]
253    fn perform_calls_the_paragraph_method() {
254        assert!(gen(DEMO).contains("self.p_sub_para();"));
255    }
256
257    #[test]
258    fn stop_run_exits() {
259        assert!(gen(DEMO).contains("std::process::exit(0);"));
260    }
261
262    #[test]
263    fn unknown_verb_becomes_a_comment() {
264        let out = gen("PROCEDURE DIVISION.\n\
265             MAIN.\n\
266                 CALL 'SUBPROG'.\n");
267        assert!(out.contains("// charka: verbo no transpilado — CALL"));
268    }
269
270    #[test]
271    fn add_giving_emits_a_sum() {
272        let out = gen("DATA DIVISION.\n\
273             WORKING-STORAGE SECTION.\n\
274             01 A PIC 9(3).\n\
275             01 B PIC 9(3).\n\
276             01 C PIC 9(3).\n\
277             PROCEDURE DIVISION.\n\
278             MAIN.\n\
279                 ADD A B GIVING C.\n");
280        assert!(out.contains("self.c.store((self.a.value()).add(&(self.b.value())));"));
281    }
282
283    #[test]
284    fn perform_times_emits_a_loop() {
285        let out = gen("PROCEDURE DIVISION.\n\
286             MAIN.\n\
287                 PERFORM SUB 3 TIMES.\n\
288             SUB.\n\
289                 CONTINUE.\n");
290        assert!(out.contains("for _ in 0..3usize {"));
291    }
292
293    #[test]
294    fn perform_varying_emits_init_loop_and_increment() {
295        let out = gen("DATA DIVISION.\n\
296             WORKING-STORAGE SECTION.\n\
297             01 WS-I PIC 9(2).\n\
298             01 WS-N PIC 9(3).\n\
299             PROCEDURE DIVISION.\n\
300             MAIN.\n\
301                 PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5\n\
302                     ADD 1 TO WS-N\n\
303                 END-PERFORM.\n");
304        assert!(out.contains("self.ws_i.store(dec(\"1\"));"));
305        assert!(out.contains("while !((self.ws_i.value()) > (dec(\"5\"))) {"));
306        assert!(out.contains("self.ws_i.store(self.ws_i.value().add(&(dec(\"1\"))));"));
307    }
308
309    #[test]
310    fn evaluate_emits_an_if_else_chain() {
311        let out = gen("DATA DIVISION.\n\
312             WORKING-STORAGE SECTION.\n\
313             01 WS-X PIC 9(1).\n\
314             PROCEDURE DIVISION.\n\
315             MAIN.\n\
316                 EVALUATE WS-X\n\
317                     WHEN 1 DISPLAY 'UNO'\n\
318                     WHEN OTHER DISPLAY 'OTRO'\n\
319                 END-EVALUATE.\n");
320        assert!(out.contains("if ((self.ws_x.value()) == (dec(\"1\"))) {"));
321        assert!(out.contains("} else {"));
322    }
323
324    #[test]
325    fn level_88_condition_resolves_to_a_comparison() {
326        let out = gen("DATA DIVISION.\n\
327             WORKING-STORAGE SECTION.\n\
328             01 WS-FLAG PIC X VALUE 'N'.\n\
329                88 ES-SI VALUE 'Y'.\n\
330             PROCEDURE DIVISION.\n\
331             MAIN.\n\
332                 IF ES-SI DISPLAY 'SI' END-IF.\n");
333        // ES-SI equivale a `WS-FLAG = 'Y'` (comparación de texto).
334        assert!(out.contains("cobol_text_cmp(self.ws_flag.display().as_str(), \"Y\").is_eq()"));
335    }
336
337    #[test]
338    fn occurs_emits_a_vec_field_and_indexed_access() {
339        let out = gen("DATA DIVISION.\n\
340             WORKING-STORAGE SECTION.\n\
341             01 WS-T.\n\
342                05 WS-E PIC 9(3) OCCURS 4 TIMES.\n\
343             01 WS-I PIC 9(1).\n\
344             PROCEDURE DIVISION.\n\
345             MAIN.\n\
346                 MOVE 7 TO WS-E(WS-I).\n");
347        assert!(out.contains("ws_e: Vec<Num>,"));
348        assert!(out.contains("; 4]"));
349        assert!(out.contains("self.ws_e["));
350        assert!(out.contains(".saturating_sub(1)]"));
351    }
352
353    #[test]
354    fn string_concatenates_and_unstring_splits() {
355        let out = gen("DATA DIVISION.\n\
356             WORKING-STORAGE SECTION.\n\
357             01 WS-A PIC X(4).\n\
358             01 WS-B PIC X(4).\n\
359             01 WS-OUT PIC X(10).\n\
360             01 WS-SRC PIC X(10).\n\
361             PROCEDURE DIVISION.\n\
362             MAIN.\n\
363                 STRING WS-A WS-B DELIMITED BY SIZE INTO WS-OUT END-STRING.\n\
364                 UNSTRING WS-SRC DELIMITED BY ',' INTO WS-A WS-B END-UNSTRING.\n");
365        assert!(out.contains("self.ws_out.store(&format!("));
366        assert!(out.contains("__src.split(__delim.as_str())"));
367        assert!(out.contains("__it.next().unwrap_or(\"\")"));
368    }
369
370    #[test]
371    fn inspect_emits_tally_and_replace() {
372        let out = gen("DATA DIVISION.\n\
373             WORKING-STORAGE SECTION.\n\
374             01 WS-T PIC X(10).\n\
375             01 WS-N PIC 9(3).\n\
376             PROCEDURE DIVISION.\n\
377             MAIN.\n\
378                 INSPECT WS-T TALLYING WS-N FOR ALL 'X'.\n\
379                 INSPECT WS-T REPLACING ALL 'X' BY 'Y'.\n");
380        assert!(out.contains(".matches("));
381        assert!(out.contains("Decimal::from_integer(__n)"));
382        assert!(out.contains(".replace("));
383    }
384
385    #[test]
386    fn evaluate_true_and_range_emit() {
387        let out = gen("DATA DIVISION.\n\
388             WORKING-STORAGE SECTION.\n\
389             01 WS-X PIC 9(3).\n\
390             PROCEDURE DIVISION.\n\
391             MAIN.\n\
392                 EVALUATE WS-X WHEN 1 THRU 9 DISPLAY 'R' END-EVALUATE.\n\
393                 EVALUATE TRUE WHEN WS-X > 5 DISPLAY 'T' END-EVALUATE.\n");
394        assert!(out.contains(">= (dec(\"1\"))"));
395        assert!(out.contains("<= (dec(\"9\"))"));
396        assert!(out.contains("> (dec(\"5\"))"));
397    }
398
399    #[test]
400    fn initialize_resets_group_members() {
401        let out = gen("DATA DIVISION.\n\
402             WORKING-STORAGE SECTION.\n\
403             01 WS-REC.\n\
404                05 WS-A PIC 9(3).\n\
405                05 WS-B PIC X(4).\n\
406             PROCEDURE DIVISION.\n\
407             MAIN.\n\
408                 INITIALIZE WS-REC.\n");
409        assert!(out.contains("self.ws_a.store(Decimal::zero());"));
410        assert!(out.contains("self.ws_b.fill(' ');"));
411    }
412
413    #[test]
414    fn set_to_true_moves_the_88_value() {
415        let out = gen("DATA DIVISION.\n\
416             WORKING-STORAGE SECTION.\n\
417             01 WS-F PIC X VALUE 'N'.\n\
418                88 LISTO VALUE 'S'.\n\
419             PROCEDURE DIVISION.\n\
420             MAIN.\n\
421                 SET LISTO TO TRUE.\n");
422        assert!(out.contains("self.ws_f.store(\"S\");"));
423    }
424
425    #[test]
426    fn perform_thru_calls_the_paragraph_range() {
427        let out = gen("PROCEDURE DIVISION.\n\
428             MAIN.\n\
429                 PERFORM A THRU C.\n\
430             A.\n\
431                 DISPLAY 'A'.\n\
432             B.\n\
433                 DISPLAY 'B'.\n\
434             C.\n\
435                 DISPLAY 'C'.\n");
436        // `PERFORM A THRU C` emite la llamada a p_b dentro de p_main,
437        // además de la que hace run() — de ahí >= 2 apariciones.
438        assert!(out.matches("self.p_b();").count() >= 2);
439    }
440
441    #[test]
442    fn empty_program_still_compiles_shape() {
443        let out = gen("");
444        assert!(out.contains("struct Program {"));
445        assert!(out.contains("fn main() {"));
446        assert!(out.contains("fn run(&mut self) {"));
447    }
448}