gioser

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114554 Sutichaykuna

⎇ 28ee1ae2602662b216549ba4db0d34a9b55b8deb crates/modules/charka/charka-codegen/src/lib.rs Rust · 352 siq'ikuna · 10.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(charka): nivel 88 + modelo de datos compartido en charka-ir 28ee1ae26 · 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 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 `VALUE` ya
146/// normalizado por `charka-ir`.
147fn field_init(f: &Field) -> String {
148    match &f.kind {
149        FieldKind::Num { int, frac, signed } => format!(
150            "Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
151            rust_str(&f.init)
152        ),
153        FieldKind::Text { len } => {
154            format!("Text::with_value({len}, {})", rust_str(&f.init))
155        }
156    }
157}
158
159/// Asigna a cada párrafo un nombre de método único.
160fn paragraph_methods(ir: &Ir) -> Vec<(String, &Procedure)> {
161    let mut seen: HashMap<String, u32> = HashMap::new();
162    let mut out = Vec::new();
163    for proc in &ir.procedures {
164        let base = paragraph_method(&proc.name);
165        let n = seen.entry(base.clone()).or_insert(0);
166        let name = if *n > 0 { format!("{base}_{n}") } else { base };
167        *n += 1;
168        out.push((name, proc));
169    }
170    out
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    /// Helper: lexa, parsea, baja a IR y transpila un fuente COBOL.
178    fn gen(src: &str) -> String {
179        let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).expect("lex");
180        let prog = charka_parser::parse(&toks).expect("parse");
181        let ir = charka_ir::lower(&prog);
182        generate(&ir)
183    }
184
185    /// Un programa COBOL de demostración, razonablemente completo.
186    const DEMO: &str = "IDENTIFICATION DIVISION.\n\
187         PROGRAM-ID. DEMO.\n\
188         DATA DIVISION.\n\
189         WORKING-STORAGE SECTION.\n\
190         01 WS-A    PIC 9(3) VALUE 10.\n\
191         01 WS-B    PIC 9(3).\n\
192         01 WS-NAME PIC X(8) VALUE 'BOB'.\n\
193         PROCEDURE DIVISION.\n\
194         MAIN-PARA.\n\
195             MOVE 5 TO WS-B.\n\
196             COMPUTE WS-B = WS-A + WS-B.\n\
197             DISPLAY 'B=' WS-B.\n\
198             IF WS-B > 0 DISPLAY 'POS' END-IF.\n\
199             PERFORM SUB-PARA.\n\
200             STOP RUN.\n\
201         SUB-PARA.\n\
202             DISPLAY WS-NAME.\n";
203
204    #[test]
205    fn header_and_main_are_emitted() {
206        let out = gen(DEMO);
207        assert!(out.contains("use charka_runtime::*;"));
208        assert!(out.contains("fn dec(s: &str) -> Decimal {"));
209        assert!(out.contains("fn main() {"));
210        assert!(out.contains("Program::new().run();"));
211    }
212
213    #[test]
214    fn numeric_field_becomes_num() {
215        let out = gen(DEMO);
216        assert!(out.contains("ws_a: Num,"));
217        assert!(out.contains("Num::with_value(Picture::new(3, 0, false), \"10\")"));
218    }
219
220    #[test]
221    fn alphanumeric_field_becomes_text() {
222        let out = gen(DEMO);
223        assert!(out.contains("ws_name: Text,"));
224        assert!(out.contains("Text::with_value(8, \"BOB\")"));
225    }
226
227    #[test]
228    fn move_emits_a_store() {
229        assert!(gen(DEMO).contains("self.ws_b.store(dec(\"5\"));"));
230    }
231
232    #[test]
233    fn compute_emits_the_expression() {
234        let out = gen(DEMO);
235        assert!(out.contains("self.ws_b.store((self.ws_a.value()).add(&(self.ws_b.value())));"));
236    }
237
238    #[test]
239    fn display_emits_a_println() {
240        let out = gen(DEMO);
241        assert!(out.contains("println!(\"{}{}\", \"B=\", self.ws_b.display());"));
242    }
243
244    #[test]
245    fn if_emits_a_rust_if() {
246        let out = gen(DEMO);
247        assert!(out.contains("if (self.ws_b.value()) > (dec(\"0\")) {"));
248    }
249
250    #[test]
251    fn paragraphs_become_methods_and_run_chains_them() {
252        let out = gen(DEMO);
253        assert!(out.contains("fn p_main_para(&mut self) {"));
254        assert!(out.contains("fn p_sub_para(&mut self) {"));
255        assert!(out.contains("fn run(&mut self) {"));
256        assert!(out.contains("self.p_main_para();"));
257        assert!(out.contains("self.p_sub_para();"));
258    }
259
260    #[test]
261    fn perform_calls_the_paragraph_method() {
262        assert!(gen(DEMO).contains("self.p_sub_para();"));
263    }
264
265    #[test]
266    fn stop_run_exits() {
267        assert!(gen(DEMO).contains("std::process::exit(0);"));
268    }
269
270    #[test]
271    fn unknown_verb_becomes_a_comment() {
272        let out = gen("PROCEDURE DIVISION.\n\
273             MAIN.\n\
274                 INSPECT WS-X TALLYING WS-N FOR ALL ' '.\n");
275        assert!(out.contains("// charka: verbo no transpilado — INSPECT"));
276    }
277
278    #[test]
279    fn add_giving_emits_a_sum() {
280        let out = gen("DATA DIVISION.\n\
281             WORKING-STORAGE SECTION.\n\
282             01 A PIC 9(3).\n\
283             01 B PIC 9(3).\n\
284             01 C PIC 9(3).\n\
285             PROCEDURE DIVISION.\n\
286             MAIN.\n\
287                 ADD A B GIVING C.\n");
288        assert!(out.contains("self.c.store((self.a.value()).add(&(self.b.value())));"));
289    }
290
291    #[test]
292    fn perform_times_emits_a_loop() {
293        let out = gen("PROCEDURE DIVISION.\n\
294             MAIN.\n\
295                 PERFORM SUB 3 TIMES.\n\
296             SUB.\n\
297                 CONTINUE.\n");
298        assert!(out.contains("for _ in 0..3usize {"));
299    }
300
301    #[test]
302    fn perform_varying_emits_init_loop_and_increment() {
303        let out = gen("DATA DIVISION.\n\
304             WORKING-STORAGE SECTION.\n\
305             01 WS-I PIC 9(2).\n\
306             01 WS-N PIC 9(3).\n\
307             PROCEDURE DIVISION.\n\
308             MAIN.\n\
309                 PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5\n\
310                     ADD 1 TO WS-N\n\
311                 END-PERFORM.\n");
312        assert!(out.contains("self.ws_i.store(dec(\"1\"));"));
313        assert!(out.contains("while !((self.ws_i.value()) > (dec(\"5\"))) {"));
314        assert!(out.contains("self.ws_i.store(self.ws_i.value().add(&(dec(\"1\"))));"));
315    }
316
317    #[test]
318    fn evaluate_emits_an_if_else_chain() {
319        let out = gen("DATA DIVISION.\n\
320             WORKING-STORAGE SECTION.\n\
321             01 WS-X PIC 9(1).\n\
322             PROCEDURE DIVISION.\n\
323             MAIN.\n\
324                 EVALUATE WS-X\n\
325                     WHEN 1 DISPLAY 'UNO'\n\
326                     WHEN OTHER DISPLAY 'OTRO'\n\
327                 END-EVALUATE.\n");
328        assert!(out.contains("if ((self.ws_x.value()) == (dec(\"1\"))) {"));
329        assert!(out.contains("} else {"));
330    }
331
332    #[test]
333    fn level_88_condition_resolves_to_a_comparison() {
334        let out = gen("DATA DIVISION.\n\
335             WORKING-STORAGE SECTION.\n\
336             01 WS-FLAG PIC X VALUE 'N'.\n\
337                88 ES-SI VALUE 'Y'.\n\
338             PROCEDURE DIVISION.\n\
339             MAIN.\n\
340                 IF ES-SI DISPLAY 'SI' END-IF.\n");
341        // ES-SI equivale a `WS-FLAG = 'Y'` (comparación de texto).
342        assert!(out.contains("cobol_text_cmp(self.ws_flag.display().as_str(), \"Y\").is_eq()"));
343    }
344
345    #[test]
346    fn empty_program_still_compiles_shape() {
347        let out = gen("");
348        assert!(out.contains("struct Program {"));
349        assert!(out.contains("fn main() {"));
350        assert!(out.contains("fn run(&mut self) {"));
351    }
352}