⎇ 28ee1ae2602662b216549ba4db0d34a9b55b8deb
crates/modules/charka/charka-codegen/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): nivel 88 + modelo de datos compartido en charka-ir
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}