diff --git a/crates/apps/charka/src/main.rs b/crates/apps/charka/src/main.rs index 9d0d864..90ffe25 100644 --- a/crates/apps/charka/src/main.rs +++ b/crates/apps/charka/src/main.rs @@ -290,13 +290,13 @@ mod tests { let ir = ir_of( "PROCEDURE DIVISION.\n\ MAIN.\n\ - INITIALIZE WS-X.\n", + CALL 'SUBPROG'.\n", ); let mut verbs = Vec::new(); for proc in &ir.procedures { collect_unknowns(&proc.body, &mut verbs); } - assert_eq!(verbs, vec!["INITIALIZE".to_string()]); + assert_eq!(verbs, vec!["CALL".to_string()]); } #[test] diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index bd20fbc..611bf3d 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -85,8 +85,9 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda reimplementar la clasificación. - `Procedure { name, body: Vec }`. `Stmt` cubre `Move`, `Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`, - `If`, `Evaluate`, `StringConcat`, `Unstring`, `Inspect`, `Perform`, - `GoTo`, `StopRun`, `Goback`, `Exit`, `Continue`. + `If`, `Evaluate`, `StringConcat`, `Unstring`, `Inspect`, + `Initialize`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`, + `Continue`. - `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt: `+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición @@ -104,6 +105,9 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda - `STRING` (concatenación) y `UNSTRING` (partición por delimitador) — el manejo de cadenas. `INSPECT` — contar (`TALLYING FOR ALL`) y reemplazar (`REPLACING ALL`). +- `INITIALIZE` — resetea un dato (o todos los elementales de un grupo) + a su valor por defecto. El `model` registra los grupos y sus + miembros (`DataModel::groups`). - Fuera de alcance v1: E/S de ficheros, CICS, SQL embebido. ## charka-runtime @@ -175,8 +179,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar. ## El corpus -`crates/modules/charka/corpus/` — 14 programas COBOL graduados -(`01-hola` … `14-clasifica`), cada uno con su `.expected`. Ejercita el +`crates/modules/charka/corpus/` — 15 programas COBOL graduados +(`01-hola` … `15-resetear`), 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 df69ee9..92cffc0 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -280,8 +280,8 @@ mod tests { fn unknown_verb_becomes_a_comment() { let out = gen("PROCEDURE DIVISION.\n\ MAIN.\n\ - INITIALIZE WS-X.\n"); - assert!(out.contains("// charka: verbo no transpilado — INITIALIZE")); + CALL 'SUBPROG'.\n"); + assert!(out.contains("// charka: verbo no transpilado — CALL")); } #[test] @@ -413,6 +413,20 @@ mod tests { assert!(out.contains("> (dec(\"5\"))")); } + #[test] + fn initialize_resets_group_members() { + let out = gen("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-REC.\n\ + 05 WS-A PIC 9(3).\n\ + 05 WS-B PIC X(4).\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + INITIALIZE WS-REC.\n"); + assert!(out.contains("self.ws_a.store(Decimal::zero());")); + assert!(out.contains("self.ws_b.fill(' ');")); + } + #[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 d0903e5..3542dba 100644 --- a/crates/modules/charka/charka-codegen/src/stmt.rs +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -86,6 +86,7 @@ pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) { into, } => emit_unstring(em, sym, source, delimiter, into), Stmt::Inspect { target, op } => emit_inspect(em, sym, target, op), + Stmt::Initialize { targets } => emit_initialize(em, sym, targets), Stmt::Perform(p) => emit_perform(em, sym, p), Stmt::GoTo { target } => { em.line(&format!( @@ -472,6 +473,56 @@ fn emit_inspect(em: &mut Emitter, sym: &Symbols, target: &Operand, op: &InspectO } } +/// `INITIALIZE` — pone cada destino (o los miembros de un grupo) en su +/// valor por defecto. +fn emit_initialize(em: &mut Emitter, sym: &Symbols, targets: &[Operand]) { + for t in targets { + match t { + Operand::Data(name) => match sym.group(name) { + Some(members) => { + for m in members { + emit_reset(em, sym, m); + } + } + None => emit_reset(em, sym, name), + }, + Operand::Indexed { .. } => emit_reset_element(em, sym, t), + _ => {} + } + } +} + +/// Resetea un campo completo (escalar o tabla entera). +fn emit_reset(em: &mut Emitter, sym: &Symbols, name: &str) { + let Some(f) = sym.lookup(name) else { + em.line(&format!("// charka: INITIALIZE de {name} no resuelto")); + return; + }; + let reset = match f.kind { + FieldKind::Num { .. } => "store(Decimal::zero())", + FieldKind::Text { .. } => "fill(' ')", + }; + match f.occurs { + None => em.line(&format!("self.{}.{reset};", f.ident)), + Some(_) => { + em.line(&format!("for __e in self.{}.iter_mut() {{", f.ident)); + em.indent(); + em.line(&format!("__e.{reset};")); + em.dedent(); + em.line("}"); + } + } +} + +/// Resetea un solo elemento de tabla (`INITIALIZE ELEM(I)`). +fn emit_reset_element(em: &mut Emitter, sym: &Symbols, op: &Operand) { + match field_ref(sym, op) { + Some((lref, FieldKind::Num { .. })) => em.line(&format!("{lref}.store(Decimal::zero());")), + Some((lref, FieldKind::Text { .. })) => em.line(&format!("{lref}.fill(' ');")), + None => em.line("// charka: INITIALIZE no resuelto"), + } +} + 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 { diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs index 1b319a5..95b6817 100644 --- a/crates/modules/charka/charka-codegen/src/sym.rs +++ b/crates/modules/charka/charka-codegen/src/sym.rs @@ -24,11 +24,12 @@ pub(crate) struct Field { pub occurs: Option, } -/// Los campos del programa y sus nombres de condición, indexados. +/// Los campos del programa, sus nombres de condición y sus grupos. pub(crate) struct Symbols { pub fields: Vec, by_name: HashMap, conditions: HashMap, + groups: HashMap>, } impl Symbols { @@ -56,13 +57,24 @@ impl Symbols { .iter() .map(|c| (c.name.clone(), c.clone())) .collect(); + let groups = model + .groups + .iter() + .map(|g| (g.name.clone(), g.members.clone())) + .collect(); Self { fields, by_name, conditions, + groups, } } + /// 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()) + } + /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas). pub(crate) fn lookup(&self, cobol: &str) -> Option<&Field> { self.by_name diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs index 9a0db93..77e6238 100644 --- a/crates/modules/charka/charka-ir/src/ast.rs +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -185,6 +185,9 @@ pub enum Stmt { }, /// `INSPECT target ...` — cuenta o reemplaza caracteres. Inspect { target: Operand, op: InspectOp }, + /// `INITIALIZE targets...` — pone cada dato (o grupo) en su valor + /// por defecto: 0 los numéricos, espacios los alfanuméricos. + Initialize { targets: Vec }, /// `PERFORM ...` — ver [`Perform`]. Perform(Perform), /// `GO TO target` diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs index 996b4a4..93a29c6 100644 --- a/crates/modules/charka/charka-ir/src/lib.rs +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -32,7 +32,7 @@ mod stmt; pub use ast::*; pub use charka_parser::Program; -pub use model::{resolve_data, ConditionName, DataModel, Field, FieldKind}; +pub use model::{resolve_data, ConditionName, DataModel, Field, FieldKind, GroupInfo}; use cursor::Cursor; @@ -440,6 +440,26 @@ mod tests { } } + #[test] + fn initialize_parses_and_groups_are_modeled() { + let program = ir("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-REC.\n\ + 05 WS-A PIC 9(3).\n\ + 05 WS-B PIC X(4).\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + INITIALIZE WS-REC.\n"); + let g = program.model.group("WS-REC").expect("grupo WS-REC"); + assert_eq!(g.members, vec!["WS-A".to_string(), "WS-B".to_string()]); + match &program.procedures[0].body[0] { + Stmt::Initialize { targets } => { + assert_eq!(targets, &vec![Operand::Data("WS-REC".into())]); + } + other => panic!("se esperaba INITIALIZE, vino {other:?}"), + } + } + #[test] fn several_statements_in_one_sentence() { let b = body("MOVE 1 TO X DISPLAY X STOP RUN."); @@ -451,10 +471,10 @@ mod tests { #[test] fn unrecognized_verb_becomes_unknown() { - let b = body("INITIALIZE WS-X WS-Y."); + let b = body("CALL 'SUBPROG' USING WS-X."); match &b[0] { Stmt::Unknown { verb, tokens } => { - assert_eq!(verb, "INITIALIZE"); + assert_eq!(verb, "CALL"); assert!(!tokens.is_empty()); } other => panic!("se esperaba Unknown, vino {other:?}"), diff --git a/crates/modules/charka/charka-ir/src/model.rs b/crates/modules/charka/charka-ir/src/model.rs index 84e61f8..44ec964 100644 --- a/crates/modules/charka/charka-ir/src/model.rs +++ b/crates/modules/charka/charka-ir/src/model.rs @@ -45,6 +45,16 @@ pub struct ConditionName { pub value: Operand, } +/// Un grupo de datos: su nombre y los datos elementales que contiene +/// (recursivamente). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GroupInfo { + /// Nombre COBOL del grupo, en mayúsculas. + pub name: String, + /// Nombres COBOL de los datos elementales que agrupa. + pub members: Vec, +} + /// El modelo de datos resuelto de un programa. #[derive(Debug, Clone, PartialEq, Default)] pub struct DataModel { @@ -52,6 +62,8 @@ pub struct DataModel { pub fields: Vec, /// Los nombres de condición (nivel 88). pub conditions: Vec, + /// Los grupos y sus datos elementales. + pub groups: Vec, } impl DataModel { @@ -66,6 +78,12 @@ impl DataModel { let up = name.to_uppercase(); self.conditions.iter().find(|c| c.name == up) } + + /// Busca un grupo por su nombre COBOL. + pub fn group(&self, name: &str) -> Option<&GroupInfo> { + let up = name.to_uppercase(); + self.groups.iter().find(|g| g.name == up) + } } /// Aplana el árbol de datos en un [`DataModel`]. @@ -75,9 +93,12 @@ pub fn resolve_data(data: &[DataItem]) -> DataModel { model } -/// Recorre el árbol: registra los 88 como condiciones sobre su dato -/// padre, recurre en los grupos y emite los datos elementales. -fn walk(items: &[DataItem], model: &mut DataModel) { +/// Recorre el árbol: registra los 88 como condiciones, los grupos con +/// sus miembros, y emite los datos elementales. Devuelve los nombres +/// de los datos elementales producidos (para que el grupo que llama +/// los reúna como sus miembros). +fn walk(items: &[DataItem], model: &mut DataModel) -> Vec { + let mut produced = Vec::new(); for it in items { if it.level == 66 || it.level == 88 { // Los 88 los registra su dato padre; los 66 se omiten. @@ -96,13 +117,21 @@ fn walk(items: &[DataItem], model: &mut DataModel) { // Un dato con hijos «reales» (no 88/66) es un grupo. let is_group = it.children.iter().any(|c| c.level != 88 && c.level != 66); if is_group { - walk(&it.children, model); + let members = walk(&it.children, model); + if it.name != "FILLER" { + model.groups.push(GroupInfo { + name: it.name.to_uppercase(), + members: members.clone(), + }); + } + produced.extend(members); } else if it.name != "FILLER" { if let Some(kind) = classify(it.picture.as_deref()) { let init = match kind { FieldKind::Num { .. } => numeric_value(it.value.as_deref()), FieldKind::Text { .. } => text_value(it.value.as_deref()), }; + produced.push(it.name.to_uppercase()); model.fields.push(Field { name: it.name.to_uppercase(), kind, @@ -112,6 +141,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) { } } } + produced } /// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`, diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs index 5c03a9b..ca570d6 100644 --- a/crates/modules/charka/charka-ir/src/stmt.rs +++ b/crates/modules/charka/charka-ir/src/stmt.rs @@ -44,6 +44,7 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt { "STRING" => parse_string(c), "UNSTRING" => parse_unstring(c), "INSPECT" => parse_inspect(c), + "INITIALIZE" => parse_initialize(c), "PERFORM" => parse_perform(c), "GO" => parse_goto(c), "STOP" => parse_stop(c), @@ -390,6 +391,14 @@ fn parse_unstring(c: &mut Cursor) -> Stmt { } } +fn parse_initialize(c: &mut Cursor) -> Stmt { + c.bump(); // INITIALIZE + let mut rounded = false; + let targets = parse_targets(c, &mut rounded); + skip_to_stmt_boundary(c); // p. ej. la cláusula `REPLACING` + Stmt::Initialize { targets } +} + fn parse_inspect(c: &mut Cursor) -> Stmt { c.bump(); // INSPECT let target = parse_operand(c); diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index 0b6e2ba..7bf19f6 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -11,7 +11,7 @@ use charka_ir::{ BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, InspectOp, Ir, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenTest, }; -use charka_runtime::{cobol_text_cmp, Decimal, Rounding}; +use charka_runtime::{cobol_text_cmp, Decimal, Num, Rounding, Text}; use crate::field::{build_fields, Cell}; @@ -293,6 +293,25 @@ impl<'a> Machine<'a> { } Flow::Normal } + Stmt::Initialize { targets } => { + for t in targets { + match t { + Operand::Data(name) => { + match self.ir.model.group(name).map(|g| g.members.clone()) { + Some(members) => { + for m in &members { + self.reset_field(m); + } + } + None => self.reset_field(name), + } + } + Operand::Indexed { .. } => self.reset_element(t), + _ => {} + } + } + Flow::Normal + } Stmt::Perform(p) => self.exec_perform(p), Stmt::GoTo { target } => { // Aproximación: ejecuta el destino y sale del párrafo. @@ -456,6 +475,44 @@ impl<'a> Machine<'a> { } } + /// Resetea un campo completo (escalar o tabla) a su valor por + /// defecto: 0 si es numérico, espacios si es alfanumérico. + fn reset_field(&mut self, name: &str) { + match self.fields.get_mut(&name.to_uppercase()) { + Some(Cell::Num(arr)) => { + for n in arr.iter_mut() { + *n = Num::new(n.picture()); + } + } + Some(Cell::Text(arr)) => { + for t in arr.iter_mut() { + *t = Text::new(t.len()); + } + } + None => {} + } + } + + /// Resetea un solo elemento de tabla a su valor por defecto. + fn reset_element(&mut self, op: &Operand) { + let Some((key, idx)) = self.resolve(op) else { + return; + }; + match self.fields.get_mut(&key) { + Some(Cell::Num(arr)) => { + if let Some(n) = arr.get_mut(idx) { + *n = Num::new(n.picture()); + } + } + Some(Cell::Text(arr)) => { + if let Some(t) = arr.get_mut(idx) { + *t = Text::new(t.len()); + } + } + None => {} + } + } + /// Almacena un texto en un destino, conformándolo a su tipo. fn store_text(&mut self, target: &Operand, text: &str) { let Some((key, idx)) = self.resolve(target) else { diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 259c867..55c84fb 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -123,6 +123,7 @@ mod tests { corpus_test!(corpus_12_cadenas, "12-cadenas"); corpus_test!(corpus_13_inspeccion, "13-inspeccion"); corpus_test!(corpus_14_clasifica, "14-clasifica"); + corpus_test!(corpus_15_resetear, "15-resetear"); #[test] fn empty_source_runs_clean() { diff --git a/crates/modules/charka/corpus/15-resetear.cob b/crates/modules/charka/corpus/15-resetear.cob new file mode 100644 index 0000000..c08d11e --- /dev/null +++ b/crates/modules/charka/corpus/15-resetear.cob @@ -0,0 +1,20 @@ +* corpus charka — nivel 6: INITIALIZE (resetear datos y grupos) +IDENTIFICATION DIVISION. +PROGRAM-ID. RESETEAR. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-REGISTRO. + 05 WS-NOMBRE PIC X(6) VALUE 'PEDRO'. + 05 WS-EDAD PIC 9(3) VALUE 42. +01 WS-CONTADOR PIC 9(3) VALUE 77. +PROCEDURE DIVISION. +MAIN. + DISPLAY 'ANTES NOMBRE=' WS-NOMBRE. + DISPLAY 'ANTES EDAD=' WS-EDAD. + DISPLAY 'ANTES CONT=' WS-CONTADOR. + INITIALIZE WS-REGISTRO. + INITIALIZE WS-CONTADOR. + DISPLAY 'DESP NOMBRE=' WS-NOMBRE. + DISPLAY 'DESP EDAD=' WS-EDAD. + DISPLAY 'DESP CONT=' WS-CONTADOR. + STOP RUN. diff --git a/crates/modules/charka/corpus/15-resetear.expected b/crates/modules/charka/corpus/15-resetear.expected new file mode 100644 index 0000000..bed8f2f --- /dev/null +++ b/crates/modules/charka/corpus/15-resetear.expected @@ -0,0 +1,6 @@ +ANTES NOMBRE=PEDRO +ANTES EDAD=042 +ANTES CONT=077 +DESP NOMBRE= +DESP EDAD=000 +DESP CONT=000 diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index 629c481..2c118d6 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -23,6 +23,7 @@ salida correcta, una línea por `DISPLAY`. | `12-cadenas` | 6 | `STRING` (concatenar) y `UNSTRING` (partir) | | `13-inspeccion` | 6 | `INSPECT` — contar (`TALLYING`) y reemplazar | | `14-clasifica` | 6 | `EVALUATE TRUE` y rangos `WHEN ... THRU` | +| `15-resetear` | 6 | `INITIALIZE` — resetear datos y grupos | ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 5201887..c72a843 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,21 @@ 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): INITIALIZE — resetear datos y grupos + +El verbo de COBOL para volver un dato (o un registro entero) a su +valor por defecto. + +- IR: `Stmt::Initialize { targets }`. El `model` de `charka-ir` + registra ahora los grupos y sus datos elementales + (`DataModel::groups`, `GroupInfo`). +- Parser: `INITIALIZE name-1 name-2 ...`. +- Codegen y shadow: cada destino, si es un grupo, se expande a sus + miembros; cada dato elemental se pone a 0 (numérico) o a espacios + (alfanumérico); una tabla `OCCURS` resetea todos sus elementos. +- Corpus: programa nuevo `15-resetear` (resetea un grupo y un escalar). + Verificado en ambas rutas. + ### feat(charka): EVALUATE TRUE y rangos WHEN ... THRU Completa el `EVALUATE` con sus dos formas que faltaban.