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 { name, members }).
- 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: el intérprete sombra y el crate compilado por scaffold
  dan la misma salida.

Tests: charka-ir 28, charka-codegen 22, charka-shadow 20. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 22:28:47 +00:00
parent 7867d6830e
commit fa65f20206
15 changed files with 260 additions and 17 deletions
@@ -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<Operand> },
/// `PERFORM ...` — ver [`Perform`].
Perform(Perform),
/// `GO TO target`
+23 -3
View File
@@ -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:?}"),
+34 -4
View File
@@ -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<String>,
}
/// 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<Field>,
/// Los nombres de condición (nivel 88).
pub conditions: Vec<ConditionName>,
/// Los grupos y sus datos elementales.
pub groups: Vec<GroupInfo>,
}
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<String> {
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`,
@@ -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);