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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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'.
|
||||
@@ -0,0 +1,4 @@
|
||||
PASO A
|
||||
PASO B
|
||||
PASO C
|
||||
X FINAL = 111
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user