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