gioser

tawasuyu/brahman

Rust 296TOML 105JSON 14Markdown 73751 Sutichaykuna

⎇ 634a43006ac4257288af40320b5360e9f157ab09 crates/modules/charka/charka-codegen/src/lib.rs Rust · 499 siq'ikuna · 16.0 KB Ñawpa willay Pi imata llamkarqan ch'uya

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