gioser

tawasuyu/brahman

Rust 459TOML 177Markdown 49JSON 176207 Definitions

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: el intérprete sombra y el crate
  compilado por scaffold dan la misma salida.

Tests: charka-codegen 24, charka-shadow 22. fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

f250fd0765d4b7b91b1bf9f58efa17c429924abf · by sergio · (5 months ago) · parent 82ba0b7a1 · browse the tree at this commit

Semantic delta · 10

crates/modules/charka/charka-codegen/src/lib.rs

crates/modules/charka/charka-codegen/src/stmt.rs

crates/modules/charka/charka-codegen/src/sym.rs

crates/modules/charka/charka-shadow/src/interp.rs

10 files · +155 −41

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<String, u32> = 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<u32>,
 }
 
-/// 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<Field>,
     by_name: HashMap<String, usize>,
     conditions: HashMap<String, ConditionName>,
     groups: HashMap<String, Vec<String>>,
+    /// 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<Field> = 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<String, u32> = 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<String> {
+        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