⎇ fa65f202065abca26e0fb28581427117b84af85c
crates/modules/charka/charka-codegen/src/lib.rs
History
Who touched what
raw
feat(charka): INITIALIZE — resetear datos y grupos
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 empty_program_still_compiles_shape() {
432 let out = gen("");
433 assert!(out.contains("struct Program {"));
434 assert!(out.contains("fn main() {"));
435 assert!(out.contains("fn run(&mut self) {"));
436 }
437}