⎇ 634a43006ac4257288af40320b5360e9f157ab09
crates/modules/charka/charka-codegen/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): PICTURE de edición — Z, coma de millares y punto decimal
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}