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>
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