⎇ f250fd0765d4b7b91b1bf9f58efa17c429924abf
crates/modules/charka/charka-codegen/src/lib.rs
Historia
Quién tocó qué
crudo
feat(charka): PERFORM ... THRU como rango real de párrafos
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}