tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Sutichaykuna

⎇ 82ba0b7a1a04b7ff2d7e2175ac74766da0d16066 crates/modules/charka/charka-codegen/src/lib.rs Rust · 449 siq'ikuna · 14.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

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