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"
|
empieza el verbo del siguiente. El parser usa palabras "frontera"
|
||||||
(verbos + terminadores `END-*`/`ELSE` + conectores `TO`/`GIVING`...)
|
(verbos + terminadores `END-*`/`ELSE` + conectores `TO`/`GIVING`...)
|
||||||
para delimitar listas de operandos.
|
para delimitar listas de operandos.
|
||||||
- `PERFORM` cubre las cuatro formas: párrafo / en línea, `n TIMES`,
|
- `PERFORM` cubre las cuatro formas: párrafo (incluido el rango
|
||||||
`UNTIL cond` y `VARYING var FROM x BY y UNTIL cond`.
|
`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`
|
- `EVALUATE` — el `case` de COBOL: `WHEN valor`, `WHEN lo THRU hi`
|
||||||
(rango), `WHEN OTHER`, y la forma `EVALUATE TRUE WHEN condición`.
|
(rango), `WHEN OTHER`, y la forma `EVALUATE TRUE WHEN condición`.
|
||||||
- `STRING` (concatenación) y `UNSTRING` (partición por delimitador) —
|
- `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
|
transpila a Rust que compila contra `charka-runtime` y produce la
|
||||||
salida correcta.
|
salida correcta.
|
||||||
- Fuera de alcance v1: grupos como campo propio, `REDEFINES`,
|
- 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
|
## charka-shadow
|
||||||
|
|
||||||
@@ -181,8 +182,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
|
|||||||
|
|
||||||
## El corpus
|
## El corpus
|
||||||
|
|
||||||
`crates/modules/charka/corpus/` — 16 programas COBOL graduados
|
`crates/modules/charka/corpus/` — 17 programas COBOL graduados
|
||||||
(`01-hola` … `16-bandera`), cada uno con su `.expected`. Ejercita el
|
(`01-hola` … `17-rangopar`), cada uno con su `.expected`. Ejercita el
|
||||||
pipeline completo de punta a punta. Ver su `README.md`.
|
pipeline completo de punta a punta. Ver su `README.md`.
|
||||||
|
|
||||||
## La CLI
|
## La CLI
|
||||||
|
|||||||
@@ -28,18 +28,16 @@ mod expr;
|
|||||||
mod stmt;
|
mod stmt;
|
||||||
mod sym;
|
mod sym;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use charka_ir::Ir;
|
||||||
|
|
||||||
use charka_ir::{Ir, Procedure};
|
|
||||||
|
|
||||||
use emit::Emitter;
|
use emit::Emitter;
|
||||||
use expr::rust_str;
|
use expr::rust_str;
|
||||||
use stmt::emit_stmt;
|
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`).
|
/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
|
||||||
pub fn generate(ir: &Ir) -> String {
|
pub fn generate(ir: &Ir) -> String {
|
||||||
let sym = Symbols::build(&ir.model);
|
let sym = Symbols::build(ir);
|
||||||
let mut em = Emitter::new();
|
let mut em = Emitter::new();
|
||||||
emit_header(&mut em);
|
emit_header(&mut em);
|
||||||
emit_struct(&mut em, &sym);
|
emit_struct(&mut em, &sym);
|
||||||
@@ -107,10 +105,9 @@ fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) {
|
|||||||
em.line("}");
|
em.line("}");
|
||||||
em.blank();
|
em.blank();
|
||||||
|
|
||||||
// Un método por párrafo.
|
// Un método por párrafo (en paralelo con `sym.paragraphs`).
|
||||||
let methods = paragraph_methods(ir);
|
for (i, proc) in ir.procedures.iter().enumerate() {
|
||||||
for (name, proc) in &methods {
|
em.line(&format!("fn {}(&mut self) {{", sym.paragraphs[i].1));
|
||||||
em.line(&format!("fn {name}(&mut self) {{"));
|
|
||||||
em.indent();
|
em.indent();
|
||||||
for s in &proc.body {
|
for s in &proc.body {
|
||||||
emit_stmt(em, sym, s);
|
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.
|
// run() — encadena los párrafos en orden.
|
||||||
em.line("fn run(&mut self) {");
|
em.line("fn run(&mut self) {");
|
||||||
em.indent();
|
em.indent();
|
||||||
if methods.is_empty() {
|
if sym.paragraphs.is_empty() {
|
||||||
em.line("// programa sin PROCEDURE division");
|
em.line("// programa sin PROCEDURE division");
|
||||||
}
|
}
|
||||||
for (name, _) in &methods {
|
for (_, method) in &sym.paragraphs {
|
||||||
em.line(&format!("self.{name}();"));
|
em.line(&format!("self.{method}();"));
|
||||||
}
|
}
|
||||||
em.dedent();
|
em.dedent();
|
||||||
em.line("}");
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -439,6 +422,22 @@ mod tests {
|
|||||||
assert!(out.contains("self.ws_f.store(\"S\");"));
|
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]
|
#[test]
|
||||||
fn empty_program_still_compiles_shape() {
|
fn empty_program_still_compiles_shape() {
|
||||||
let out = gen("");
|
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.
|
// 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 {
|
let emit_body = |em: &mut Emitter, sym: &Symbols| match &p.target {
|
||||||
PerformTarget::Paragraph { name, thru } => {
|
PerformTarget::Paragraph { name, thru } => {
|
||||||
let note = thru
|
for m in sym.paragraph_range(name, thru.as_deref()) {
|
||||||
.as_ref()
|
em.line(&format!("self.{m}();"));
|
||||||
.map(|t| format!(" // charka: THRU {t} — rango no soportado"))
|
}
|
||||||
.unwrap_or_default();
|
|
||||||
em.line(&format!("self.{}();{note}", paragraph_method(name)));
|
|
||||||
}
|
}
|
||||||
PerformTarget::Inline(body) => emit_block(em, sym, body),
|
PerformTarget::Inline(body) => emit_block(em, sym, body),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
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
|
/// El tipo de campo lo aporta `charka-ir`; se reexporta para que el
|
||||||
/// resto del crate lo nombre como `crate::sym::FieldKind`.
|
/// resto del crate lo nombre como `crate::sym::FieldKind`.
|
||||||
@@ -24,17 +24,21 @@ pub(crate) struct Field {
|
|||||||
pub occurs: Option<u32>,
|
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(crate) struct Symbols {
|
||||||
pub fields: Vec<Field>,
|
pub fields: Vec<Field>,
|
||||||
by_name: HashMap<String, usize>,
|
by_name: HashMap<String, usize>,
|
||||||
conditions: HashMap<String, ConditionName>,
|
conditions: HashMap<String, ConditionName>,
|
||||||
groups: HashMap<String, Vec<String>>,
|
groups: HashMap<String, Vec<String>>,
|
||||||
|
/// Los párrafos en orden: `(nombre COBOL, nombre de método Rust)`.
|
||||||
|
pub paragraphs: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Symbols {
|
impl Symbols {
|
||||||
/// Construye la tabla desde el modelo de datos resuelto.
|
/// Construye la tabla desde el IR (su modelo de datos y párrafos).
|
||||||
pub(crate) fn build(model: &DataModel) -> Self {
|
pub(crate) fn build(ir: &Ir) -> Self {
|
||||||
|
let model = &ir.model;
|
||||||
let mut fields: Vec<Field> = model
|
let mut fields: Vec<Field> = model
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -62,14 +66,56 @@ impl Symbols {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|g| (g.name.clone(), g.members.clone()))
|
.map(|g| (g.name.clone(), g.members.clone()))
|
||||||
.collect();
|
.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 {
|
Self {
|
||||||
fields,
|
fields,
|
||||||
by_name,
|
by_name,
|
||||||
conditions,
|
conditions,
|
||||||
groups,
|
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.
|
/// Los miembros de un grupo, si `name` es un grupo.
|
||||||
pub(crate) fn group(&self, name: &str) -> Option<&[String]> {
|
pub(crate) fn group(&self, name: &str) -> Option<&[String]> {
|
||||||
self.groups.get(&name.to_uppercase()).map(|v| v.as_slice())
|
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.
|
/// él termina esa pasada, no el programa.
|
||||||
fn run_target(&mut self, target: &'a PerformTarget) -> Flow {
|
fn run_target(&mut self, target: &'a PerformTarget) -> Flow {
|
||||||
let flow = match target {
|
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),
|
PerformTarget::Inline(body) => self.exec_block(body),
|
||||||
};
|
};
|
||||||
match flow {
|
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
|
/// Resuelve una referencia a dato (escalar o elemento de tabla) a
|
||||||
/// su nombre y un índice 0-based. `None` si no es una referencia.
|
/// su nombre y un índice 0-based. `None` si no es una referencia.
|
||||||
fn resolve(&self, op: &Operand) -> Option<(String, usize)> {
|
fn resolve(&self, op: &Operand) -> Option<(String, usize)> {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ mod tests {
|
|||||||
corpus_test!(corpus_14_clasifica, "14-clasifica");
|
corpus_test!(corpus_14_clasifica, "14-clasifica");
|
||||||
corpus_test!(corpus_15_resetear, "15-resetear");
|
corpus_test!(corpus_15_resetear, "15-resetear");
|
||||||
corpus_test!(corpus_16_bandera, "16-bandera");
|
corpus_test!(corpus_16_bandera, "16-bandera");
|
||||||
|
corpus_test!(corpus_17_rangopar, "17-rangopar");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_source_runs_clean() {
|
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` |
|
| `14-clasifica` | 6 | `EVALUATE TRUE` y rangos `WHEN ... THRU` |
|
||||||
| `15-resetear` | 6 | `INITIALIZE` — resetear datos y grupos |
|
| `15-resetear` | 6 | `INITIALIZE` — resetear datos y grupos |
|
||||||
| `16-bandera` | 5 | `SET` de nombres de condición (nivel 88) a `TRUE` |
|
| `16-bandera` | 5 | `SET` de nombres de condición (nivel 88) a `TRUE` |
|
||||||
|
| `17-rangopar` | 5 | `PERFORM ... THRU` — un rango de párrafos |
|
||||||
|
|
||||||
## Formato
|
## Formato
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,20 @@
|
|||||||
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
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.
|
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)
|
### 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
|
La cara de escritura de los nombres de condición de COBOL: si `IF
|
||||||
|
|||||||
Reference in New Issue
Block a user