diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 6dda34e..664e6f6 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -98,8 +98,9 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda empieza el verbo del siguiente. El parser usa palabras "frontera" (verbos + terminadores `END-*`/`ELSE` + conectores `TO`/`GIVING`...) para delimitar listas de operandos. -- `PERFORM` cubre las cuatro formas: párrafo / en línea, `n TIMES`, - `UNTIL cond` y `VARYING var FROM x BY y UNTIL cond`. +- `PERFORM` cubre las cuatro formas: párrafo (incluido el rango + `THRU`) / en línea, `n TIMES`, `UNTIL cond` y `VARYING var FROM x + BY y UNTIL cond`. - `EVALUATE` — el `case` de COBOL: `WHEN valor`, `WHEN lo THRU hi` (rango), `WHEN OTHER`, y la forma `EVALUATE TRUE WHEN condición`. - `STRING` (concatenación) y `UNSTRING` (partición por delimitador) — @@ -158,7 +159,7 @@ del programa COBOL. transpila a Rust que compila contra `charka-runtime` y produce la salida correcta. - Fuera de alcance v1: grupos como campo propio, `REDEFINES`, - `OCCURS` de grupo, `PERFORM ... THRU` como rango, E/S de ficheros. + `OCCURS` de grupo, E/S de ficheros. ## charka-shadow @@ -181,8 +182,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar. ## El corpus -`crates/modules/charka/corpus/` — 16 programas COBOL graduados -(`01-hola` … `16-bandera`), cada uno con su `.expected`. Ejercita el +`crates/modules/charka/corpus/` — 17 programas COBOL graduados +(`01-hola` … `17-rangopar`), cada uno con su `.expected`. Ejercita el pipeline completo de punta a punta. Ver su `README.md`. ## La CLI diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs index 0b5ffd2..2c0be88 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -28,18 +28,16 @@ mod expr; mod stmt; mod sym; -use std::collections::HashMap; - -use charka_ir::{Ir, Procedure}; +use charka_ir::Ir; use emit::Emitter; use expr::rust_str; use stmt::emit_stmt; -use sym::{paragraph_method, Field, FieldKind, Symbols}; +use sym::{Field, FieldKind, Symbols}; /// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`). pub fn generate(ir: &Ir) -> String { - let sym = Symbols::build(&ir.model); + let sym = Symbols::build(ir); let mut em = Emitter::new(); emit_header(&mut em); emit_struct(&mut em, &sym); @@ -107,10 +105,9 @@ fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) { em.line("}"); em.blank(); - // Un método por párrafo. - let methods = paragraph_methods(ir); - for (name, proc) in &methods { - em.line(&format!("fn {name}(&mut self) {{")); + // Un método por párrafo (en paralelo con `sym.paragraphs`). + for (i, proc) in ir.procedures.iter().enumerate() { + em.line(&format!("fn {}(&mut self) {{", sym.paragraphs[i].1)); em.indent(); for s in &proc.body { emit_stmt(em, sym, s); @@ -123,11 +120,11 @@ fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) { // run() — encadena los párrafos en orden. em.line("fn run(&mut self) {"); em.indent(); - if methods.is_empty() { + if sym.paragraphs.is_empty() { em.line("// programa sin PROCEDURE division"); } - for (name, _) in &methods { - em.line(&format!("self.{name}();")); + for (_, method) in &sym.paragraphs { + em.line(&format!("self.{method}();")); } em.dedent(); em.line("}"); @@ -165,20 +162,6 @@ fn field_init(f: &Field) -> String { } } -/// Asigna a cada párrafo un nombre de método único. -fn paragraph_methods(ir: &Ir) -> Vec<(String, &Procedure)> { - let mut seen: HashMap = HashMap::new(); - let mut out = Vec::new(); - for proc in &ir.procedures { - let base = paragraph_method(&proc.name); - let n = seen.entry(base.clone()).or_insert(0); - let name = if *n > 0 { format!("{base}_{n}") } else { base }; - *n += 1; - out.push((name, proc)); - } - out -} - #[cfg(test)] mod tests { use super::*; @@ -439,6 +422,22 @@ mod tests { assert!(out.contains("self.ws_f.store(\"S\");")); } + #[test] + fn perform_thru_calls_the_paragraph_range() { + let out = gen("PROCEDURE DIVISION.\n\ + MAIN.\n\ + PERFORM A THRU C.\n\ + A.\n\ + DISPLAY 'A'.\n\ + B.\n\ + DISPLAY 'B'.\n\ + C.\n\ + DISPLAY 'C'.\n"); + // `PERFORM A THRU C` emite la llamada a p_b dentro de p_main, + // además de la que hace run() — de ahí >= 2 apariciones. + assert!(out.matches("self.p_b();").count() >= 2); + } + #[test] fn empty_program_still_compiles_shape() { let out = gen(""); diff --git a/crates/modules/charka/charka-codegen/src/stmt.rs b/crates/modules/charka/charka-codegen/src/stmt.rs index 9bbf22d..dbe7e81 100644 --- a/crates/modules/charka/charka-codegen/src/stmt.rs +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -543,11 +543,9 @@ fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) { // Emite el "cuerpo": la llamada al párrafo o el bloque en línea. let emit_body = |em: &mut Emitter, sym: &Symbols| match &p.target { PerformTarget::Paragraph { name, thru } => { - let note = thru - .as_ref() - .map(|t| format!(" // charka: THRU {t} — rango no soportado")) - .unwrap_or_default(); - em.line(&format!("self.{}();{note}", paragraph_method(name))); + for m in sym.paragraph_range(name, thru.as_deref()) { + em.line(&format!("self.{m}();")); + } } PerformTarget::Inline(body) => emit_block(em, sym, body), }; diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs index 95b6817..c18d764 100644 --- a/crates/modules/charka/charka-codegen/src/sym.rs +++ b/crates/modules/charka/charka-codegen/src/sym.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; -use charka_ir::{ConditionName, DataModel}; +use charka_ir::{ConditionName, Ir}; /// El tipo de campo lo aporta `charka-ir`; se reexporta para que el /// resto del crate lo nombre como `crate::sym::FieldKind`. @@ -24,17 +24,21 @@ pub(crate) struct Field { pub occurs: Option, } -/// Los campos del programa, sus nombres de condición y sus grupos. +/// Los campos del programa, sus nombres de condición, sus grupos y +/// sus párrafos. pub(crate) struct Symbols { pub fields: Vec, by_name: HashMap, conditions: HashMap, groups: HashMap>, + /// Los párrafos en orden: `(nombre COBOL, nombre de método Rust)`. + pub paragraphs: Vec<(String, String)>, } impl Symbols { - /// Construye la tabla desde el modelo de datos resuelto. - pub(crate) fn build(model: &DataModel) -> Self { + /// Construye la tabla desde el IR (su modelo de datos y párrafos). + pub(crate) fn build(ir: &Ir) -> Self { + let model = &ir.model; let mut fields: Vec = model .fields .iter() @@ -62,14 +66,56 @@ impl Symbols { .iter() .map(|g| (g.name.clone(), g.members.clone())) .collect(); + // Párrafos en orden, con su nombre de método único. + let mut seen: HashMap = HashMap::new(); + let paragraphs = ir + .procedures + .iter() + .map(|proc| { + let base = paragraph_method(&proc.name); + let n = seen.entry(base.clone()).or_insert(0); + let method = if *n > 0 { format!("{base}_{n}") } else { base }; + *n += 1; + (proc.name.to_uppercase(), method) + }) + .collect(); Self { fields, by_name, conditions, groups, + paragraphs, } } + /// Los métodos a llamar para un `PERFORM name [THRU thru]`: el + /// rango de párrafos desde `name` hasta `thru` inclusive. + pub(crate) fn paragraph_range(&self, name: &str, thru: Option<&str>) -> Vec { + let up = name.to_uppercase(); + let Some(start) = self.paragraphs.iter().position(|(c, _)| *c == up) else { + return vec![paragraph_method(name)]; + }; + let end = match thru { + Some(t) => { + let tu = t.to_uppercase(); + self.paragraphs + .iter() + .position(|(c, _)| *c == tu) + .unwrap_or(start) + } + None => start, + }; + let (lo, hi) = if start <= end { + (start, end) + } else { + (end, start) + }; + self.paragraphs[lo..=hi] + .iter() + .map(|(_, m)| m.clone()) + .collect() + } + /// Los miembros de un grupo, si `name` es un grupo. pub(crate) fn group(&self, name: &str) -> Option<&[String]> { self.groups.get(&name.to_uppercase()).map(|v| v.as_slice()) diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index d0d83fb..02975ea 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -391,7 +391,9 @@ impl<'a> Machine<'a> { /// él termina esa pasada, no el programa. fn run_target(&mut self, target: &'a PerformTarget) -> Flow { let flow = match target { - PerformTarget::Paragraph { name, .. } => self.run_paragraph(name), + PerformTarget::Paragraph { name, thru } => { + self.run_paragraph_range(name, thru.as_deref()) + } PerformTarget::Inline(body) => self.exec_block(body), }; match flow { @@ -411,6 +413,34 @@ impl<'a> Machine<'a> { } } + /// Ejecuta el rango de párrafos de `name` a `thru` inclusive (el + /// `PERFORM name THRU thru`); sólo `name` si `thru` es `None`. + fn run_paragraph_range(&mut self, name: &str, thru: Option<&str>) -> Flow { + let Some(&start) = self.para_index.get(&name.to_uppercase()) else { + return Flow::Normal; + }; + let end = match thru { + Some(t) => self + .para_index + .get(&t.to_uppercase()) + .copied() + .unwrap_or(start), + None => start, + }; + let (lo, hi) = if start <= end { + (start, end) + } else { + (end, start) + }; + let ir = self.ir; + for i in lo..=hi { + if let Flow::Stop = self.exec_block(&ir.procedures[i].body) { + return Flow::Stop; + } + } + Flow::Normal + } + /// Resuelve una referencia a dato (escalar o elemento de tabla) a /// su nombre y un índice 0-based. `None` si no es una referencia. fn resolve(&self, op: &Operand) -> Option<(String, usize)> { diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 3404d63..1efab6b 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -125,6 +125,7 @@ mod tests { corpus_test!(corpus_14_clasifica, "14-clasifica"); corpus_test!(corpus_15_resetear, "15-resetear"); corpus_test!(corpus_16_bandera, "16-bandera"); + corpus_test!(corpus_17_rangopar, "17-rangopar"); #[test] fn empty_source_runs_clean() { diff --git a/crates/modules/charka/corpus/17-rangopar.cob b/crates/modules/charka/corpus/17-rangopar.cob new file mode 100644 index 0000000..7a5bd92 --- /dev/null +++ b/crates/modules/charka/corpus/17-rangopar.cob @@ -0,0 +1,20 @@ +* corpus charka — nivel 5: PERFORM ... THRU (rango de párrafos) +IDENTIFICATION DIVISION. +PROGRAM-ID. RANGOPAR. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-X PIC 9(3) VALUE 0. +PROCEDURE DIVISION. +MAIN. + PERFORM PASO-A THRU PASO-C. + DISPLAY 'X FINAL = ' WS-X. + STOP RUN. +PASO-A. + ADD 1 TO WS-X. + DISPLAY 'PASO A'. +PASO-B. + ADD 10 TO WS-X. + DISPLAY 'PASO B'. +PASO-C. + ADD 100 TO WS-X. + DISPLAY 'PASO C'. diff --git a/crates/modules/charka/corpus/17-rangopar.expected b/crates/modules/charka/corpus/17-rangopar.expected new file mode 100644 index 0000000..dd56e9b --- /dev/null +++ b/crates/modules/charka/corpus/17-rangopar.expected @@ -0,0 +1,4 @@ +PASO A +PASO B +PASO C +X FINAL = 111 diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index 4a20837..1c947f8 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -25,6 +25,7 @@ salida correcta, una línea por `DISPLAY`. | `14-clasifica` | 6 | `EVALUATE TRUE` y rangos `WHEN ... THRU` | | `15-resetear` | 6 | `INITIALIZE` — resetear datos y grupos | | `16-bandera` | 5 | `SET` de nombres de condición (nivel 88) a `TRUE` | +| `17-rangopar` | 5 | `PERFORM ... THRU` — un rango de párrafos | ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 5445c36..9ed626a 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,20 @@ Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D del plan macro) — el parser COBOL completo es un esfuerzo multi-mes. +### feat(charka): PERFORM ... THRU como rango real de párrafos + +`PERFORM A THRU C` ejecuta A, B y C; antes el transpilador sólo +ejecutaba A (lo marcaba como aproximado). + +- `charka-codegen`: `Symbols` registra ahora los párrafos en orden con + su nombre de método; `Symbols::build` toma el `Ir` completo. + `paragraph_range(name, thru)` da los métodos del rango. `emit_perform` + emite la llamada a cada uno. +- `charka-shadow`: `run_paragraph_range` ejecuta los párrafos de + `name` a `thru` inclusive. +- Corpus: programa nuevo `17-rangopar` (`PERFORM PASO-A THRU PASO-C` + sobre tres párrafos). Verificado en ambas rutas. + ### feat(charka): SET ... TO TRUE — escribir nombres de condición (88) La cara de escritura de los nombres de condición de COBOL: si `IF