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>
This commit is contained in:
sergio
2026-05-21 22:36:53 +00:00
parent 82ba0b7a1a
commit f250fd0765
10 changed files with 155 additions and 41 deletions
+25 -26
View File
@@ -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("");
@@ -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),
};
@@ -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())